How does Spring Boot decide which WebServerFactoryCustomizer beans apply to a given factory, and what does the WebServerFactory type hierarchy mean for servlet vs reactive apps?
answer
- ResolvableType reads generic T -> assignable? -> apply
- hierarchy: WebServerFactory -> Configurable(Web/Servlet/Reactive)Factory -> concrete
- servlet branch vs reactive branch
- broad type = portable; concrete type = container features
- stack/container swap = silent no-op
basics
~20 sBoot reads the customizer's generic type parameter T (via ResolvableType) and applies it only if the factory is assignable to T. Broad types like WebServerFactory match everything; specific types like ReactiveWebServerFactory or TomcatServletWebServerFactory match only their branch.
solid answer
~40 sThe WebServerFactoryCustomizerBeanPostProcessor resolves each customizer's generic parameter with Spring's ResolvableType and applies it only when the factory instance is assignable to that type. The factory hierarchy has two branches under WebServerFactory: servlet (ConfigurableServletWebServerFactory, e.g. TomcatServletWebServerFactory) and reactive (ConfigurableReactiveWebServerFactory, e.g. NettyReactiveWebServerFactory). A WebServerFactoryCustomizer<WebServerFactory> runs in both servlet and reactive apps; one typed to ConfigurableServletWebServerFactory runs only in servlet apps; one typed to a concrete container (TomcatServletWebServerFactory) runs only when that container is active. This type-based dispatch is why a customizer 'silently does nothing' after switching container or web stack — the type no longer matches. Choose the broadest type that still exposes the methods you need: container-agnostic settings on ConfigurableWebServerFactory, container-specific on the concrete factory.
code
java · 21 lines// Portable: runs in BOTH servlet and reactive apps; only agnostic setters available.
@Component
class PortableCustomizer
implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
factory.setPort(9000); // available on the agnostic interface
factory.setServerHeader("katajob");
}
}
// Narrow: runs ONLY on Tomcat servlet apps; unlocks Tomcat-specific APIs but
// silently does nothing if you switch to Jetty/Undertow or to WebFlux+Netty.
@Component
class TomcatOnlyCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(c -> c.setProperty("maxConnections", "500"));
}
}go deeper
Not expected to know the dispatch mechanism.
Should at least know servlet and reactive stacks use different factories.
Should explain generic-type filtering and pick a specific-enough type for the methods needed.
Should reason about the full hierarchy, portability vs capability trade-offs, silent no-op failure modes on stack/container migration, and how Boot's own customizers use the same dispatch.
## The dispatch rule `WebServerFactoryCustomizerBeanPostProcessor.postProcessBeforeInitialization` receives the `WebServerFactory` bean and asks, for each candidate customizer bean: *does this factory match the customizer's generic type parameter?* It answers using Spring's `ResolvableType`, which reads the reified generic `T` from `WebServerFactoryCustomizer<T>` at runtime (the type is recoverable because it's part of the class/interface declaration, not an erased local). If the concrete factory is **assignable to** `T`, the customizer applies; otherwise it's skipped. This is pure static-type filtering — no annotations, no conditions. ## The factory type hierarchy Root marker interface: **`WebServerFactory`**. It has a configurable sub-interface **`ConfigurableWebServerFactory`** (`setPort`, `setSsl`, `setCompression`, `setHttp2`, `setShutdown`, error pages). Below that the tree forks: **Servlet branch** - `ServletWebServerFactory` / `ConfigurableServletWebServerFactory` (adds context path, session, MIME mappings…) - Concrete: `TomcatServletWebServerFactory`, `JettyServletWebServerFactory`, `UndertowServletWebServerFactory`. **Reactive branch** (WebFlux, e.g. Netty) - `ReactiveWebServerFactory` / `ConfigurableReactiveWebServerFactory` - Concrete: `NettyReactiveWebServerFactory`, plus reactive Tomcat/Jetty/Undertow variants. ## What each generic choice matches | Customizer typed as… | Applies in… | |---|---| | `WebServerFactoryCustomizer<WebServerFactory>` | every app (servlet **and** reactive) | | `<ConfigurableWebServerFactory>` | every app, but you only get container-agnostic setters | | `<ConfigurableServletWebServerFactory>` | servlet apps only | | `<ConfigurableReactiveWebServerFactory>` | reactive (WebFlux) apps only | | `<TomcatServletWebServerFactory>` | servlet apps running on Tomcat only | ## Design guidance **Choose the broadest type that still exposes the methods you need.** If you only touch port/SSL/compression, type it to `ConfigurableWebServerFactory` (or even `WebServerFactory` and cast is unnecessary) so it survives a container swap. If you need `addAdditionalTomcatConnectors`, you *must* narrow to `TomcatServletWebServerFactory` and accept the coupling. Narrowing is a deliberate trade of portability for capability. ## Why this matters at scale - **Silent no-op on stack change**: moving from spring-boot-starter-web (servlet) to spring-boot-starter-webflux (reactive), or Tomcat→Undertow, leaves servlet/Tomcat-typed customizers dangling — they compile fine and are registered as beans, but never run. There's no warning. Teams get bitten when infra config quietly stops applying. - **Multiple stacks in one deployment**: type dispatch lets a shared library ship both a servlet and a reactive customizer; each only fires where relevant. - **Interaction with Boot's own customizers**: Boot registers stack-appropriate customizers (`ServletWebServerFactoryCustomizer`, `TomcatWebServerFactoryCustomizer`, reactive equivalents) using the same generic-dispatch mechanism — your customizers are peers, ordered together by `AnnotationAwareOrderComparator`. ## Gotchas - Using a raw `WebServerFactoryCustomizer` (no type parameter) resolves `T` to `WebServerFactory`, so it matches everything but only gives you the marker interface — you'd have to `instanceof`-check and cast inside `customize`, which is a code smell versus typing it correctly. - Assignability is one-directional: a `<ConfigurableServletWebServerFactory>` customizer will run for the *more specific* `TomcatServletWebServerFactory` (subtype is assignable), but a `<TomcatServletWebServerFactory>` one will **not** run for a Jetty factory. - The dispatch is per-factory-bean; in an app with only one web stack there's exactly one factory, so exactly one branch's customizers fire.
- A team migrates from spring-boot-starter-web to spring-boot-starter-webflux and a security header set by a WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> disappears. Why, and how do you make it survive?WebFlux uses a reactive factory (e.g. NettyReactiveWebServerFactory), which is not assignable to ConfigurableServletWebServerFactory, so the customizer is filtered out and never runs — silently. Retype it to ConfigurableWebServerFactory (the shared agnostic interface that still exposes setServerHeader) so it matches both branches.
- How does Boot recover the generic type at runtime given type erasure?The type parameter of an implemented interface is retained in the class metadata, not erased like a local generic. Spring's ResolvableType reads WebServerFactoryCustomizer<T> from the bean's class to resolve T, then checks factory assignability against it.
saying these in an interview costs you the question
- Believing all customizers run for every factory regardless of generic type
- Thinking type erasure makes the generic parameter invisible at runtime
- Expecting a servlet-typed or Tomcat-typed customizer to run in a WebFlux/Netty app
- Not realizing a stack/container swap makes narrow customizers silently no-op