今天咱们来放松下心情,不聊分布式,云原生,来聊一聊初学者接触的最多的 java web 基础。几乎全部人都是从 servlet,jsp,filter 开始编写本身的第一个 hello world 工程。那时,还离不开 web.xml 的配置,在 xml 文件中编写繁琐的 servlet 和 filter 的配置。随着 spring 的普及,配置逐渐演变成了两种方式—java configuration 和 xml 配置共存。现现在,springboot 的普及,java configuration 成了主流,xml 配置彷佛已经“灭绝”了。不知道你有没有好奇过,这中间都发生了哪些改变,web.xml 中的配置项又是被什么替代项取代了?
java
为了体现出整个演进过程,仍是来回顾下 n 年前咱们是怎么写 servlet 和 filter 代码的。git
项目结构(本文都采用 maven 项目结构)github
public class HelloWorldServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/plain"); PrintWriter out = resp.getWriter(); out.println("hello world"); }
public class HelloWorldFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("触发 hello world 过滤器..."); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { } }
别忘了在 web.xml 中配置 servlet 和 filterweb
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>HelloWorldServlet</servlet-name> <servlet-class>moe.cnkirito.servlet.HelloWorldServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <filter> <filter-name>HelloWorldFilter</filter-name> <filter-class>moe.cnkirito.filter.HelloWorldFilter</filter-class> </filter> <filter-mapping> <filter-name>HelloWorldFilter</filter-name> <url-pattern>/hello</url-pattern> </filter-mapping> </web-app>
这样,一个 java web hello world 就完成了。固然,本文不是 servlet 的入门教程,只是为了对比。算法
Servlet 3.0 做为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一块儿发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中一项新特性即是提供了无 xml 配置的特性。spring
servlet3.0 首先提供了 @WebServlet,@WebFilter 等注解,这样便有了抛弃 web.xml 的第一个途径,凭借注解声明 servlet 和 filter 来作到这一点。数组
除了这种方式,servlet3.0 规范还提供了更强大的功能,能够在运行时动态注册 servlet ,filter,listener。以 servlet 为例,过滤器与监听器与之相似。ServletContext 为动态配置 Servlet 增长了以下方法:tomcat
其中前三个方法的做用是相同的,只是参数类型不一样而已;经过 createServlet() 方法建立的 Servlet,一般须要作一些自定义的配置,而后使用 addServlet() 方法来将其动态注册为一个能够用于服务的 Servlet。两个 getServletRegistration() 方法主要用于动态为 Servlet 增长映射信息,这等价于在 web.xml 中使用 标签为存在的 Servlet 增长映射信息。springboot
以上 ServletContext 新增的方法要么是在 ServletContextListener 的 contexInitialized 方法中调用,要么是在 ServletContainerInitializer 的 onStartup() 方法中调用。app
ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,而且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup() 方法处理,咱们一般须要在该实现类上使用 @HandlesTypes 注解来指定但愿被处理的类,过滤掉不但愿给 onStartup() 处理的类。
一个典型的 servlet3.0+ 的 web 项目结构以下:
. ├── pom.xml └── src ├── main │ ├── java │ │ └── moe │ │ └── cnkirito │ │ ├── CustomServletContainerInitializer.java │ │ ├── filter │ │ │ └── HelloWorldFilter.java │ │ └── servlet │ │ └── HelloWorldServlet.java │ └── resources │ └── META-INF │ └── services │ └── javax.servlet.ServletContainerInitializer └── test └── java |
我并未对 HelloWorldServlet 和 HelloWorldFilter 作任何改动,而是新增了一个 CustomServletContainerInitializer ,它实现了 javax.servlet.ServletContainerInitializer
接口,用来在 web 容器启动时加载指定的 servlet 和 filter,代码以下:
public class CustomServletContainerInitializer implements ServletContainerInitializer { private final static String JAR_HELLO_URL = "/hello"; @Override public void onStartup(Set<Class<?>> c, ServletContext servletContext) { System.out.println("建立 helloWorldServlet..."); ServletRegistration.Dynamic servlet = servletContext.addServlet( HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class); servlet.addMapping(JAR_HELLO_URL); System.out.println("建立 helloWorldFilter..."); FilterRegistration.Dynamic filter = servletContext.addFilter( HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class); EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class); dispatcherTypes.add(DispatcherType.REQUEST); dispatcherTypes.add(DispatcherType.FORWARD); filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL); } }
对上述代码进行一些解读。ServletContext 咱们称之为 servlet 上下文,它维护了整个 web 容器中注册的 servlet,filter,listener,以 servlet 为例,可使用 servletContext.addServlet 等方法来添加 servlet。而方法入参中 Set<Class<?>> c 和 @HandlesTypes 注解在 demo 中我并未使用,感兴趣的朋友能够 debug 看看到底获取了哪些 class ,通常正常的流程是使用 @HandlesTypes 指定须要处理的 class,然后对 Set<Class<?>> 进行判断是否属于该 class,正如前文所言,onStartup 会加载不须要被处理的一些 class。
这么声明一个 ServletContainerInitializer 的实现类,web 容器并不会识别它,因此,须要借助 SPI 机制来指定该初始化类,这一步骤是经过在项目路径下建立 META-INF/services/javax.servlet.ServletContainerInitializer
来作到的,它只包含一行内容
moe.cnkirito.CustomServletContainerInitializer
使用 ServletContainerInitializer 和 SPI 机制,咱们的 web 应用即可以完全摆脱 web.xml 了。
回到咱们的 spring 全家桶,可能已经忘了具体是何时开始不写 web.xml 了,我只知道如今的项目已经再也看不到它了,spring 又是如何支持 servlet3.0 规范的呢?
寻找 spring 中 ServletContainerInitializer 的实现类并不困难,能够迅速定位到 SpringServletContainerInitializer 该实现类。
@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>(); if (webAppInitializerClasses != null) { for (Class<?> waiClass : webAppInitializerClasses) { // Be defensive: Some servlet containers provide us with invalid classes, // no matter what @HandlesTypes says... // <1> if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { try { initializers.add((WebApplicationInitializer) waiClass.newInstance()); } catch (Throwable ex) { throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex); } } } } if (initializers.isEmpty()) { servletContext.log("No Spring WebApplicationInitializer types detected on classpath"); return; } servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath"); AnnotationAwareOrderComparator.sort(initializers); // <2> for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); } } }
查看其 java doc,描述以下:
Servlet 3.0 {@link ServletContainerInitializer} designed to support code-based configuration of the servlet container using Spring’s {@link WebApplicationInitializer} SPI as opposed to (or possibly in combination with) the traditional {@code web.xml}-based approach.
注意我在源码中标注两个序号,这对于咱们理解 spring 装配 servlet 的流程来讲很是重要。
<1> 英文注释是 spring 源码中自带的,它提示咱们因为 servlet 厂商实现的差别,onStartup 方法会加载咱们本不想处理的 class,因此进行了特判。
<2> spring 与咱们以前的 demo 不一样,并无在 SpringServletContainerInitializer 中直接对 servlet 和 filter 进行注册,而是委托给了一个陌生的类 WebApplicationInitializer ,WebApplicationInitializer 类即是 spring 用来初始化 web 环境的委托者类,它一般有三个实现类:
你必定不会对 dispatcherServlet 感到陌生,AbstractDispatcherServletInitializer#registerDispatcherServlet 即是无 web.xml 前提下建立 dispatcherServlet 的关键代码。
能够去项目中寻找一下 org.springframework:spring-web:version 的依赖,它下面就存在一个 servletContainerInitializer 的扩展,指向了 SpringServletContainerInitializer,这样只要在 servlet3.0 环境下部署,spring 即可以自动加载进行初始化:
注意,上述这一切特性从 spring 3 就已经存在了,而现在 spring 5 已经伴随 springboot 2.0 一块儿发行了。
读到这儿,你已经阅读了全文的 1/2。springboot 对于 servlet 的处理才是重头戏,其一,是由于 springboot 使用范围很广,不多有人用 spring 而不用 springboot 了;其二,是由于它没有彻底遵照 servlet3.0 的规范!
是的,前面所讲述的 servlet 的规范,不管是 web.xml 中的配置,仍是 servlet3.0 中的 ServletContainerInitializer 和 springboot 的加载流程都没有太大的关联。按照惯例,先卖个关子,先看看如何在 springboot 中注册 servlet 和 filter,再来解释下 springboot 的独特之处。
springboot 依旧兼容 servlet3.0 一系列以 @Web* 开头的注解:@WebServlet,@WebFilter,@WebListener
@WebServlet("/hello") public class HelloWorldServlet extends HttpServlet{}
@WebFilter("/hello/*") public class HelloWorldFilter implements Filter {} |
不要忘记让启动类去扫描到这些注解
@SpringBootApplication @ServletComponentScan public class SpringBootServletApplication { public static void main(String[] args) { SpringApplication.run(SpringBootServletApplication.class, args); } } |
我认为这是几种方式中最为简洁的方式,若是真的有特殊需求,须要在 springboot 下注册 servlet,filter,能够采用这样的方式,比较直观。
@Bean public ServletRegistrationBean helloWorldServlet() { ServletRegistrationBean helloWorldServlet = new ServletRegistrationBean(); myServlet.addUrlMappings("/hello"); myServlet.setServlet(new HelloWorldServlet()); return helloWorldServlet; } @Bean public FilterRegistrationBean helloWorldFilter() { FilterRegistrationBean helloWorldFilter = new FilterRegistrationBean(); myFilter.addUrlPatterns("/hello/*"); myFilter.setFilter(new HelloWorldFilter()); return helloWorldFilter; } |
ServletRegistrationBean 和 FilterRegistrationBean 都集成自 RegistrationBean ,RegistrationBean 是 springboot 中普遍应用的一个注册类,负责把 servlet,filter,listener 给容器化,使他们被 spring 托管,而且完成自身对 web 容器的注册。这种注册方式也值得推崇。
从图中能够看出 RegistrationBean 的地位,它的几个实现类做用分别是:帮助容器注册 filter,servlet,listener,最后的 DelegatingFilterProxyRegistrationBean 使用的很少,但熟悉 SpringSecurity 的朋友不会感到陌生,SpringSecurityFilterChain 就是经过这个代理类来调用的。另外 RegistrationBean 实现了 ServletContextInitializer 接口,这个接口将会是下面分析的核心接口,你们先混个眼熟,了解下它有一个抽象实现 RegistrationBean 便可。
暂时只介绍这两种方式,下面解释下以前卖的关子,为何说 springboot 没有彻底遵照 servlet3.0 规范。讨论的前提是 springboot 环境下使用内嵌的容器,好比最典型的 tomcat。高能预警,如下内容比较烧脑,以为看起来吃力的朋友能够跳过本节直接看下一节的总结!
当使用内嵌的 tomcat 时,你会发现 springboot 彻底走了另外一套初始化流程,彻底没有使用前面提到的 SpringServletContainerInitializer,实际上一开始我在各类 ServletContainerInitializer 的实现类中打了断点,最终定位到,根本没有运行到 SpringServletContainerInitializer 内部,而是进入了 TomcatStarter 这个类中。
而且,仔细扫了一眼源码的包,并无发现有 SPI 文件对应到 TomcatStarter。因而我猜测,内嵌 tomcat 的加载可能不依赖于 servlet3.0 规范和 SPI!它彻底走了一套独立的逻辑。为了验证这一点,我翻阅了 spring github 中的 issue,获得了 spring 做者确定的答复:https://github.com/spring-projects/spring-boot/issues/321
This was actually an intentional design decision. The search algorithm used by the containers was problematic. It also causes problems when you want to develop an executable WAR as you often want a
javax.servlet.ServletContainerInitializer
for the WAR that is not executed when you runjava -jar
.See the
org.springframework.boot.context.embedded.ServletContextInitializer
for an option that works with Spring Beans.
springboot 这么作是有意而为之。springboot 考虑到了以下的问题,咱们在使用 springboot 时,开发阶段通常都是使用内嵌 tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另外一种是打成 war 包,交给外置容器去运行。前者就会致使容器搜索算法出现问题,由于这是 jar 包的运行策略,不会按照 servlet3.0 的策略去加载 ServletContainerInitializer!最后做者还提供了一个替代选项:ServletContextInitializer,注意是 ServletContextInitializer!它和 ServletContainerInitializer 长得特别像,别搞混淆了,前者 ServletContextInitializer 是 org.springframework.boot.web.servlet.ServletContextInitializer,后者 ServletContainerInitializer 是 javax.servlet.ServletContainerInitializer,前文还提到 RegistrationBean 实现了 ServletContextInitializer 接口。
TomcatStarter 中的 org.springframework.boot.context.embedded.ServletContextInitializer
是 springboot 初始化 servlet,filter,listener 的关键。
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 { for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); } } } |
通过删减源码后,能够看出 TomcatStarter 的主要逻辑,它其实就是负责调用一系列 ServletContextInitializer 的 onStartup 方法,那么在 debug 中,ServletContextInitializer[] initializers 到底包含了哪些类呢?会不会有咱们前面介绍的 RegisterBean 呢?
太天真了,RegisterBean 并无出如今 TomcatStarter 的 debug 信息中,initializers 只包含了三个类,其中只有第一个类看上去比较核心,注意第一个类不是 EmbeddedWebApplicationContext!而是这个类中的 $1 匿名类,为了搞清楚 springboot 如何加载 filter servlet listener ,看来还得研究下 EmbeddedWebApplicationContext 的结构。
ApplicationContext 你们应该是比较熟悉的,这是 spring 一个比较核心的类,通常咱们能够从中获取到那些注册在容器中的托管 Bean,而这篇文章,主要分析的即是它在内嵌容器中的实现类:EmbeddedWebApplicationContext,重点分析它加载 filter servlet listener 这部分的代码。这里是整个代码中迭代层次最深的部分,作好心理准备起航,来看看 EmbeddedWebApplicationContext 是怎么获取到全部的 servlet filter listener 的!如下方法均出自于 EmbeddedWebApplicationContext。
第一层:onRefresh()
onRefresh 是 ApplicationContext 的生命周期方法,EmbeddedWebApplicationContext 的实现很是简单,只干了一件事:
@Override protected void onRefresh() { super.onRefresh(); try { createEmbeddedServletContainer();//第二层的入口 } catch (Throwable ex) { throw new ApplicationContextException("Unable to start embedded container", ex); } } |
createEmbeddedServletContainer 链接到了第二层
第二层:createEmbeddedServletContainer()
看名字 spring 是想建立一个内嵌的 servlet 容器,ServletContainer 其实就是 servlet filter listener 的总称。
private void createEmbeddedServletContainer() { EmbeddedServletContainer localContainer = this.embeddedServletContainer; ServletContext localServletContext = getServletContext(); if (localContainer == null && localServletContext == null) { EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory(); this.embeddedServletContainer = containerFactory .getEmbeddedServletContainer(getSelfInitializer());//第三层的入口 } else if (localServletContext != null) { try { getSelfInitializer().onStartup(localServletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources(); } |
凡是带有 servlet,initializer 字样的方法都是咱们须要留意的,getSelfInitializer() 便涉及到了咱们最为关心的初始化流程。
第三层:getSelfInitializer()
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { return new ServletContextInitializer() { @Override public void onStartup(ServletContext servletContext) throws ServletException { selfInitialize(servletContext); } }; } private void selfInitialize(ServletContext servletContext) throws ServletException { prepareEmbeddedWebApplicationContext(servletContext); ConfigurableListableBeanFactory beanFactory = getBeanFactory(); ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes( beanFactory); WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, getServletContext()); existingScopes.restore(); WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, getServletContext()); //第四层的入口 for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); } } |
还记得前面 TomcatStarter 的 debug 信息中,第一个 ServletContextInitializer 就是出如今 EmbeddedWebApplicationContext 中的一个匿名类,没错了,就是这里的 getSelfInitializer() 方法建立的!解释下这里的 getSelfInitializer() 和 selfInitialize(ServletContext servletContext) 为何要这么设计:这是典型的回调式方式,当匿名 ServletContextInitializer 类被 TomcatStarter 的 onStartup 方法调用,设计上是触发了 selfInitialize(ServletContext servletContext) 的调用。因此这下就清晰了,为何 TomcatStarter 中没有出现 RegisterBean ,实际上是隐式触发了 EmbeddedWebApplicationContext 中的 selfInitialize 方法。selfInitialize 方法中的 getServletContextInitializerBeans() 成了关键。
第四层:getServletContextInitializerBeans()
/** * Returns {@link ServletContextInitializer}s that should be used with the embedded * Servlet context. By default this method will first attempt to find * {@link ServletContextInitializer}, {@link Servlet}, {@link Filter} and certain * {@link EventListener} beans. * @return the servlet initializer beans */ protected Collection<ServletContextInitializer> getServletContextInitializerBeans() { return new ServletContextInitializerBeans(getBeanFactory());//第五层的入口 } |
没错了,注释都告诉咱们,这个 ServletContextInitializerBeans 是用来加载 Servlet 和 Filter 的。
第五层:ServletContextInitializerBeans的构造方法
public ServletContextInitializerBeans(ListableBeanFactory beanFactory) { this.initializers = new LinkedMultiValueMap<Class<?>, ServletContextInitializer>(); addServletContextInitializerBeans(beanFactory);// 第六层的入口 addAdaptableBeans(beanFactory); List<ServletContextInitializer> sortedInitializers = new ArrayList<ServletContextInitializer>(); for (Map.Entry<?, List<ServletContextInitializer>> entry : this.initializers .entrySet()) { AnnotationAwareOrderComparator.sort(entry.getValue()); sortedInitializers.addAll(entry.getValue()); } this.sortedList = Collections.unmodifiableList(sortedInitializers); } |
第六层:addServletContextInitializerBeans(beanFactory)
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) { for (Entry<String, ServletContextInitializer> initializerBean : getOrderedBeansOfType( beanFactory, ServletContextInitializer.class)) { addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory); } } |
getOrderedBeansOfType 方法即是去容器中寻找注册过得 ServletContextInitializer ,这时候就能够把以前那些 RegisterBean 所有加载出来了,而且 RegisterBean 还实现了 Ordered 接口,在这儿用于排序。再也不往下迭代了。
若是你对具体的代码流程不感兴趣,能够跳过上述的6层分析,直接看本节的结论。总结以下:
研究完了上述 springboot 启动的内部原理,能够发现 ServletContextInitializer 实际上是 spring 中 ServletContainerInitializer 的代理,虽然 springboot 中 Servlet3.0 不起做用了,但它的代理仍是会被加载的,因而咱们有了第三种方式注册 servlet。
@Configuration public class CustomServletContextInitializer implements ServletContextInitializer { private final static String JAR_HELLO_URL = "/hello"; @Override public void onStartup(ServletContext servletContext) throws ServletException { System.out.println("建立 helloWorldServlet..."); ServletRegistration.Dynamic servlet = servletContext.addServlet( HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class); servlet.addMapping(JAR_HELLO_URL); System.out.println("建立 helloWorldFilter..."); FilterRegistration.Dynamic filter = servletContext.addFilter( HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class); EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class); dispatcherTypes.add(DispatcherType.REQUEST); dispatcherTypes.add(DispatcherType.FORWARD); filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL); } } |
虽然 ServletCantainerInitializer 不能被内嵌容器加载,ServletContextInitializer 却能被 springboot 的 EmbeddedWebApplicationContext 加载到,从而装配其中的 servlet 和 filter。实际开发中,仍是以一,二两种方法来注册为主,这里只是提供一个可能性,来让咱们理解 springboot 的加载流程。
天然是被 new 出来的,在 TomcatEmbeddedServletContainerFactory#configureContext 中能够看到,TomcatStarter 是被主动实例化出来的,而且还传入了 ServletContextInitializer 的数组,和上面分析的同样,一共有三个 ServletContextInitializer,包含了 EmbeddedWebApplicationContext 中的匿名实现。
protected void configureContext(Context context, ServletContextInitializer[] initializers) { TomcatStarter starter = new TomcatStarter(initializers); if (context instanceof TomcatEmbeddedContext) { // Should be true ((TomcatEmbeddedContext) context).setStarter(starter); } context.addServletContainerInitializer(starter, NO_CLASSES); ... } } |
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Configuration @ConditionalOnWebApplication @Import(BeanPostProcessorsRegistrar.class) public class EmbeddedServletContainerAutoConfiguration { /** * Nested configuration if Tomcat is being used. */ @Configuration @ConditionalOnClass({ Servlet.class, Tomcat.class }) @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT) public static class EmbeddedTomcat { @Bean public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() { return new TomcatEmbeddedServletContainerFactory(); } } } |
只要类路径下存在 Tomcat 类,以及在 web 环境下,就会触发 springboot 的自动配置。
存在 web.xml 配置的 java web 项目,servlet3.0 的 java web 项目,springboot 内嵌容器的 java web 项目加载 servlet,filter,listener 的流程都是有所差别的,理解清楚这其中的原来,其实并不容易,至少得搞懂 servlet3.0 的规范,springboot 内嵌容器的加载流程等等前置逻辑。
最后感谢下小马哥的点拨,在此以前误觉得: TomcatStarter 既然继承了 ServletContainerInitializer,应该也是符合 servlet3.0 规范的,但实际上并无被 SPI 加载。