Source Code Analysis of Spring Boot and Spring MVC Integrated Startup Process

Recommendation of Open Source Projects

Pepper Metrics It's an open source tool developed by me and my colleagues( https://github.com/zrbcool/pepper-metrics) By collecting the running performance statistics of jedis/mybatis/httpservlet/dubbo/motan, and exposing them to prometheus and other mainstream time series database compatible data, it shows the trend through grafana. Its plug-in architecture is also very convenient for users to extend and integrate other open source components.
Please give us a star, and welcome you to become developers to submit PR to improve the project together.

Start with a simple Spring Boot Web project

We know that it's very easy to write a web project with spring-boot. pom inherits spring-boot-parent, then introduces spring-boot-starter-web dependency, then writes a main starter class like this, and then writes Controller. It's very simple, like this:

@SpringBootApplication
public class SampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }
}
// Then write a Controller to declare a Rest service
@RestController
@RequestMapping("/perf")
public class PerfController {
    @RequestMapping("/trace")
    public Object trace() {
        Object result = yourLogic();
        return result;
    }
}

Talk about Spring Application.run

But we have thought about what spring-boot has done behind it to make our work so simple. How does it integrate spring, spring-mvc and tomcat? Next, we analyze the whole initialization process from the project startup point of view.

PS: In the process of code analysis, emphasis is laid on the concatenation of processes. When a variable is called, the author will directly give the concrete implementation of this variable. The reader may be confused, but don't stop. First, take it for granted to finish the process according to the author's ideas, and then initialize and select the main variables. The process of implementation is explained one by one.

Starting with Spring Application.run:
The method is defined as follows

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return new SpringApplication(primarySources).run(args);
}
public ConfigurableApplicationContext run(String... args) {
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        context = createApplicationContext();//1)
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);//2)
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        listeners.started(context);
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

Let's decompose the run method.
First look at 1)context = createApplicationContext()
Responsible for creating spring main container. This method is implemented dynamically according to the classes that the specific project depends on at run time. If it is a web project, Annotation Config Servlet Web Server Application Context will be selected. As for the rules and reasons of selection, we will ignore them here and introduce them later. ServletWebServerApplicationContext).
Next, let's focus on 2)refreshContext(context) method
The ((AbstractApplicationContext) applicationContext. refresh () method is finally called inside its method, and we expand this method.

@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        prepareRefresh();
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        prepareBeanFactory(beanFactory);
        try {
            postProcessBeanFactory(beanFactory);
            invokeBeanFactoryPostProcessors(beanFactory);
            registerBeanPostProcessors(beanFactory);
            initMessageSource();
            initApplicationEventMulticaster();
            onRefresh();//3)
            registerListeners();
            finishBeanFactoryInitialization(beanFactory);
            finishRefresh();
        }
        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                        "cancelling refresh attempt: " + ex);
            }
            destroyBeans();
            cancelRefresh(ex);
            throw ex;
        }
        finally {
            resetCommonCaches();
        }
    }
}

In fact, our call has come to the spring-context package, which has nothing to do with spring-boot. This is actually the refresh() part of the standard Spring Application Context startup process. We don't decompose the spring startup process, so we only focus on the combination of tomcat and spring-mvc. Part.
Look directly at the 3)onRefresh() method, because Annotation ConfigServlet Web Server Application Context is a subclass of Servlet Web Server Application Context, the process enters the onRefresh() method of Servlet Web Server Application Context.

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

As you can see, this 4)createWebServer() is our key

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        ServletWebServerFactory factory = getWebServerFactory();//5)
        this.webServer = factory.getWebServer(getSelfInitializer());//6)
    }
    else if (servletContext != null) {
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}

Among them:
5)ServletWebServerFactory factory = getWebServerFactory();
The concrete implementation of the above sentence is Tomcat Servlet Web Server Factory. TomcatServletWebServerFactory)
6)this.webServer = factory.getWebServer(getSelfInitializer());
First look at the getSelfInitializer() method in 6:

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);
    }
}

This is a bit interesting. The return is this::selfInitialize. The method definition is to return org. spring framework. boot. web. servlet. ServletContextInitializer. Let's see what it defines.

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

@ Functional Interface is a functional interface supported by lambda in Java 8, selfInitialize. This logic will be called in the later process.
Continue to look at this.webServer = factory.getWebServer(...) in 6. Let's look at the implementation:

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    Tomcat tomcat = new Tomcat();
    File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    Connector connector = new Connector(this.protocol);
    tomcat.getService().addConnector(connector);
    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);//7)
    return getTomcatWebServer(tomcat);
}

You can see that the Tomcat instance is created as an internal implementation of webServer, then injected Connector into Tomcat's Service container, and then set the AutoDeploy attribute of the default Host container and other Tomcat initialization tasks, the most important line being 7)
Let's take a look:

protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
    File documentRoot = getValidDocumentRoot();
    TomcatEmbeddedContext context = new TomcatEmbeddedContext();
    if (documentRoot != null) {
        context.setResources(new LoaderHidingResourceRoot(context));
    }
    ...//Omit some code that we don't care about
    ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);//8)
    host.addChild(context);//Add context to host as a subcontainer of host
    configureContext(context, initializersToUse);//9)
    postProcessContext(context);
}

We can see that it calls host.addChild(context) to add context to host as a subcontainer of host, and then
8) Find all ServletContextInitializer implementations and merge them into an array, then call the 9)configureContext method. Let's see:

protected void configureContext(Context context, ServletContextInitializer[] initializers) {
    TomcatStarter starter = new TomcatStarter(initializers);//10)
    if (context instanceof TomcatEmbeddedContext) {
        TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
        embeddedContext.setStarter(starter);
        embeddedContext.setFailCtxIfServletStartFails(true);
    }
    context.addServletContainerInitializer(starter, NO_CLASSES);//11)
    ...//ignore
}

10) Create the TomcatStarter object and add the starter to the context's conainer Initializer list (see 11), so that the TomcatStarter instance is called during the container startup of tomcat.
Let's see what TomcatStarter did.

class TomcatStarter implements ServletContainerInitializer {
    ...
    private final ServletContextInitializer[] initializers;
    ...
    TomcatStarter(ServletContextInitializer[] initializers) {
        this.initializers = initializers;
    }
    ...
    @Override
    public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
        try {
            for (ServletContextInitializer initializer : this.initializers) {
                initializer.onStartup(servletContext);
            }
        }
        catch (Exception ex) {
            this.startUpException = ex;
            if (logger.isErrorEnabled()) {
                logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
                        + ex.getMessage());
            }
        }
    }
    ...
}

You can see that TomcatStarter is equivalent to hook ing the context-initiated event and then calling the onStartup method of all injected initializers. Seems familiar, right? This is the @Functional Interface function interface mentioned earlier, so let's take a closer look at the onStartup of the initializer mentioned earlier.

//In the Servlet WebServer Application Context class
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);
    }
}

You can see that it calls the onStartup method for each ServletContextInitializerBeans () of getServletContextInitializerBeans().

protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
    return new ServletContextInitializerBeans(getBeanFactory());
}

Look at what new ServletContextInitializer Beans (getBeanFactory ()) have done.

@SafeVarargs
public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
        Class<? extends ServletContextInitializer>... initializerTypes) {
    this.initializers = new LinkedMultiValueMap<>();
    this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
            : Collections.singletonList(ServletContextInitializer.class);
    addServletContextInitializerBeans(beanFactory);
    addAdaptableBeans(beanFactory);
    List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
            .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
            .collect(Collectors.toList());
    this.sortedList = Collections.unmodifiableList(sortedInitializers);
    logMappings(this.initializers);
}

You can see that it retrieves all the ServletContextInitializer implementations from the bean Factory in the spring container. The integration section here refers to the injection process of the ServletRegistrationBean in the ServletRegistrationBean: Spatio-temporal gate: Dispatcher servlet registration bean

private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
    for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
        for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory,
                initializerType)) {
            addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
        }
    }
}
private void addServletContextInitializerBean(String beanName, ServletContextInitializer initializer,
        ListableBeanFactory beanFactory) {
    if (initializer instanceof ServletRegistrationBean) {
        Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();
        addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source);
    }
    else if (initializer instanceof FilterRegistrationBean) {
        Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();
        addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
    }
    else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
        String source = ((DelegatingFilterProxyRegistrationBean) initializer).getTargetBeanName();
        addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
    }
    else if (initializer instanceof ServletListenerRegistrationBean) {
        EventListener source = ((ServletListenerRegistrationBean<?>) initializer).getListener();
        addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source);
    }
    else {
        addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory,
                initializer);
    }
}

Then the process goes smoothly. We call the onStartup method of the Servlet Registration Bean and eventually call the Servlet 3.0 standard of the servletContext.addServlet to inject the Dispatch Servlet into the servlet container to intercept all requests.
See the following code:

//RegistrationBean
@Override
public final void onStartup(ServletContext servletContext) throws ServletException {
    String description = getDescription();
    if (!isEnabled()) {
        logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
        return;
    }
    register(description, servletContext);
}
//DynamicRegistrationBean
@Override
protected final void register(String description, ServletContext servletContext) {
    D registration = addRegistration(description, servletContext);
    if (registration == null) {
        logger.info(
                StringUtils.capitalize(description) + " was not registered " + "(possibly already registered?)");
        return;
    }
    configure(registration);
}
//ServletRegistrationBean
@Override
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
    String name = getServletName();
    return servletContext.addServlet(name, this.servlet);
}

At this point, all integration is completed and the start-up process is handed over to tomcat.

Unfinished Story: How Dependent Components are Initialized

TomcatServletWebServerFactory

There is a section of configuration in spring-boot-autoconfigure/META-INF/spring.factories:

...
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
...

Then let's look at the Servlet WebServerFactoryAutoConfiguration class

@Configuration
@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 {
    ...
}

The @Import section introduces Servlet Web Server FactoryConfiguration. Embedded Tomcat. class for a closer look.

@Configuration
class ServletWebServerFactoryConfiguration {
    @Configuration
    @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
    @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
    public static class EmbeddedTomcat {
        @Bean
        public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
            return new TomcatServletWebServerFactory();
        }
    }
    ...
}

This Spring Book determines whether the current runtime environment is qualified according to @ConditionalOnClass, that is, the jar package containing tomcat. If it is satisfied, it creates a Bean instance of Tomcat Servlet Web Server Factory to be added to the spring container management, which is useful later.

ServletWebServerApplicationContext

When it actually starts, it starts its subclass AnnotationConfigServlet WebServer Application Context. Let's look at the Spring Application class. In fact, Spring Application decides which ApplicationContext to use at runtime, depending on the situation.

View the createApplicationContext() method

So where does this. Web Application Type come from?
Let's look at this construction method.

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

WebApplicationType.deduceFromClasspath() is used to automatically identify this value. Look at the implementation:

static WebApplicationType deduceFromClasspath() {
    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
            && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    for (String className : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    return WebApplicationType.SERVLET;
}

You can see that it is judged by judging whether there are Servlet-related classes in the classloader, so it is judged at runtime.

DispatcherServletRegistrationBean

Dispatcher Servlet Registration Bean is the key to ensuring that our Dispatcher Servlet is injected into the Servlet container and takes effect. Let's see how it is initialized.
There is a section of configuration in spring-boot-autoconfigure/META-INF/spring.factories:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\

Look at the implementation

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
    @Configuration
    @Conditional(DispatcherServletRegistrationCondition.class)
    @ConditionalOnClass(ServletRegistration.class)
    @EnableConfigurationProperties(WebMvcProperties.class)
    @Import(DispatcherServletConfiguration.class)
    protected static class DispatcherServletRegistrationConfiguration {

        private final WebMvcProperties webMvcProperties;

        private final MultipartConfigElement multipartConfig;

        public DispatcherServletRegistrationConfiguration(WebMvcProperties webMvcProperties,
                ObjectProvider<MultipartConfigElement> multipartConfigProvider) {
            this.webMvcProperties = webMvcProperties;
            this.multipartConfig = multipartConfigProvider.getIfAvailable();
        }

        @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
        @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
        public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
            DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
                    this.webMvcProperties.getServlet().getPath());
            registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
            registration.setLoadOnStartup(this.webMvcProperties.getServlet().getLoadOnStartup());
            if (this.multipartConfig != null) {
                registration.setMultipartConfig(this.multipartConfig);
            }
            return registration;
        }

    }
}

As you can see, it registers a bean instance of Dispatcher Servlet Registration Bean like the spring container to see its inheritance relationship:

Its parent ServletRegistrationBean class has the following methods:

@Override
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
    String name = getServletName();
    return servletContext.addServlet(name, this.servlet);
}

It calls the ServletContext.addServlet method to add Dispatch Servlet to the Servlet container, which is the method of registering servlets in Servlet 3.0.
So you might ask, when did addRegistration call?
Based on the inheritance relationship, look at the parent RegistrationBean of its parent class, which has one

@Override
public final void onStartup(ServletContext servletContext) throws ServletException {
    String description = getDescription();
    if (!isEnabled()) {
        logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
        return;
    }
    register(description, servletContext);
}

The register method is a template method that calls the implementation of the subclass DynamicRegistrationBean

@Override
protected final void register(String description, ServletContext servletContext) {
    D registration = addRegistration(description, servletContext);
    if (registration == null) {
        logger.info(StringUtils.capitalize(description) + " was not registered " + "(possibly already registered?)");
        return;
    }
    configure(registration);
}

The addRegistration method is another template method. The implementation is the addRegistration implementation of the previous Servlet Registration Bean. The onStartup method will be invoked in the process of SpringApplication.run() method. As mentioned in the main course, I won't go into any more details here.
In this way, Dispatch Servlet and Tomcat are integrated. Dispatch Servlet uses template method to design patterns and assign specific requests to different handler s. This article will focus on the integration principle of Spring Boot with Spring MVC and Tomcat.

Tags: Java Spring Tomcat Web Server github

Posted on Mon, 26 Aug 2019 20:34:06 -0700 by Rayne