深入 Spring Boot :web.xml 去哪了
如今,开发基于 Spring 的 web 应用越来越少使用到 web.xml,或者基本上已经看不到 web.xml,那这个 web.xml 到底去哪了呢,接下来我们一起来探索一下。
Servlet3 前使用 web.xml 在 Servlet3.0 之前,web.xml 是开发 web 应用必须配置的文件,可以通过它配置 DispatcherServlet、ContextLoaderListener 和其它额外的 Servlet、Filter、Listener,就像如下的 web.xml 配置。
<?xml version="1.0" encoding="UTF-8"?><web-app version="2.5" 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_2_5.xsd"><display-name>web-app</display-name>
</web-app>以上的 web.xml 在应用启动的时候会创建两个 Spring 上下文,一个由 ContextLoaderListener 创建的上下文,一个由 DispatcherServlet 创建的上下文。ContextLoaderListener 创建的上下文用于装载非 web 功能相关的 bean,例如 Service、DAO 等,而 DispatcherServlet 创建的上下文用于装载 web 功能相关的 bean,例如 Controller、ViewResolver 等。ContextLoaderListener 创建的上下文要装载的 bean 来自于 web.xml 中通过 context-param 标签配置的 contextConfigLocation 指定的 xml,例如 classpath:spring/.xml;而 DispatcherServlet 创建的上下文要装载的 bean 来自于 web.xml 中配置的 DispatcherServlet 中通过 init-param 标签配置的 contextConfigLocation 指定的 xml,例如 classpath*:springMVC.xml,如果没有通过 init-param 标签配置 contextConfigLocation,默认使用以 DispatcherServlet 在 web.xml 中配置的 servlet-name 为前缀,-servlet.xml 为后缀的 xml 文件,例如 springMVC-servlet.xml。
Servlet3+弱化 web.xmlServlet3.0 在 Servlet2.5 的基础上提供了若干新特性用于简化 Web 应用的开发和部署,在 servlet-api.jar 的 javax.servlet.annotation 包中新增了 @WebServlet、@WebFilter 和 @WebListener 注解,用于简化 Servlet、过滤器和监听器的声明,也就是说从此之后开发 web 应用不一定非要使用 web.xml 了,例如如下代码声明了一个自定义 Filter。
在 Servlet3.0 API 中提供了一个 javax.servlet.ServletContainerInitializer 接口,接口只有一个 onStartup 方法,在支持 Servlet3.0 的 Web 应用服务器中,例如 Tomcat7 或更高版本,服务器会在启动的时候在类路径下查找 javax.servlet.ServletContainerInitializer 接口的实现类,执行实现类的 onStartup 方法用于配置 Servlet 容器,例如注册 Servlet、Filter 或 Listener。
onStartup 方法有两个参数:Set<Class<?>> c 和 ServletContext ctx,ServletContext 即 Servlet 上下文,它定义了一些方法用于和 Servlet 容器进行交流,也可以获取 web 应用的一些资源信息;如果 ServletContainerInitializer 接口的实现类使用 @HandlesTypes 注解声明了感兴趣的类或接口,那么这个感兴趣的类及其子类或接口的实现类就会被设置到 Set<Class<?>> c 中。
ServletContainerInitializer 接口的具体使用方法必须在代码的 classpath 下的 META-INF/services/路径下定义一个名为 javax.servlet.ServletContainerInitializer 的文件,这个文件的内容是 ServletContainerInitializer 接口实现类的全路径,例如 com.example.demo.MyServletContainerInitializer,下面实现一个简单的 MyServletContainerInitializer。
从上图可以看到,ServletContext 提供了可以注册 Servlet、Filter 和 Listener 的方法,下面动态注册一个 Servlet 和 Filter。
Servlet3.0 新增的这些特性在弱化 web.xml,下面来看一下 Spring 是如何支持 Servlet3 的。
Spring3+逐渐替换 web.xmlSpring 框架从 3.1 版本开始支持 Servlet3.0,可以在基于 Java 的配置中声明 Servlet、Filter 和 Listener,并且从 3.2 版本开始可以使用 AbstractAnnotationConfigDispatcherServletInitializer 的子类来配置 DispatcherServlet,它会创建 DispatcherServlet 和 ContextLoaderListener,真正实现不再需要使用 web.xml,例如如下代码自定义了一个 DispatcherServletInitializer,它继承了 AbstractAnnotationConfigDispatcherServletInitializer。
DispatcherServletInitializer 分别实现了 getRootConfigClasses、getServletConfigClasses 和 getServletMappings 方法。getRootConfigClasses 方法返回使用 @Configuration 标注的类用于 ContextLoaderListener 创建 Spring 上下文装载非 web 相关的 bean。getServletConfigClasses 方法返回使用 @Configuration 标注的类用于 DispatcherServlet 创建 Spring 上下文装载 web 相关的 bean。getServletMappings 方法返回的字符串数组用于告诉 DispatcherServlet 处理那些 url 的请求。到这里这三个方法的用途是不是很熟悉,其实就是 web.xml 中配置的 DispatcherServlet 和 ContextLoaderListener 替代方案。
Spring3.1 中的 SpringServletContainerInitializer 实现了 ServletContainerInitializer 接口,同时在相应的 jar 包 META-INF/services/路径下定义了 javax.servlet.ServletContainerInitializer 文件,它的内容是 org.springframework.web.SpringServletContainerInitializer。
根据上面对 javax.servlet.ServletContainerInitializer 接口分析可知,SpringServletContainerInitializer 的 onStartup 方法会在 web 应用启动的时候被调用。在分析 onStartup 方法之前,关注到 SpringServletContainerInitializer 类上使用 @HandlesTypes 注解标注,这个注解的 value 是 WebApplicationInitializer,所以,支持 Servlet3.0+的容器在启动时会自动扫描 classpath 下 WebApplicationInitializer 接口的实现类,并将这些实现类传递给 onStartup 方法的第一个参数。
上面代码做了简单的注释,可以看到 onStartup 会遍历执行 WebApplicationInitializer 接口实现类的 onStartup 方法。现在,我们在回过来看一下 AbstractAnnotationConfigDispatcherServletInitializer 类结构。
AbstractAnnotationConfigDispatcherServletInitializer 继承自 AbstractDispatcherServletInitializer,而 AbstractDispatcherServletInitializer 又继承自 AbstractContextLoaderInitializer,AbstractContextLoaderInitializer 实现了 WebApplicationInitializer 接口,所以 AbstractAnnotationConfigDispatcherServletInitializer 子类的 onStartup 方法会在 web 应用启动的时候被调用,创建 DispatcherServlet 和 ContextLoaderListener,进而创建 Spring 上下文,完成应用初始化操作。
SpringBoot 不再使用 web.xml 既然 Spring 框架从 3.1 开始逐步使用 Java Config 替换 web.xml,那么 SpringBoot 作为快速、简便使用 Spring 框架的脚手架,必然也不会再继续使用 web.xml 了。
在基于 SpringBoot 开发的代码中依然可以继续使用 servlet-api 中 javax.servlet.annotation 包中新增的 @WebServlet、@WebFilter 和 @WebListener 注解,用于简化 Servlet、过滤器和监听器的声明,不过需要注意别忘记使用 SpringBoot 提供的 @ServletComponentScan 注解开启对这三注解的扫描。
SpringBoot 提供了 ServletRegistrationBean、FilterRegistrationBean 和 ServletListenerRegistrationBean,用于动态注册 Servlet、过滤器和监听器,例如如下代码。
上面代码虽然创建了三个 bean,但这三个 bean 仅托管到了 Spring 上下文中,并没有注册到 ServletContext 中,那什么时候被注册到 ServletContext 中呢?查看 ServletRegistrationBean、FilterRegistrationBean 和 ServletListenerRegistrationBean 源码会发现,它们都间接继承自 RegistrationBean,而 RegistrationBean 实现了 ServletContextInitializer 接口。
注意看 RegistrationBean 实现的是 ServletContextInitializer 接口,它是 SpringBoot 提供的接口,和我们上面说到的 ServletContainerInitializer 接口不是同一个,一定不要混淆。ServletContextInitializer 接口使用编程的方式配置 Servlet3.0+的 ServletContext,它不会被 Servlet 容器启动时自动调用,它的生命周期由 Spring 管理。
下面以代码运行在 Tomcat7+的版本为例,当 SpringBoot 项目代码运行的时候,无论是内嵌 Tomcat 还是将代码打成 war 部署到外部 Tomcat,代码都会运行到 SpringApplication.run 方法,创建 Spring 上下文装载 bean,做初始化操作,在这一过程中会创建一个 TomcatStarter 对象,然后会执行 TomcatStarter 中的 onStartup 方法,下面是 TomcatStarter 源码。
通过源码发现 TomcatStarter 也实现了 ServletContainerInitializer 接口,不过它没有使用和 SpringServletContainerInitializer 一样的实现机制,而是采用硬编码的方式,直接 new 了一个 TomcatStarter,在创建 TomcatStarter 对象的时候,传入了一个 ServletContextInitializer 数组,这个数组里的内容是从 Spring 上下文搜索到的 ServletContextInitializer 接口实现类的 bean,也就是说我们上面通过 ServletRegistrationBean、FilterRegistrationBean 和 ServletListenerRegistrationBean 创建的 bean(myServlet、myFilter 和 myListener)都会注入到这个数组中,其实这个数组里面还有一个很重要的 bean,就是 dispatcherServlet,它是由 SpringBoot 自动配置功能通过 DispatcherServletRegistrationBean 创建的 bean。
TomcatStarter 对象创建完成后,在接下来的初始化过程中会回调它的 onStartup 方法,在这个方法的内部可以看到,它依然是执行了各个 ServletContextInitializer 接口实现类的 onStartup,进而将 Servlet、Filter 和 Listener 注册到 ServletContext。
总结至此,我们已经了解了 web.xml 是如何被替换的,我们也发现框架封装的东西越来越多,集成度也越来越高,框架虽好,如果我们不了解来龙去脉,只做一个工具的使用者,时间久了,我们也就是一个工具人,所以,研究一下 why、what 和 how 很有必要。
评论