DEV Community

Silver_dev
Silver_dev

Posted on

Spring Boot Under the Hood. Part 7: From Embedded Tomcat to Your @RestController

We've reached the point where refresh() calls onRefresh() on the context. For web applications, onRefresh() is where the embedded web server is created and started — we saw this moment on the diagram in Part 6. Let's break down how Spring Boot picks the server, creates the DispatcherServlet, and handles errors.

7.1. Servlet vs Reactive — Two Parallel Worlds

Spring Boot supports two stacks:

Stack WebApplicationType Default server Context
Spring MVC SERVLET Tomcat AnnotationConfigServletWebServerApplicationContext
Spring WebFlux REACTIVE Reactor Netty AnnotationConfigReactiveWebServerApplicationContext

The choice happens in the SpringApplication constructor via deduceWebApplicationType(). If DispatcherServlet (Spring MVC) is on the classpath → SERVLET. If DispatcherHandler (WebFlux) is present without MVC → REACTIVE.

Important: if both are on the classpath, SERVLET wins. Spring Boot gives MVC priority as the "older" stack.

7.2. Web Server Auto-configuration — ServletWebServerFactoryAutoConfiguration

The main class for the Servlet stack:

@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ 
    ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
    ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
    ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
    ServletWebServerFactoryConfiguration.EmbeddedUndertow.class 
})
public class ServletWebServerFactoryAutoConfiguration {

    @Bean
    public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(
            ServerProperties serverProperties) {
        return new ServletWebServerFactoryCustomizer(serverProperties);
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) — the server is created before most other auto-configurations.
  • @Import pulls in three nested configurations — for Tomcat, Jetty, and Undertow. Each one is activated via @ConditionalOnClass.

7.2.1. EmbeddedTomcat — Picking the Server

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, 
    search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {

    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory(
            ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
            ObjectProvider<TomcatContextCustomizer> contextCustomizers,
            ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
        // ...
        return factory;
    }
}
Enter fullscreen mode Exit fullscreen mode

The selection logic:

  1. If Tomcat.class is on the classpath and there's no custom ServletWebServerFactory — a TomcatServletWebServerFactory is created.
  2. If you swap Tomcat for spring-boot-starter-jetty — @ConditionalOnClass(Tomcat.class) fails, and EmbeddedJetty kicks in.
  3. If you declare your own ServletWebServerFactory — @ConditionalOnMissingBean backs off, and your factory wins.

7.3. ServletWebServerApplicationContext.onRefresh() — Creating the Server

The entry point is onRefresh() in ServletWebServerApplicationContext:

@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createWebServer();
    } catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}
Enter fullscreen mode Exit fullscreen mode

createWebServer():

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        StartupStep createWebServer = getApplicationStartup()
            .start("spring.boot.webserver.create");
        ServletWebServerFactory factory = getWebServerFactory();
        this.webServer = factory.getWebServer(getSelfInitializer());
        createWebServer.tag("factory", factory.getClass().toString());
        getBeanFactory().registerSingleton("webServerGracefulShutdown", 
            new WebServerGracefulShutdownLifecycle(this.webServer));
        getBeanFactory().registerSingleton("webServerStartStop", 
            new WebServerStartStopLifecycle(this, this.webServer));
    } else if (servletContext != null) {
        // ... embedded container
    }
}
Enter fullscreen mode Exit fullscreen mode

Key steps:

  1. getWebServerFactory() — fetches the ServletWebServerFactory from the context.
  2. factory.getWebServer(getSelfInitializer()) — creates the WebServer and passes a callback for initializing the ServletContext.
  3. Two lifecycle beans are registered:
    • webServerStartStop — starts/stops the server
    • webServerGracefulShutdown — graceful shutdown

7.3.1. getSelfInitializer() — the Bridge to ServletContextInitializer

private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
    return this::selfInitialize;
}

private void selfInitialize(ServletContext servletContext) throws ServletException {
    prepareWebApplicationContext(servletContext);
    registerApplicationScope(servletContext);
    WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
    for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
        beans.onStartup(servletContext);
    }
}
Enter fullscreen mode Exit fullscreen mode

ServletContextInitializer is a Spring Boot SPI interface:

@FunctionalInterface
public interface ServletContextInitializer {
    void onStartup(ServletContext servletContext) throws ServletException;
}
Enter fullscreen mode Exit fullscreen mode

Every bean implementing ServletContextInitializer is invoked when the server starts. This is exactly how the DispatcherServlet, filters, and listeners get registered.

7.3.2. ServletContextInitializerBeans — Collecting All the Initializers

public class ServletContextInitializerBeans 
        extends AbstractCollection<ServletContextInitializer> {

    public ServletContextInitializerBeans(ListableBeanFactory beanFactory, 
            Class<? extends ServletContextInitializer>... initializerTypes) {
        // 1. Collects all ServletContextInitializer beans
        // 2. Collects all ServletRegistrationBean, FilterRegistrationBean,
        //    ServletListenerRegistrationBean instances
        // 3. Sorts them via @Order / Ordered
    }
}
Enter fullscreen mode Exit fullscreen mode

What ends up in here:

  • DispatcherServletRegistrationBean — registers the DispatcherServlet (from DispatcherServletAutoConfiguration)
  • Custom Filters (Spring Security, etc.)
  • @WebServlet, @WebFilter, @WebListener (with @ServletComponentScan)

7.4. WebServerStartStopLifecycle — Starting Tomcat

Once the WebServer is created, a lifecycle bean is registered:

class WebServerStartStopLifecycle implements SmartLifecycle {
    private final ServletWebServerApplicationContext applicationContext;
    private final WebServer webServer;
    private volatile boolean running;

    @Override
    public void start() {
        this.webServer.start();
        this.running = true;
        this.applicationContext.publishEvent(
            new ServletWebServerInitializedEvent(this.webServer, this.applicationContext));
    }

    @Override
    public void stop() {
        this.webServer.stop();
        this.running = false;
    }

    @Override
    public int getPhase() {
        return Integer.MAX_VALUE - 1;  // starts almost last
    }
}
Enter fullscreen mode Exit fullscreen mode

SmartLifecycle callbacks fire in finishRefresh() → lifecycleProcessor.onRefresh(). getPhase() = Integer.MAX_VALUE - 1 means the server starts after all the other lifecycle beans.

The order:

  • refresh() → finishRefresh() → lifecycleProcessor.onRefresh()
  • WebServerStartStopLifecycle.start() → tomcat.start()
  • ServletWebServerInitializedEvent is published

7.5. DispatcherServletAutoConfiguration — Creating the DispatcherServlet

@AutoConfiguration(after = ServletWebServerFactoryAutoConfiguration.class)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(DispatcherServlet.class)
    @EnableConfigurationProperties(WebMvcProperties.class)
    protected static class DispatcherServletConfiguration {

        @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
            DispatcherServlet dispatcherServlet = new DispatcherServlet();
            dispatcherServlet.setDispatchOptionsRequest(
                webMvcProperties.isDispatchOptionsRequest());
            dispatcherServlet.setDispatchTraceRequest(
                webMvcProperties.isDispatchTraceRequest());
            // ...
            return dispatcherServlet;
        }

        @Bean
        public DispatcherServletRegistrationBean dispatcherServletRegistration(
                DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties,
                ObjectProvider<MultipartConfigElement> multipartConfig) {
            DispatcherServletRegistrationBean registration = 
                new DispatcherServletRegistrationBean(dispatcherServlet, 
                    webMvcProperties.getServlet().getPath());
            registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
            registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
            multipartConfig.ifAvailable(registration::setMultipartConfig);
            return registration;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Two key beans:

  • dispatcherServlet — the DispatcherServlet itself.
  • dispatcherServletRegistration — a DispatcherServletRegistrationBean, which implements ServletContextInitializer. When the server starts, it registers the DispatcherServlet in the ServletContext.

Ordering: @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) guarantees that the DispatcherServlet is created after the ServletWebServerFactory has been defined.

7.6. WebMvcAutoConfiguration — Wiring Up Spring MVC

@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, 
    TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public InternalResourceViewResolver defaultViewResolver() { ... }

    @Bean
    @ConditionalOnMissingBean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter(...) { ... }

    @Bean
    @ConditionalOnMissingBean
    public RequestMappingHandlerMapping requestMappingHandlerMapping(...) { ... }

    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(...) { ... }
}
Enter fullscreen mode Exit fullscreen mode

@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) — the key point. If you add @EnableWebMvc, Spring Boot backs off, and the entire MVC auto-configuration is disabled. You get vanilla Spring MVC without Boot's customizations.

What WebMvcAutoConfiguration creates:

  • RequestMappingHandlerMapping — mapping URLs → controller methods
  • RequestMappingHandlerAdapter — invoking controller methods
  • HttpMessageConverters — Jackson, String, ByteArray, etc.
  • InternalResourceViewResolver — for JSPs
  • MessageSource — i18n
  • Validator — JSR-380

7.7. ErrorMvcAutoConfiguration — Error Handling

/error is not part of Spring MVC. It's a Spring Boot add-on.

@AutoConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
    public DefaultErrorAttributes errorAttributes() {
        return new DefaultErrorAttributes();
    }

    @Bean
    @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
            ObjectProvider<ErrorViewResolver> errorViewResolvers) {
        return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
            errorViewResolvers.orderedStream().toList());
    }

    @Bean
    public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
        return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
    }
}
Enter fullscreen mode Exit fullscreen mode

7.7.1. BasicErrorController — the /error Handler

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(
            getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, 
            getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }
}
Enter fullscreen mode Exit fullscreen mode

What BasicErrorController does:

  • Handles /error
  • If Accept: text/html → renders an HTML page (the Whitelabel Error Page)
  • If Accept: application/json → returns JSON

7.7.2. DefaultErrorAttributes — the Error Model

public class DefaultErrorAttributes implements ErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, 
            ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        addStatus(errorAttributes, webRequest);
        addErrorDetails(errorAttributes, webRequest, options);
        addPath(errorAttributes, webRequest, options);
        return errorAttributes;
    }
}
Enter fullscreen mode Exit fullscreen mode

The standard JSON:

{
    "timestamp": "2026-09-23T12:00:00.000+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "...",
    "path": "/api/order"
}
Enter fullscreen mode Exit fullscreen mode

7.7.3. ErrorPageCustomizer — Registering the Error Page

private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {

    @Override
    public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
        ErrorPage errorPage = new ErrorPage(
            this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
        errorPageRegistry.addErrorPages(errorPage);
    }
}
Enter fullscreen mode Exit fullscreen mode

It registers /error as the error page in Tomcat. When Tomcat catches an unhandled exception (or a 404/405), it forwards the request to /error, where BasicErrorController produces the response.

The chain:

Controller → DispatcherServlet → HandlerExceptionResolver → not handled
    → Tomcat → forward to /error → BasicErrorController
Enter fullscreen mode Exit fullscreen mode

7.8. Server Customization — ServerProperties

All the server.* settings are read into ServerProperties:

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
    private Integer port;
    private InetAddress address;
    private String contextPath;
    private final Servlet servlet = new Servlet();
    private final Tomcat tomcat = new Tomcat();
    private final Jetty jetty = new Jetty();
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Key settings:

Property What it does
server.port the port (8080 by default)
server.address the address to listen on
server.servlet.context-path the application's root path
server.servlet.session.timeout session timeout
server.tomcat.threads.max Tomcat's max threads
server.tomcat.threads.min-spare min spare threads
server.shutdown=graceful graceful shutdown
spring.lifecycle.timeout-per-shutdown-phase graceful shutdown timeout

7.8.1. ServletWebServerFactoryCustomizer — Applying the Properties

public class ServletWebServerFactoryCustomizer 
        implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {

    private final ServerProperties serverProperties;

    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
        PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
        map.from(this.serverProperties::getPort).to(factory::setPort);
        map.from(this.serverProperties::getAddress).to(factory::setAddress);
        map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath);
        map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
        map.from(this.serverProperties::getSsl).to(factory::setSsl);
        map.from(this.serverProperties::getCompression).to(factory::setCompression);
        map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
        map.from(this.serverProperties::getError).to(factory::setErrorPages);
    }

    @Override
    public int getOrder() {
        return 0;  // before user-defined customizers
    }
}
Enter fullscreen mode Exit fullscreen mode

@Order(0) — auto-configured customizers run before user-defined ones. If you want to override a setting, declare your own WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> — it will run later and overwrite the value.

7.8.2. Your Own WebServerFactoryCustomizer

@Component
public class MyTomcatCustomizer 
        implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.addConnectorCustomizers(connector -> {
            connector.setProperty("maxKeepAliveRequests", "100");
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

7.9. The Reactive Stack — WebFlux and Netty

7.9.1. ReactiveWebServerFactoryAutoConfiguration

@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ReactiveHttpInputMessage.class)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ 
    ReactiveWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
    ReactiveWebServerFactoryConfiguration.EmbeddedTomcat.class,
    ReactiveWebServerFactoryConfiguration.EmbeddedJetty.class,
    ReactiveWebServerFactoryConfiguration.EmbeddedUndertow.class,
    ReactiveWebServerFactoryConfiguration.EmbeddedNetty.class 
})
public class ReactiveWebServerFactoryAutoConfiguration {

    @Bean
    public ReactiveWebServerFactoryCustomizer reactiveWebServerFactoryCustomizer(
            ServerProperties serverProperties) {
        return new ReactiveWebServerFactoryCustomizer(serverProperties);
    }
}
Enter fullscreen mode Exit fullscreen mode

EmbeddedNetty is activated via @ConditionalOnClass({ HttpServer.class, ... }) and creates a NettyReactiveWebServerFactory.

7.9.2. NettyReactiveWebServerFactory

public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFactory {

    @Override
    public WebServer getWebServer(HttpHandler httpHandler) {
        HttpServer httpServer = createHttpServer();
        // ... configuration
        return new NettyWebServer(httpServer, handler, 
            getRouteProvider(), this::getServerShutdownTimeout);
    }
}
Enter fullscreen mode Exit fullscreen mode

The HttpHandler is built by WebHttpHandlerBuilder, which wires up WebFlux's DispatcherHandler. No Servlet API — Netty talks to the HttpHandler directly.

7.9.3. Key Differences from Servlet

Aspect Servlet (Tomcat) Reactive (Netty)
Model Thread-per-request Event loop
API ServletContext, HttpServletRequest ServerHttpRequest, ServerHttpResponse
Handler DispatcherServlet DispatcherHandler
Errors /error + BasicErrorController DefaultErrorWebExceptionHandler
Lifecycle WebServerStartStopLifecycle NettyWebServer

7.10. Spring Boot 3 vs Spring Boot 4 — Key Differences

7.10.1. Modularization

The biggest change. In Boot 3, everything lives in spring-boot-autoconfigure. In Boot 4, there are 47 modules:

Boot 3 Boot 4
spring-boot-autoconfigure (everything) spring-boot-webmvc (MVC)
spring-boot-webflux (WebFlux)
spring-boot-tomcat (Tomcat)
spring-boot-jetty (Jetty)
spring-boot-jackson (Jackson)
spring-boot-starter-web spring-boot-starter-webmvc (renamed)
spring-boot-starter-webflux spring-boot-starter-webflux (unchanged)

The auto-configuration packages have moved too:

  • org.springframework.boot.autoconfigure.web.servlet → org.springframework.boot.webmvc.autoconfigure
  • org.springframework.boot.autoconfigure.web.reactive → org.springframework.boot.webflux.autoconfigure

7.10.2. Undertow Is Gone

Spring Boot 4 requires a Servlet 6.1 baseline, and Undertow isn't compatible with it. Undertow has been removed entirely:

"Spring Boot 4.0 requires a Servlet 6.1 baseline, with which Undertow is not yet compatible. As a result, Undertow support is dropped, including the Undertow starter and the ability to use Undertow as an embedded server."

What remains: Tomcat, Jetty, and Netty (for WebFlux).

7.10.3. Class Relocations

The embedded server classes have moved:

Boot 3 Boot 4
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory org.springframework.boot.tomcat.TomcatServletWebServerFactory
org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory org.springframework.boot.netty.NettyReactiveWebServerFactory

7.10.4. What Hasn't Changed

  • The ServletWebServerFactory mechanism — the same abstraction.
  • ServletContextInitializer — the same SPI.
  • ErrorMvcAutoConfiguration / BasicErrorController — the same logic.
  • WebMvcAutoConfiguration — the same beans, just in different packages.
  • WebServerFactoryCustomizer — the same interface.

7.11. Diagram: Creating the Web Server (Servlet)

refresh()
  └─ AbstractApplicationContext.refresh()
        ├─ ...
        └─ onRefresh()                          ← overridden in ServletWebServerApplicationContext
              │
              └─ createWebServer()
                    ├─ getWebServerFactory()
                    │     └─ TomcatServletWebServerFactory (from ServletWebServerFactoryAutoConfiguration)
                    │
                    ├─ factory.getWebServer(getSelfInitializer())
                    │     └─ TomcatServletWebServerFactory.getWebServer()
                    │           ├─ new Tomcat()
                    │           ├─ context = tomcat.addContext()
                    │           ├─ initializer.onStartup(servletContext)   ← the callback
                    │           │     └─ ServletContextInitializerBeans
                    │           │           ├─ DispatcherServletRegistrationBean
                    │           │           ├─ FilterRegistrationBean
                    │           │           └─ ServletListenerRegistrationBean
                    │           └─ return new TomcatWebServer(tomcat)
                    │
                    ├─ registerSingleton("webServerStartStop", 
                    │     new WebServerStartStopLifecycle(...))
                    │
                    └─ registerSingleton("webServerGracefulShutdown", 
                          new WebServerGracefulShutdownLifecycle(...))
        │
        └─ finishRefresh()
              └─ lifecycleProcessor.onRefresh()
                    └─ WebServerStartStopLifecycle.start()    ← phase = MAX-1
                          ├─ tomcat.start()
                          ├─ publishEvent(ServletWebServerInitializedEvent)
                          └─ running = true
Enter fullscreen mode Exit fullscreen mode

7.12. Key Takeaways

  1. ServletWebServerFactoryAutoConfiguration picks Tomcat/Jetty/Undertow from the classpath. @AutoConfigureOrder(HIGHEST_PRECEDENCE).
  2. onRefresh() → createWebServer() is where the server is born. It runs inside refresh(), before finishRefresh().
  3. ServletContextInitializer is the SPI for registering servlets, filters, and listeners. DispatcherServletRegistrationBean is one of its implementations.
  4. WebServerStartStopLifecycle is a SmartLifecycle with phase = MAX-1 — it starts Tomcat after all the other beans.
  5. DispatcherServletAutoConfiguration creates the DispatcherServlet and its registration. Ordered after ServletWebServerFactoryAutoConfiguration.
  6. WebMvcAutoConfiguration wires up MVC: HandlerMapping, HandlerAdapter, HttpMessageConverters. Opt out via @EnableWebMvc.
  7. ErrorMvcAutoConfiguration — /error + BasicErrorController. This isn't MVC; it's a Boot add-on.
  8. ServletWebServerFactoryCustomizer applies the server.* properties. @Order(0) — user-defined customizers run after it.
  9. The reactive stack: NettyReactiveWebServerFactory + DispatcherHandler. A different model, different APIs. 10 Boot 4: modularization (47 modules), spring-boot-starter-webmvc, Undertow removed, packages relocated. The mechanism itself — unchanged.

Top comments (0)