1. web.xml
我们还记得,最早的时候编写一个java web工程,需要在应用中按照servlet的规范,编写一个web.xml,然后将我们需要配置的servlet、filter、listener等配置到这个xml中。
这样,当tomcat启动时就会加载这个web.xml,然后实例化其中配置的各个servlet组件。
在编写这个web,xml时,往往需要配置很多的组件,很繁琐。
不知道我们想过没有,为什么从springboot开始后,就不再使用web.xml了?
我们知道,随着spring的普及,配置逐渐演变成了两种方式:java config 和 传统xml。
现在随着springboot的普及,java config似乎已经成了主流,xml的方式几乎彻底消失。
这中间都发生什么?
2. servlet3.0 以前的时代
为了体现出整个演进过程,还是来回顾下 n 年前我们是怎么写 servlet 和 filter 代码的。
项目结构(本文都采用 maven 项目结构)
写一个servlet:
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");
}
}
写一个filter:
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:
<?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工程就完成了。
3. servlet3.0 的新特性
3.1 ServletContext动态配置servlet等
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<? extends Servlet> servletClass)
ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
ServletRegistration.Dynamic addServlet(String servletName, String className)
T createServlet(Class clazz)
ServletRegistration getServletRegistration(String servletName)
Map<String,? extends ServletRegistration> getServletRegistrations()
其中前三个方法的作用是相同的,只是参数类型不同而已;
通过 createServlet () 方法创建的 Servlet,通常需要做一些自定义的配置,然后使用 addServlet () 方法来将其动态注册为一个可以用于服务的 Servlet。
两个 getServletRegistration () 方法主要用于动态为 Servlet 增加映射信息,这等价于在 web.xml 中使用 标签为存在的 Servlet 增加映射信息。
以上 ServletContext 新增的方法要么是在 ServletContextListener 的 contextInitialized 方法中调用,要么是在 ServletContainerInitializer 的 onStartup () 方法中调用。
3.2 ServletContainerInitializer
上面说了,ServletContext中提供了servlet3.0新增的方法用来动态侵入式的注册servlet等到servlet上下文中,而这些方法有可能在ServletContainerInitializer 的 onStartup () 方法中调用,那这个原理是什么呢?
ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API (JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的 onStartup () 方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给 onStartup () 处理的类。
上面对ServletContainerInitializer的描述可能不是很清楚,我们先看下ServletContainerInitializer接口的源码:
package javax.servlet;
import java.util.Set;
// 这个接口属于servlet的包,是servlet3.0规范的体现
// 该接口只有servlet3.0提供规范,具体的实现由servlet容器负责实现,比如tomcat等,然后在web容器启动时onStartup方法会被回调。
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
}
我们要使用ServletContainerInitializer,一般需要自定义一个类来实现它。
在实现该接口后,需要在SPI文件中注册,在Servlet容器启动时通过SPI从classpath下查找其实现类,将其实现类实例化后,回调其中实现的onStartup方法。
参考spring对其的实现: org.springframework.web.SpringServletContainerInitializer
package org.springframework.web;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = Collections.emptyList();
Iterator var4;
if (webAppInitializerClasses != null) {
initializers = new ArrayList(webAppInitializerClasses.size());
var4 = webAppInitializerClasses.iterator();
while(var4.hasNext()) {
Class<?> waiClass = (Class)var4.next();
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
((List)initializers).add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}
if (((List)initializers).isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
servletContext.log(((List)initializers).size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort((List)initializers);
var4 = ((List)initializers).iterator();
while(var4.hasNext()) {
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
initializer.onStartup(servletContext);
}
}
}
}
spring实现中提供的SPI:
实现类上标注的@HandlesTypes注解是干啥的呢?
package javax.servlet.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 该注解属于 tomcat-embed-core-9.0.55.jar 中
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {
Class<?>[] value();
}
这个注解也是Java EE规范中的注解,表示当前ServletContainerInitializer的实现类,能处理的类型。这两个都是Servlet3.0中的东西。
主要作用如下:
Servlet容器比如tomcat等在启动时,会将通过SPI注册进来的所有 ServletContainerInitializer 接口的实现类都加载进JVM并实例化其对象后,然后在适当的时机遍历回调所有对象的onStartup方法。
而我们看到onStartup方法是需要参数的,其需要两个参数:
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
这些参数怎么来呢?就需要使用@HandlesTypes注解了。
onStartup方法所需要的参数就是通过@HandlesTypes注解传入的。
@HandlesTypes注解的实现原理:
@HandlesTypes注解由Servlet容器提供支持(实现),参数中指定的所有实现类,利用字节码扫描框架(例如ASM、BCEL)从classpath中扫描出来,放入集合,传给回调方法onStartup的第一个参数。
也就是说,这个注解主要就是用来扫描出其参数中指定的接口的所有实现类,然后将这些实现类进行实例化后聚合到一起放入 onStartup 方法的第一个参数 webAppInitializerClasses 中。
@HandlesTypes 注解使用注意:
@HandlesTypes 注解只能接受一个class,这个class必须是接口,不能是其他抽象类或者普通类。
标注接口后会把该接口的所有实现类进行实例化后自动聚合到set中。
注意,只能是接口,且接口必须有实现类。
如果是标注一个普通类,或者标注的接口没有实现类,则聚合到set中的对象将为null.
ServletContainerInitializer 使用注意:
ServletContainerInitializer的 onStartup 方法,默认接受的set是WEB-INF/lib下的所有class.
但一旦在servletContainerInitializer的实现类上使用了@HandlesTypes导入了感兴趣的类,则servlet规范只会导入该类型的所有类进来。
3.3 应用举例
一个典型的 servlet3.0+ 的 web 项目结构如下:
承接上例,我并未对 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 了。
4. spring对servlet3.0的支持
4.1 spring中对ServletContainerInitializer的实现类
正如上面所举示例,spring中ServletContainerInitializer 的实现类就是org.springframework.web.SpringServletContainerInitializer。我们简化其源码如下:
package org.springframework.web;
// 注意,这个实现类在spring-web-5.3.13.jar中,是spring对其的实现,和springboot无关
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = Collections.emptyList();
Iterator var4;
if (webAppInitializerClasses != null) {
initializers = new ArrayList(webAppInitializerClasses.size());
var4 = webAppInitializerClasses.iterator();
while(var4.hasNext()) {
Class<?> waiClass = (Class)var4.next();
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
((List)initializers).add((WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass, new Class[0]).newInstance());
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}
if (((List)initializers).isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
servletContext.log(((List)initializers).size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort((List)initializers);
var4 = ((List)initializers).iterator();
while(var4.hasNext()) {
// 遍历入参的第1个参数 webAppInitializerClasses
// 与我们前面自己编写的demo不同,SpringServletContainerInitializer 中并没有直接注册servlet、filter等,而是委托一个完全陌生的类 WebApplicationInitializer。
// 在此处只是遍历调用了所有WebApplicationInitializer对象的onStartup方法,传入了一个参数servletContext而已。
// 可以猜测,对servlet、filter等的注册,都是在 WebApplicationInitializer 的onStartup方法中实现的。
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
initializer.onStartup(servletContext);
}
}
}
}
4.2 WebApplicationInitializer
我们已经知道,上面实现类中的第一个参数Set<Class<?» webAppInitializerClasses,其值就是通过类上标注的注解@HandlesTypes({WebApplicationInitializer.class})进行导入的。
该注解会扫描所有WebApplicationInitializer接口的实现类,然后将其class对象聚合为set传入到SpringServletContainerInitializer的onStartup方法中。
WebApplicationInitializer接口源码如下:
package org.springframework.web;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
// 这个接口也是在 spring-web-5.3.13.jar 中
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
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 一起发行了。
我们来总结一下,spring对servlet3.0的支持:
(1)web容器启动时,通过SPI机制发现classpath下ServletContainerInitializer接口的实现类,然后创建它们的对象
(2)spring提供的实现类上有@HandlesTypes({WebApplicationInitializer.class})注解,会导入所有WebApplicationInitializer类型的class到onStartup方法的参数中
(3)遍历执行ServletContainerInitializer所有实现对象的onStartup方法,而spring实现类自己不做逻辑处理,而是委托所有的WebApplicationInitializer的实现对象去做核心逻辑。
spring中真正干活的,实际上是WebApplicationInitializer接口的所有实现类。
文档信息
- 本文作者:Marshall