# 精尽 Spring MVC 源码分析 —— 容器的初始化(三)之 Servlet 3.0 集成 本文,我们直接基于徐妈的 [《Spring 揭秘 —— 寻找遗失的 web.xml》](https://www.cnkirito.moe/servlet-explore/) 的文章,一起来看看 Spring MVC 的初始化过程,是如何集成到 Servlet 3.0 和 Spring Boot 之中的。 因为篇幅较长,所以捉摸了下,会拆分成两部分: - **Spring MVC 是如何集成到 Servlet 3.0 中** 【本文】 - Spring MVC 是如何集成到 Spring Boot 中 > 艿艿的吐槽:大半夜( 大概是陵城 1 点左右),问徐妈拿这篇文章的授权,徐妈说哦了。然后问他在干嘛,竟然在写代码。可怕的 95 后啊~~~~ ------ 今天我们来放松下心情,不聊分布式,云原生,来聊一聊初学者接触的最多的 java web 基础。几乎所有人都是从 servlet,jsp,filter 开始编写自己的第一个 hello world 工程。那时,还离不开 `web.xml` 的配置,在 xml 文件中编写繁琐的 servlet 和 filter 的配置。随着 spring 的普及,配置逐渐演变成了两种方式 —— java configuration 和 xml 配置共存。现如今,springboot 的普及,java configuration 成了主流,xml 配置似乎已经“灭绝”了。不知道你有没有好奇过,这中间都发生了哪些改变,web.xml 中的配置项又是被什么替代项取代了? [![servlet](06-Spring MVC 源码分析-容器的初始化(三)之 Servlet 3.0 集成.assets/servlet.png)](http://kirito.iocoder.cn/servlet.png)servlet # 1. Servlet 3.0 以前的时代 为了体现出整个演进过程,还是来回顾下 n 年前我们是怎么写 servlet 和 filter 代码的。 项目结构(本文都采用 maven 项目结构) ``` . ├── pom.xml ├── src ├── main │ ├── java │ │ └── moe │ │ └── cnkirito │ │ ├── filter │ │ │ └── HelloWorldFilter.java │ │ └── servlet │ │ └── HelloWorldServlet.java │ └── resources │ └── WEB-INF │ └── web.xml └── test └── java 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 和 filter 。 ``` HelloWorldServlet moe.cnkirito.servlet.HelloWorldServlet HelloWorldServlet /hello HelloWorldFilter moe.cnkirito.filter.HelloWorldFilter HelloWorldFilter /hello ``` 这样,一个 java web hello world 就完成了。当然,本文不是 servlet 的入门教程,只是为了对比。 # 2. Servlet 3.0 新特性 [![servlet_3.0](06-Spring MVC 源码分析-容器的初始化(三)之 Servlet 3.0 集成.assets/servlet_3.0.jpg)](http://kirito.iocoder.cn/servlet_3.0.jpg)servlet_3.0 Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet 2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。其中一项新特性便是提供了无 xml 配置的特性。 servlet3.0 首先提供了 `@WebServlet` ,`@WebFilter` 等注解,这样便有了抛弃 `web.xml` 的第一个途径,凭借注解声明 servlet 和 filter 来做到这一点。 除了这种方式,servlet3.0 规范还提供了更强大的功能,可以在运行时动态注册 servlet ,filter,listener。以 servlet 为例,过滤器与监听器与之类似。ServletContext 为动态配置 Servlet 增加了如下方法: - ServletRegistration.Dynamic addServlet(String servletName,Class servletClass) - ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) - ServletRegistration.Dynamic addServlet(String servletName, String className) - T createServlet(Class clazz) - ServletRegistration getServletRegistration(String servletName) - Map getServletRegistrations() 其中前三个方法的作用是相同的,只是参数类型不同而已; 通过 `#createServlet(Class clazz)` 方法创建的 Servlet,通常需要做一些自定义的配置,然后使用 `#addServlet(...)` 方法来将其动态注册为一个可以用于服务的 Servlet。 两个 `#getServletRegistration()` 方法主要用于动态为 Servlet 增加映射信息,这等价于在 `web.xml` 中使用 标签为存在的 Servlet 增加映射信息。 以上 ServletContext 新增的方法: - 要么是在 `javax.servlet.ServletContextListener` 的 `#contextInitialized(ServletContextEvent sce)` 方法中调用。 - 要么是在 `javax.servlet.ServletContainerInitializer` 的 `#onStartup(Set> c, ServletContext ctx)` 方法中调用。 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> 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 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> c` 和 `@HandlesTypes` 注解在 demo 中我并未使用,感兴趣的朋友可以 debug 看看到底获取了哪些 class ,一般正常的流程是使用 `@HandlesTypes` 指定需要处理的 class,而后对 `Set>` 进行判断是否属于该 class,正如前文所言,`#onStartup(...)` 会加载不需要被处理的一些 class。 这么声明一个 ServletContainerInitializer 的实现类,web 容器并不会识别它,所以,需要借助 SPI 机制来指定该初始化类,这一步骤是通过在项目路径下创建 `META-INF/services/javax.servlet.ServletContainerInitializer` 来做到的,它只包含一行内容: ``` moe.cnkirito.CustomServletContainerInitializer ``` 使用 ServletContainerInitializer 和 SPI 机制,我们的 web 应用便可以彻底摆脱 `web.xml` 了。 # 3. Spring 是如何支持 Servlet 3.0 的? 回到我们的 spring 全家桶,可能已经忘了具体是什么时候开始不写 web.xml 了,我只知道现在的项目已经再也看不到它了,spring 又是如何支持 servlet3.0 规范的呢? 寻找 spring 中 ServletContainerInitializer 的实现类并不困难,可以迅速定位到 `org.springframework.web.SpringServletContainerInitializer` 该实现类。 ``` @HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List initializers = new LinkedList(); 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,所以进行了特判。**另外,也要注意下 `@HandlesTypes(WebApplicationInitializer.class)` 注解**,如果厂商正确的实现 `@HandlesTypes` 的逻辑,传入的 `Set> webAppInitializerClasses` 都是 WebApplicationInitializer 对象。 - `<2>` spring 与我们之前的 demo 不同,并没有在 SpringServletContainerInitializer 中直接对 servlet 和 filter 进行注册,而是委托给了一个陌生的类 `org.springframework.web.WebApplicationInitializer` 。WebApplicationInitializer 类便是 spring 用来初始化 web 环境的委托者类,它通常有三个实现类: [![WebApplicationInitializer](06-Spring MVC 源码分析-容器的初始化(三)之 Servlet 3.0 集成.assets/WebApplicationInitializer.png)](http://kirito.iocoder.cn/WebApplicationInitializer.png)WebApplicationInitializer 你一定不会对 DispatcherServlet 感到陌生,`AbstractDispatcherServletInitializer#registerDispatcherServlet` 便是无 `web.xml` 前提下创建 DispatcherServlet 的关键代码。代码如下: ``` // AbstractDispatcherServletInitializer.java @Override public void onStartup(ServletContext servletContext) throws ServletException { // 调用父类启动的逻辑 super.onStartup(servletContext); // 注册 DispacherServlt registerDispatcherServlet(servletContext); } protected void registerDispatcherServlet(ServletContext servletContext) { // 获得 Servlet 名 String servletName = getServletName(); Assert.hasLength(servletName, "getServletName() must not return null or empty"); // <1> 创建 WebApplicationContext 对象 WebApplicationContext servletAppContext = createServletApplicationContext(); Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null"); // <2> 创建 FrameworkServlet 对象 FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext); Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null"); dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet); if (registration == null) { throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " + "Check if there is another servlet registered under the same name."); } registration.setLoadOnStartup(1); registration.addMapping(getServletMappings()); registration.setAsyncSupported(isAsyncSupported()); // <3> 注册过滤器 Filter[] filters = getServletFilters(); if (!ObjectUtils.isEmpty(filters)) { for (Filter filter : filters) { registerServletFilter(servletContext, filter); } } customizeRegistration(registration); } ``` - `<1>` 处,调用 `#createServletApplicationContext()` 方法,创建 WebApplicationContext 对象。代码如下: ``` // AbstractAnnotationConfigDispatcherServletInitializer.java @Override protected WebApplicationContext createServletApplicationContext() { AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); Class[] configClasses = getServletConfigClasses(); if (!ObjectUtils.isEmpty(configClasses)) { context.register(configClasses); } return context; } ``` - 该方法由子类 AbstractAnnotationConfigDispatcherServletInitializer 重写,并且创建的 WebApplicationContext 的子类 AnnotationConfigWebApplicationContext 对象。 - `<2>` 处,调用 `#createDispatcherServlet(WebApplicationContext servletAppContext)` 方法,创建 FrameworkServlet 对象。代码如下: ``` // AbstractDispatcherServletInitializer.java protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) { return new DispatcherServlet(servletAppContext); } ``` - 创建 FrameworkServlet 的子类 DispatcherServlet 对象。 - 另外,比较有趣的是传入的 `servletAppContext` 方法参数,这就是该 DispatcherServlet 的 **Servlet WebApplicationContext 容器**啊。 ------ 然后,我们可以去项目中寻找一下 `org.springframework:spring-web:version` 的依赖,它下面就存在一个 ServletContainerInitializer 的扩展,指向了 SpringServletContainerInitializer,这样只要在 servlet 3.0 环境下部署,spring 便可以自动加载进行初始化: [![SpringServletContainerInitializer](06-Spring MVC 源码分析-容器的初始化(三)之 Servlet 3.0 集成.assets/F835D518-A725-40D9-84BA-6AC014DAE5A7.png)](http://kirito.iocoder.cn/F835D518-A725-40D9-84BA-6AC014DAE5A7.png)SpringServletContainerInitializer 注意,上述这一切特性从 spring 3 就已经存在了,而如今 spring 5 已经伴随 springboot 2.0 一起发行了。 # 666. 彩蛋 因为有胖友可能不熟悉 Java SPI 机制,所以推荐阅读徐妈的 [《JAVA拾遗 —— 关于SPI机制》](https://www.cnkirito.moe/spi/) 。 感谢徐妈给的这篇文章的授权,使我能够完美的进行偷懒,哈哈哈哈。 文章可能信息量比较大,如果有疑问的地方,请在知识星球留言,我们一起讨论。