SpringBoot启动流程简要分析

声明

源码基于Spring Boot 2.3.12.RELEASE

背景

此文的目的主要想弄明白为什么在Spring Boot中注册ServletFilterListener组件时需要加上@ServletComponentScan注解才能生效。

启动分析

Spring Boot应用程序的启动类一般如下所示

  • jar包启动
@SpringBootApplication
public class BootApplication {

    public static void main(String[] args) {
        SpringApplication.run(BootApplication.class, args);
    }
}
  • war包启动
@SpringBootApplication
public class BootApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(BootApplication.class);
    }
}

下面主要分析jar包启动流程,war包其实最终走的也是jar包启动逻辑。只不过入口不是在main方法中调用而已,当war包部署在外部容器时,servlet容器会通过SPI寻找ServletContainerInitializer接口实现,然后调用它的onStartup方法。在spring-webjar包中,META-INF/service目录下便指定了一个实现类SpringServletContainerInitializer,入口便是该类了,该类会调用SpringApplication中的run方法。

现在来看SpringApplication

public static ConfigurableApplicationContext run(Class<?> primarySource,
                                                 String... args) {
    return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources,
                                                 String[] args) {
    return new SpringApplication(primarySources).run(args);
}

构造方法

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // 配置类,一般为启动类
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 推断应用程序类型,主要根据类路径存不存在指定的类来推断,如SERVLET、REACTIVE、NONE
    // 不同的类型会对应不同的ApplicationContext实现
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 从spring.factories文件中获取ApplicationContextInitializer实现
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 从spring.factories文件中获取ApplicationListener实现
    // 注意这里的监听器不会注册到Spring容器中,而是在Spring Boot启动中依次触发,独立于Srping容器上下文
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // 推断启动类
    this.mainApplicationClass = deduceMainApplicationClass();
}

核心实例run方法

public ConfigurableApplicationContext run(String... args) {
    // 用于记录启动时间
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // Spring容器
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    // 用于管理SpringBoot启动过程中的声明周期,目前只有一个实现类
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // starting生命周期
    listeners.starting();
    try {
        // 封装参数
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 构建环境
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        configureIgnoreBeanInfo(environment);
        Banner printedBanner = printBanner(environment);
        // 创建容器
        context = createApplicationContext();
        // 配置容器
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        // 刷新容器,实际调用applicationContext.refresh方法
        refreshContext(context);
        // 空实现
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        // started生命周期
        listeners.started(context);
        // 调用ApplicationRunner、CommandLineRunner
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // running生命周期
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

getRunListeners方法

/**
 * 从spring.factories获取SpringApplicationRunListener实现,管理启动过程中的生命周期
 * 目前只有EventPublishingRunListener一个实现,用于广播事件,与前文构造方法中提到的监听器一起工作
 * 比如加载application.yml文件到环境中,就是通过它实现的
 */
private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    return new SpringApplicationRunListeners(logger,
                                             getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

prepareEnvironment方法

private ConfigurableEnvironment prepareEnvironment(
    SpringApplicationRunListeners listeners,
    ApplicationArguments applicationArguments) {
    // Create and configure the environment
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach(environment);
    /*
     * 触发environmentPrepared生命周期,会广播ApplicationEnvironmentPreparedEvent事件
     * 从而触发ConfigFileApplicationListener监听器,加载application.yml文件的属性到
     * 环境中
     */
    listeners.environmentPrepared(environment);
    bindToSpringApplication(environment);
    if (!this.isCustomEnvironment) {
        environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                                                                                               deduceEnvironmentClass());
    }
    ConfigurationPropertySources.attach(environment);
    return environment;
}

createApplicationContext方法

/**
 * 创建ApplicationContext对象
 * servlet环境,实现类AnnotationConfigServletWebServerApplicationContext
 * 响应式环境, 实现类AnnotationConfigReactiveWebServerApplicationContext
 * 其它, 实现类AnnotationConfigApplicationContext
 */
protected ConfigurableApplicationContext createApplicationContext() {
    Class<?> contextClass = this.applicationContextClass;
    if (contextClass == null) {
        try {
            switch (this.webApplicationType) {
                case SERVLET:
                    contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
                    break;
                case REACTIVE:
                    contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
                    break;
                default:
                    contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
            }
        }
        catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                "Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
        }
    }
    return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

prepareContext方法

/**
 * 主要对ApplicationContext做一些初始化
 */
private void prepareContext(
    ConfigurableApplicationContext context,
    ConfigurableEnvironment environment,
    SpringApplicationRunListeners listeners,
    ApplicationArguments applicationArguments,
    Banner printedBanner) {
    // 设置环境
    context.setEnvironment(environment);
    postProcessApplicationContext(context);
    // 调用前文构造方法中提到的ApplicationContextInitializer实例,对context做一些配置
    applyInitializers(context);
    listeners.contextPrepared(context);
    if (this.logStartupInfo) {
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
    }
    // Add boot specific singleton beans
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
    if (beanFactory instanceof DefaultListableBeanFactory) {
        ((DefaultListableBeanFactory) beanFactory)
        .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
    }
    if (this.lazyInitialization) {
        context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
    }
    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    // 注册配置类
    load(context, sources.toArray(new Object[0]));
    listeners.contextLoaded(context);
}

refreshContext方法

private void refreshContext(ConfigurableApplicationContext context) {
    // 注册钩子,jvm退出时会回调
    if (this.registerShutdownHook) {
        try {
            context.registerShutdownHook();
        }
        catch (AccessControlException ex) {
            // Not allowed in some environments.
        }
    }
    /*
     * 刷新容器,这里会创建tomcat容器,实例化IOC容器中所有的单例bean
     */
    refresh((ApplicationContext) context);
}

protected void refresh(ConfigurableApplicationContext applicationContext) {
    applicationContext.refresh();
}

tomcat内嵌容器创建

createApplicationContext方法可知,servlet环境中,Spring容器实现类为AnnotationConfigServletWebServerApplicationContext,便来看看它的refresh方法。

该方法继承自父类AbstractApplicationContext,也就是Spring IOC容器的核心方法。

@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // Prepare this context for refreshing.
        prepareRefresh();

        // Tell the subclass to refresh the internal bean factory.
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);

        try {
            // Allows post-processing of the bean factory in context subclasses.
            postProcessBeanFactory(beanFactory);

            // Invoke factory processors registered as beans in the context.
            invokeBeanFactoryPostProcessors(beanFactory);

            // Register bean processors that intercept bean creation.
            registerBeanPostProcessors(beanFactory);

            // Initialize message source for this context.
            initMessageSource();

            // Initialize event multicaster for this context.
            initApplicationEventMulticaster();

            /**
             * 子类实现
             */
            onRefresh();

            // Check for listener beans and register them.
            registerListeners();

            // 初始化单例bean
            finishBeanFactoryInitialization(beanFactory);

            // Last step: publish corresponding event.
            finishRefresh();
        }

        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
            }

            // Destroy already created singletons to avoid dangling resources.
            destroyBeans();

            // Reset 'active' flag.
            cancelRefresh(ex);

            // Propagate exception to caller.
            throw ex;
        }

        finally {
            // Reset common introspection caches in Spring's core, since we
            // might not ever need metadata for singleton beans anymore...
            resetCommonCaches();
        }
    }
}

再看AnnotationConfigServletWebServerApplicationContextonRefresh方法。继承自ServletWebServerApplicationContext

@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        // 创建WebServer
        createWebServer();
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}
private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    // 内嵌容器这两个都是null,外部容器servletContext不为null
    if (webServer == null && servletContext == null) {
        // 从IOC容器中获取工厂
        ServletWebServerFactory factory = getWebServerFactory();
        // 创建WebServer,其中getSelfInitializer是注册servlet,filter组件的核心入口
        this.webServer = factory.getWebServer(getSelfInitializer());
        getBeanFactory().registerSingleton("webServerGracefulShutdown",
                                           new WebServerGracefulShutdownLifecycle(this.webServer));
        getBeanFactory().registerSingleton("webServerStartStop",
                                           new WebServerStartStopLifecycle(this, this.webServer));
    }
    else if (servletContext != null) {
        try {
            // 外部容器手动调用
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}
/**
 * 从IOC容器获取WebServer工厂
 * WebServer工厂的自动配置类ServletWebServerFactoryAutoConfiguration
 * 主要注册了tomcat、jetty、Undertow这3个servlet容器工厂,默认使用tomcat
 */
protected ServletWebServerFactory getWebServerFactory() {
    // Use bean names so that we don't consider the hierarchy
    String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
    if (beanNames.length == 0) {
        throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
                                              + "ServletWebServerFactory bean.");
    }
    if (beanNames.length > 1) {
        throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
                                              + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
    }
    return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

getWebServer方法,以tomcat为例,TomcatServletWebServerFactory.java

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
    if (this.disableMBeanRegistry) {
        Registry.disableRegistry();
    }
    Tomcat tomcat = new Tomcat();
    File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
    tomcat.setBaseDir(baseDir.getAbsolutePath());
    Connector connector = new Connector(this.protocol);
    connector.setThrowOnFailure(true);
    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);
    return getTomcatWebServer(tomcat);
}
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
    File documentRoot = getValidDocumentRoot();
    TomcatEmbeddedContext context = new TomcatEmbeddedContext();
    if (documentRoot != null) {
        context.setResources(new LoaderHidingResourceRoot(context));
    }
    context.setName(getContextPath());
    context.setDisplayName(getDisplayName());
    context.setPath(getContextPath());
    File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
    // 熟悉的名字,tomcat部署应用程序的根目录
    context.setDocBase(docBase.getAbsolutePath());
    context.addLifecycleListener(new FixContextListener());
    context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
                                 : ClassUtils.getDefaultClassLoader());
    resetDefaultLocaleMapping(context);
    addLocaleMappings(context);
    try {
        context.setCreateUploadTargets(true);
    }
    catch (NoSuchMethodError ex) {
        // Tomcat is < 8.5.39. Continue.
    }
    configureTldPatterns(context);
    WebappLoader loader = new WebappLoader();
    loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
    loader.setDelegate(true);
    context.setLoader(loader);
    if (isRegisterDefaultServlet()) {
        addDefaultServlet(context);
    }
    if (shouldRegisterJspServlet()) {
        addJspServlet(context);
        addJasperInitializer(context);
    }
    context.addLifecycleListener(new StaticResourceConfigurer(context));
    ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
    host.addChild(context);
    configureContext(context, initializersToUse);
    postProcessContext(context);
}
protected void configureContext(Context context, 
                                ServletContextInitializer[] initializers) {
    // ServletContainerInitializer实现,并设置ServletContextInitializer列表
    TomcatStarter starter = new TomcatStarter(initializers);
    if (context instanceof TomcatEmbeddedContext) {
        TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
        embeddedContext.setStarter(starter);
        embeddedContext.setFailCtxIfServletStartFails(true);
    }
    /*
     * 添加ServletContainerInitializer实现
     * tomcat启动时会异步调用ServletContainerInitializer实例的onStartup方法
     * 而TomcatStarter内部维护了ServletContextInitializer列表,依次调用
     * ServletContextInitializer实例的onStartup方法,从而注册Servlet、Filter组件
     */
    context.addServletContainerInitializer(starter, NO_CLASSES);
    for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
        context.addLifecycleListener(lifecycleListener);
    }
    for (Valve valve : this.contextValves) {
        context.getPipeline().addValve(valve);
    }
    for (ErrorPage errorPage : getErrorPages()) {
        org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
        tomcatErrorPage.setLocation(errorPage.getPath());
        tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
        tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
        context.addErrorPage(tomcatErrorPage);
    }
    for (MimeMappings.Mapping mapping : getMimeMappings()) {
        context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
    }
    configureSession(context);
    new DisableReferenceClearingContextCustomizer().customize(context);
    for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
        customizer.customize(context);
    }
}
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
    return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
    Assert.notNull(tomcat, "Tomcat Server must not be null");
    this.tomcat = tomcat;
    this.autoStart = autoStart;
    this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
    initialize();
}

private void initialize() throws WebServerException {
    logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
    synchronized (this.monitor) {
        try {
            addInstanceIdToEngineName();

            Context context = findContext();
            context.addLifecycleListener((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();
                }
            });

            // 该方法会导致ServletContainerInitializer实例的onStartup执行
            // 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, context.getNamingToken(), 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) {
            stopSilently();
            destroySilently();
            throw new WebServerException("Unable to start embedded Tomcat", ex);
        }
    }
}

总结

下面主要总结下比较在意的点

  • SpringApplication在创建时会从spring.factories文件中获取ApplicationContextInitializer以及ApplicationListener实现,其中ApplicationContextInitializer可以对ApplicationContext做一些定制。
  • run方法中接着从spring.factories文件中获取SpringApplicationRunListener实现,目前只有一个实现类,即EventPublishingRunListener,与前面ApplicationListener搭配使用,用于广播事件以及监听。这里的事件监听与ApplicationContext中的事件监听是相互独立的,毕竟有些事件触发时,应用上下文ApplicationContext还没有初始化好。其中application.yml属性文件加载到Environment中便是借助它实现的,SpringApplicationprepareEnvironment方法执行了environmentPrepared方法,从而广播了ApplicationEnvironmentPreparedEvent事件,而ConfigFileApplicationListener监听了该事件,会去执行application.yml属性文件的加载逻辑。
  • Environment初始化了之后,会创建ApplicationContext,对于Servlet环境,无论时内嵌容器还是运行在外部容器,实现类都是AnnotationConfigServletWebServerApplicationContext
  • 接着会调用refresh方法对ApplicationContext进行初始化,而refresh方法内部会调用onRefresh方法,然后再实例化单例bean。AnnotationConfigServletWebServerApplicationContext类在onRefresh方法中会实例化tomcat容器(默认是tomcat),其中添加了一个TomcatStarter类,这是一个ServletContainerInitializer实现,tomcat启动时会异步调用ServletContainerInitializer实例的onStartup方法,而TomcatStarteronStartup方法则会执行Spring Boot中ServletContextInitializer实例的onStartup方法,Spring Boot中注册ServletFilterListener便是通过ServletContextInitializer实现。这么做的目的是为了统一jar包运行和war包运行的差异,tomcat注册时只会在应用的部署目录搜索带有@WebServlet@WebFilter注解的类,war包运行时这没有任何问题,但是jar包运行时,我们的类文件并没有在tomcat指定的部署目录中,因此不会被注册。

黑魔法

为了验证上面所说的想法,来做以下一个实验,在上述tomcat创建过程时,会指定tomcat的docBase目录,即应用程序的部署目录,默认情况下是一个临时目录,为了方便测试,便指定一个目录。

/**
 * 实现WebServerFactoryCustomizer接口,可自定义WebServerFactory
 * 处理逻辑在ServletWebServerFactoryAutoConfiguration自动配置类中,
 * 往容器中注册了WebServerFactoryCustomizerBeanPostProcessor
 */
@Component
public class WebServerFactoryConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.setDocumentRoot(new File("D:\\Deploy"));
    }
}
package com.wangtao.tomcat;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet({"/test"})
public class TestServlet extends HttpServlet {

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("=================TestServlet============");
        System.out.println(req.getRequestURI());
    }
}

然后往D:\Deploy目录中增加以下内容WEB-INF\classes\com\wangtao\tomcat\TestServlet.class,记得项目中本身不要有这个类,要的效果就是tomcat能不能加载到这个类。

测试类

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        // tomcat类加载器
        System.out.println(Thread.currentThread().getContextClassLoader());
        TomcatEmbeddedWebappClassLoader classLoader = (TomcatEmbeddedWebappClassLoader) Thread.currentThread().getContextClassLoader();
        // file:/D:/Deploy/WEB-INF/classes/
        Arrays.stream(classLoader.getURLs()).forEach(System.out::println);
        // true
        System.out.println(ClassUtils.isPresent("com.wangtao.tomcat.FirstFilter", classLoader));
        // 系统类加载器
        System.out.println(HelloController.class.getClassLoader());
        // false
        System.out.println(ClassUtils.isPresent("com.wangtao.tomcat.FirstFilter", HelloController.class.getClassLoader()));
        return "Hello, Spring Boot!";
    }
}

从结果中可以看到,内嵌tomcat确实可以加载到部署目录下的类文件,当开开心心的去访问这个Servlet时却发现访问不了,然而tomcat类加载器确实可以加载到它呀,经过多番查找资料,原来tomcat扫描web.xml以及@WebServlet等这些注解是通过ContextConfig组件实现的,但是Spring Boot创建tomcat时并没有加入该组件,于是虽然可以加载到类,但是相当于是一个普通的类罢了,并没有注册成Servlet。于是改写配置,如下所示

@Component
public class WebServerFactoryConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        // 指定docBase目录
        factory.setDocumentRoot(new File("D:\\Deploy"));
        /* 
         * 添加生命周期组件,用于解析扫描web.xml、@WebFilter、@WebServlet等注解
         * 从类路径中使用SPI扫描ServletContainerInitializer实现添加到tomcat中
         */
        factory.addContextLifecycleListeners(new ContextConfig());
    }
}

再次访问,便能看到控制台的打印了。

posted on 2022-10-09 22:16  wastonl  阅读(472)  评论(0编辑  收藏  举报