NOTE

Embedded Tomcat Auto-Configuration Principle

1. Enable ServletContainer auto-configuration. 2. Inject the Tomcat container factory. 3. How custom configuration takes effect. 4. How to modify Tomcat configuration. 5. Summary.

JavaCreated Updated 2 min readhistorical

This is a historical learning note and may contain outdated or incomplete understanding.

1. Enable ServletContainer Auto-Configuration

  • @EmbeddedServletContainerAutoConfiguration

EmbeddedServletContainerAutoConfiguration is the auto-configuration class through which Spring Boot automatically configures an embedded servlet container.

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication
@Import(BeanPostProcessorsRegistrar.class)// Imports BeanPostProcessorsRegistrar. This class is key to how custom Tomcat configuration takes effect
public class EmbeddedServletContainerAutoConfiguration {

    @Configuration
    @ConditionalOnClass({ Servlet.class, Tomcat.class })// Takes effect when Tomcat.class exists -- use Tomcat
    @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class,
            search = SearchStrategy.CURRENT)// Takes effect when the user has not customized EmbeddedServletContainerFactory
    public static class EmbeddedTomcat {

        // First inject a Tomcat factory into the Spring container
        @Bean
        public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
            return new TomcatEmbeddedServletContainerFactory();
        }

    }

    @Configuration
    @ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
            WebAppContext.class })// Takes effect when Loader.class exists -- use Jetty
    @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class,
            search = SearchStrategy.CURRENT)
    public static class EmbeddedJetty {

        @Bean
        public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
            return new JettyEmbeddedServletContainerFactory();
        }

    }
}

It determines which servlet container to create based on whether Tomcat or Jetty exists on the classpath.

We use Tomcat’s TomcatEmbeddedServletContainerFactory as an example.

2. Inject the Tomcat Container Factory

2.1. What the Tomcat Container Factory Looks Like

  • Create Tomcat
public class TomcatEmbeddedServletContainerFactory
        extends AbstractEmbeddedServletContainerFactory implements ResourceLoaderAware {

        @Override
        public EmbeddedServletContainer getEmbeddedServletContainer(
                ServletContextInitializer... initializers) {
            // Create Tomcat
            Tomcat tomcat = new Tomcat();
            // The following configures Tomcat's basic properties

            // Base directory: if not configured, create a temporary directory
            File baseDir = (this.baseDirectory != null) ? this.baseDirectory
                    : createTempDir("tomcat");
            tomcat.setBaseDir(baseDir.getAbsolutePath());

            // HTTP connector
            Connector connector = new Connector(this.protocol);
            tomcat.getService().addConnector(connector);

            // Key method for custom configuration
            customizeConnector(connector);

            tomcat.setConnector(connector);
            tomcat.getHost().setAutoDeploy(false);
            configureEngine(tomcat.getEngine());
            for (Connector additionalConnector : this.additionalTomcatConnectors) {
                tomcat.getService().addConnector(additionalConnector);
            }
            prepareContext(tomcat.getHost(), initializers);

            //new TomcatEmbeddedServletContainer(tomcat, getPort() >= 0);
            return getTomcatEmbeddedServletContainer(tomcat);
        }

        protected void customizeConnector(Connector connector) {
            int port = (getPort() >= 0) ? getPort() : 0;
            connector.setPort(port);
            if (StringUtils.hasText(this.getServerHeader())) {
                connector.setAttribute("server", this.getServerHeader());
            }
            if (connector.getProtocolHandler() instanceof AbstractProtocol) {
                customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler());
            }
            if (getUriEncoding() != null) {
                connector.setURIEncoding(getUriEncoding().name());
            }

            // If ApplicationContext is slow to start we want Tomcat not to bind to the socket
            // prematurely...
            connector.setProperty("bindOnInit", "false");

            if (getSsl() != null && getSsl().isEnabled()) {
                customizeSsl(connector);
            }
            if (getCompression() != null && getCompression().getEnabled()) {
                customizeCompression(connector);
            }
            // Get all TomcatConnectorCustomizers and call customize to configure them
            // ServerProperties is one of them
            for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) {
                customizer.customize(connector);
            }
        }
}

The role of TomcatEmbeddedServletContainerFactory is to create the Tomcat container. Its key method is getEmbeddedServletContainer. So when is this method called?

2.2. When Is Tomcat Created?

2.2.1. Refresh the Container in a Web Environment

We know that one step during Spring context refresh is onRefresh. It is a template method implemented by subclasses.

In a web environment, EmbeddedWebApplicationContext.onRefresh is called.

  • EmbeddedWebApplicationContext.onRefresh
protected void onRefresh() {
        super.onRefresh();
    try {
        // Create the servlet container
        createEmbeddedServletContainer();
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start embedded container",
                ex);
    }
}

private void createEmbeddedServletContainer() {
    // The first time this is entered, both are null
    EmbeddedServletContainer localContainer = this.embeddedServletContainer;
    ServletContext localServletContext = getServletContext();
    if (localContainer == null && localServletContext == null) {
        // Get the servlet-container factory; this calls @Bean TomcatEmbeddedServletContainerFactory
        EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();
        // Create the servlet container through the factory
        this.embeddedServletContainer = containerFactory
                // Call TomcatEmbeddedServletContainerFactory#getEmbeddedServletContainer
                .getEmbeddedServletContainer(getSelfInitializer());
    }
    else if (localServletContext != null) {
        try {
            getSelfInitializer().onStartup(localServletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context",
                    ex);
        }
    }
    initPropertySources();
}

2.3. Tomcat Creation Process

  • TomcatEmbeddedServletContainer
public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer {

    public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) {
        Assert.notNull(tomcat, "Tomcat Server must not be null");
        this.tomcat = tomcat;
        this.autoStart = autoStart;
        // Initialize Tomcat
        initialize();
    }

    private void initialize() throws EmbeddedServletContainerException {
        TomcatEmbeddedServletContainer.logger
                .info("Tomcat initialized with port(s): " + getPortsDescription(false));
        synchronized (this.monitor) {
            try {
                addInstanceIdToEngineName();
                try {
                    final Context context = findContext();
                    context.addLifecycleListener(new LifecycleListener() {

                        @Override
                        public void lifecycleEvent(LifecycleEvent event) {
                            if (context.equals(event.getSource())
                                    && Lifecycle.START_EVENT.equals(event.getType())) {
                                // Remove service connectors so that protocol
                                // binding doesn't happen when the service is
                                // started.
                                removeServiceConnectors();
                            }
                        }

                    });

                    // Start the Tomcat server
                    // Start the server to trigger initialization listeners
                    this.tomcat.start();

                    // We can re-throw failure exception directly in the main thread
                    rethrowDeferredStartupExceptions();

                    try {
                        ContextBindings.bindClassLoader(context, getNamingToken(context),
                                getClass().getClassLoader());
                    }
                    catch (NamingException ex) {
                        // Naming is not enabled. Continue
                    }

                    // Unlike Jetty, all Tomcat threads are daemon threads. We create a
                    // blocking non-daemon to stop immediate shutdown
                    startDaemonAwaitThread();
                }
                catch (Exception ex) {
                    containerCounter.decrementAndGet();
                    throw ex;
                }
            }
            catch (Exception ex) {
                stopSilently();
                throw new EmbeddedServletContainerException(
                        "Unable to start embedded Tomcat", ex);
            }
        }
    }
}

Besides injecting the servlet container, it also imports a BeanPostProcessorsRegistrar class, which is key to custom configuration.

3. Principle Behind Custom Configuration Taking Effect

Besides creating and starting the Tomcat container, EmbeddedServletContainerAutoConfiguration also imports a key class, BeanPostProcessorsRegistrar.

3.1. Inject BeanPostProcessorsRegistrar for Custom Tomcat Configuration

public static class BeanPostProcessorsRegistrar
        implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
            @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
            BeanDefinitionRegistry registry) {
        if (this.beanFactory == null) {
            return;
        }
        registerSyntheticBeanIfMissing(registry,
                "embeddedServletContainerCustomizerBeanPostProcessor",
                // Import this BeanPostProcessor
                // After bean instantiation and property assignment, when custom initialization methods are called,
                // postProcessBeforeInitialization and postProcessAfterInitialization are invoked
                EmbeddedServletContainerCustomizerBeanPostProcessor.class);
        registerSyntheticBeanIfMissing(registry,
                "errorPageRegistrarBeanPostProcessor",
                ErrorPageRegistrarBeanPostProcessor.class);
    }
}

3.1.1. After the Tomcat Bean Is Instantiated, Call postProcessBeforeInitialization for Custom Configuration

  • EmbeddedServletContainerCustomizerBeanPostProcessor
public class EmbeddedServletContainerCustomizerBeanPostProcessor
        implements BeanPostProcessor, BeanFactoryAware {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean instanceof ConfigurableEmbeddedServletContainer) {
            // Pass in ConfigurableEmbeddedServletContainer
            postProcessBeforeInitialization((ConfigurableEmbeddedServletContainer) bean);
        }
        return bean;
    }
    private void postProcessBeforeInitialization(
            ConfigurableEmbeddedServletContainer bean) {
        // Get all customizers and call customize
        for (EmbeddedServletContainerCustomizer customizer : getCustomizers()) {
            customizer.customize(bean);
        }
    }

    private Collection<EmbeddedServletContainerCustomizer> getCustomizers() {
        if (this.customizers == null) {
            // Get EmbeddedServletContainerCustomizer classes from the bean factory by type, and sort them
            // This is the key to our custom Tomcat container configuration
            this.customizers = new ArrayList<EmbeddedServletContainerCustomizer>(
                    this.beanFactory
                            .getBeansOfType(EmbeddedServletContainerCustomizer.class,
                                    false, false)
                            .values());
            Collections.sort(this.customizers, AnnotationAwareOrderComparator.INSTANCE);
            this.customizers = Collections.unmodifiableList(this.customizers);
        }
        return this.customizers;
    }


    // Not very useful; simply return the bean
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        return bean;
    }
}

4. How to Modify Tomcat Configuration

  • Configuration file
server:
  port: 8000
  • Configuration class
@Bean
public EmbeddedServletContainerCustomizer embeddedServletContainerCustomizer()
{
    return new EmbeddedServletContainerCustomizer()
    {
        @Override
        public void customize(ConfigurableEmbeddedServletContainer configurableEmbeddedServletContainer)
        {
            configurableEmbeddedServletContainer.setPort(8000);
        }
    };
}

4.1. Principle Behind the Configuration Taking Effect After Modification

  • Configuration-file approach
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties
    // ServerProperties corresponding to the configuration file is essentially an EmbeddedServletContainerCustomizer
    implements EmbeddedServletContainerCustomizer, EnvironmentAware, Ordered
  • Configuration-class approach

The principle is the same.

5. Summary

  • During Spring startup, one step of refreshing the context is invokeBeanFactoryPostProcessors, which calls the postProcessBeanDefinitionRegistry methods of all BeanFactoryPostProcessor instances.
  • One of them is ConfigurationClassPostProcessor. Its postProcessBeanDefinitionRegistry method parses the @Import annotations of all @Configuration classes, calls the selectImport methods of ImportSelector instances, and calls the registerBeanDefinitions methods of ImportBeanDefinitionRegistrar instances.
  • BeanPostProcessorsRegistrar, imported by @Configuration EmbeddedServletContainerAutoConfiguration, is one of them. It registers the EmbeddedServletContainerCustomizerBeanPostProcessor BeanPostProcessor in the container.
  • The EmbeddedServletContainerAutoConfiguration class used by Spring Boot to auto-configure Tomcat will:
  • One step during Spring context refresh is onRefresh, which calls the onRefresh method of the subclass EmbeddedWebApplicationContext.
  • This onRefresh method calls @Bean TomcatEmbeddedServletContainerFactory and EmbeddedServletContainerFactory.getEmbeddedServletContainer.
  • getEmbeddedServletContainer creates the Tomcat container, configures it (the customize methods of all TomcatConnectorCustomizer instances, including ServerProperties), and starts Tomcat.
  • One step during Spring context refresh is finishBeanFactoryInitialization. When the Tomcat container is instantiated, its properties are assigned, and custom methods are called, the postProcessBeforeInitialization method of EmbeddedServletContainerCustomizerBeanPostProcessor is called. It calls the customize methods of all EmbeddedServletContainerCustomizer instances to customize Tomcat.

Discussion

Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub