如今,开发基于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>
-
<context-param>
-
<param-name>contextConfigLocation
</param-name>
-
<param-value>classpath*:spring/*.xml
</param-value>
-
</context-param>
-
<listener>
-
<listener-class>org.springframework.web.context.ContextLoaderListener
</listener-class>
-
</listener>
-
<filter>
-
<filter-name>characterEncodingFilter
</filter-name>
-
<filter-class>org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
-
<init-param>
-
<param-name>encoding
</param-name>
-
<param-value>UTF-8
</param-value>
-
</init-param>
-
</filter>
-
<filter-mapping>
-
<filter-name>characterEncodingFilter
</filter-name>
-
<url-pattern>/*
</url-pattern>
-
</filter-mapping>
-
<servlet>
-
<servlet-name>springMVC
</servlet-name>
-
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
-
<init-param>
-
<param-name>contextConfigLocation
</param-name>
-
<param-value>classpath*:springMVC.xml
</param-value>
-
</init-param>
-
<load-on-startup>1
</load-on-startup>
-
</servlet>
-
<servlet-mapping>
-
<servlet-name>springmvc
</servlet-name>
-
<url-pattern>/
</url-pattern>
-
</servlet-mapping>
-
</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.xml
Servlet3.0在Servlet2.5的基础上提供了若干新特性用于简化Web应用的开发和部署,在servlet-api.jar的javax.servlet.annotation包中新增了@WebServlet、@WebFilter和@WebListener注解,用于简化Servlet、过滤器和监听器的声明,也就是说从此之后开发web应用不一定非要使用web.xml了,例如如下代码声明了一个自定义Filter。
-
@WebFilter(filterName = "myFilter", urlPatterns = { "/*" })
-
public
class MyFilter implements Filter {
-
private Logger logger = LoggerFactory.getLogger(MyFilter.class);
-
@Override
-
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
-
// do something
-
logger.info(
"my Filter...");
-
chain.doFilter(request, response);
-
}
-
}
在Servlet3.0 API中提供了一个javax.servlet.ServletContainerInitializer接口,接口只有一个onStartup方法,在支持Servlet3.0的Web应用服务器中,例如Tomcat7或更高版本,服务器会在启动的时候在类路径下查找javax.servlet.ServletContainerInitializer接口的实现类,执行实现类的onStartup方法用于配置Servlet容器,例如注册Servlet、Filter或Listener。
-
public
interface ServletContainerInitializer {
-
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
-
}
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。
-
public
class MyServletContainerInitializer implements ServletContainerInitializer {
-
@Override
-
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
-
FilterRegistration.Dynamic myFilter = ctx.addFilter(
"myFilter", MyFilter.class);
-
EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
-
dispatcherTypes.add(DispatcherType.FORWARD);
-
dispatcherTypes.add(DispatcherType.REQUEST);
-
myFilter.addMappingForUrlPatterns(dispatcherTypes,
true,"/*");
-
ServletRegistration.Dynamic myServlet = ctx.addServlet("myServlet", MyServlet.class);
-
myServlet.addMapping("/hello");
-
}
-
}
Servlet3.0新增的这些特性在弱化web.xml,下面来看一下Spring是如何支持Servlet3的。
Spring3+逐渐替换web.xml
Spring框架从3.1版本开始支持Servlet3.0,可以在基于Java的配置中声明Servlet、Filter和Listener,并且从3.2版本开始可以使用AbstractAnnotationConfigDispatcherServletInitializer的子类来配置DispatcherServlet,它会创建DispatcherServlet和ContextLoaderListener,真正实现不再需要使用web.xml,例如如下代码自定义了一个DispatcherServletInitializer,它继承了AbstractAnnotationConfigDispatcherServletInitializer。
-
public
class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
-
protected Class<?>[] getRootConfigClasses() {
-
return
new Class[]{RootConfig.class};
-
}
-
protected Class<?>[] getServletConfigClasses() {
-
return
new Class[]{ServletConfig.class};
-
}
-
protected String[] getServletMappings() {
-
return
new String[]{
"/"};
-
}
-
}
-
/**
-
* 扫描装载指定包路径下非web相关的bean,排除Spring MVC相关的bean
-
*/
-
@Configuration
-
@ComponentScan(basePackages={"com.example.demo.service"},
-
excludeFilters={
-
@ComponentScan.Filter(type= FilterType.ANNOTATION, value= EnableWebMvc.class)
-
})
-
public
class RootConfig {
-
}
-
/**
-
* 扫描装载指定包路径下web相关bean,例如controller、ViewResolver等
-
*/
-
@Configuration
-
@EnableWebMvc
-
@ComponentScan("com.example.demo.controller")
-
public
class ServletConfig extends WebMvcConfigurerAdapter {
-
@Bean
-
public ViewResolver viewResolver() {
-
InternalResourceViewResolver resolver =
new InternalResourceViewResolver();
-
resolver.setPrefix(
"/WEB-INF/views/");
-
resolver.setSuffix(
".html");
-
resolver.setExposeContextBeansAsAttributes(
true);
-
return resolver;
-
}
-
@Override
-
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
-
// 静态资源访问交由默认Servlet处理,不由DispatcherServlet处理
-
configurer.enable();
-
}
-
}
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方法的第一个参数。
-
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
-
throws ServletException {
-
// 创建一个空list
-
List<WebApplicationInitializer> initializers = Collections.emptyList();
-
if (webAppInitializerClasses !=
null) {
-
initializers =
new ArrayList<>(webAppInitializerClasses.size());
-
// 遍历webAppInitializerClasses
-
for (Class<?> waiClass : webAppInitializerClasses) {
-
// 将WebApplicationInitializer接口实现类加入到initializers中
-
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
-
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
-
try {
-
initializers.add((WebApplicationInitializer)
-
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
-
}
-
catch (Throwable ex) {
-
throw
new ServletException(
"Failed to instantiate WebApplicationInitializer class", ex);
-
}
-
}
-
}
-
}
-
// 如果initializers没有元素,返回
-
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);
-
// 遍历initializers中的接口实现类
-
for (WebApplicationInitializer initializer : initializers) {
-
// 调用实现类的onStartup方法
-
initializer.onStartup(servletContext);
-
}
-
}
上面代码做了简单的注释,可以看到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
-
public ServletRegistrationBean myServlet() {
-
ServletRegistrationBean myServlet =
new ServletRegistrationBean();
-
myServlet.setServlet(
new MyServlet());
-
myServlet.addUrlMappings(
"/hello");
-
return myServlet;
-
}
-
@Bean
-
public FilterRegistrationBean myFilter() {
-
FilterRegistrationBean myFilter =
new FilterRegistrationBean();
-
myFilter.setFilter(
new MyFilter());
-
myFilter.setName(
"myFilter");
-
myFilter.addUrlPatterns(
"/*");
-
return myFilter;
-
}
-
@Bean
-
public ServletListenerRegistrationBean myListener() {
-
ServletListenerRegistrationBean myListener =
new ServletListenerRegistrationBean<>();
-
myListener.setListener(
new MyListener());
-
return myListener;
-
}
上面代码虽然创建了三个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很有必要。
往期推荐:
深入Spring Boot (十四):jar/war打包解决方案
聊一聊Redis官方置顶推荐的Java客户端Redisson
我画了25张图展示线程池工作原理和实现原理,原创干货,建议先收藏再阅读
Spring框架你敢写精通,面试官就敢问@Autowired注解的实现原理
面试被问为什么使用Spring Boot?答案好像没那么简单
面试官一步一步的套路你,为什么SimpleDateFormat不是线程安全的
都说ThreadLocal被面试官问烂了,可为什么面试官还是喜欢继续问
学之多,而后知之少!朋友们点【在看】是我持续更新的最大动力!
转载:https://blog.csdn.net/tianruirui/article/details/112057763