学过 web 基础,学过SSM框架,SSH框架,我们对 web.xml 文件应该都不陌生,最起码都能看到这个文件的存在。特别是看了spring源码以后,我更加执着与这个文件了,在我看来,一切的一切都从这个配置文件开始。大家有没有想过这个问题,为啥你引入了框架的jar包,就能给你加载进去呢,你引入了那么多jar包,为啥能找到正确的jar包加载呢?
另外web.xml文件都干了什么?
以及web.xml 文件的演变历史,后边的springboot已经没有web.xml文件了,为啥程序还是能正确的执行?为啥,为啥?反正我是很好奇的。
# # 一切从认清 web.xml 文件开始
- 先看这儿文件藏在哪里:
- 这个文件里边藏了什么?
最原始是这样的:
项目名字
webDemo
默认加载的页面,就像职责链一样,从上到下一个一个的找,如果有的话,就作为第一个加载展示的页面。
index.html
index.htm
index.jsp
default.html
default.htm
default.jsp
我们在使用框架以后加入了点配置:
使用SSM框架,配置是要这样写的:
springmvcDemo
index.html
index.htm
index.jsp
default.html
default.htm
default.jsp
springmvc
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:springmvc.xml
springmvc
*.action
org.springframework.web.servlet.DispatcherServlet
其实到这里我们已经能够明白,其实SSM框架,起始也是一个servlet。如果我们不用框架也能实现功能,只不过我们每一个控方法都要来配置一个 servlet,而使用框架以后,只需要配置框架的一个servlet,接下来的事框架帮我们做。
下边的内容来源于别人的文章,原文地址:
https://www.cnkirito.moe/servlet-explore/
# # web.xml 文件的变化随着技术的发展,这个文件可以不写了,这正是 servlet 3.0 版本,一起看下,如何摆脱 web.xml 文件的把:
servlet3.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 的入门教程,只是为了对比。
servlet3.0 新特性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> 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);
}
}
这么声明一个 ServletContainerInitializer 的实现类,web 容器并不会识别它,所以,需要借助 SPI 机制来指定该初始化类,这一步骤是通过在项目路径下创建
META-INF/services/javax.servlet.ServletContainerInitializer
来做到的,它只包含一行内容:对上述代码进行一些解读。ServletContext 我们称之为 servlet 上下文,它维护了整个 web 容器中注册的 servlet,filter,listener,以 servlet 为例,可以使用 servletContext.addServlet 等方法来添加 servlet。而方法入参中 Set> 进行判断是否属于该 class,正如前文所言,onStartup 会加载不需要被处理的一些 class。1
moe.cnkirito.CustomServletContainerInitializer
使用 ServletContainerInitializer 和 SPI 机制,我们的 web 应用便可以彻底摆脱 web.xml 了。
上边模块可以看到的是,在 servlet 3.0 开始可以不写 web.xml 文件了,那么 spring 又是如何支持的呢?
回到我们的 spring 全家桶,可能已经忘了具体是什么时候开始不写 web.xml 了,我只知道现在的项目已经再也看不到它了,spring 又是如何支持 servlet3.0 规范的呢?
寻找 spring 中 ServletContainerInitializer 的实现类并不困难,可以迅速定位到 SpringServletContainerInitializer 该实现类。
@HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set waiClass : webAppInitializerClasses) { // Be defensive: Some servlet containers provide us with invalid classes, // no matter what @HandlesTypes says... // 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); // for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); } } }
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.查看其 java doc,描述如下:
注意我在源码中标注两个序号,这对于我们理解 spring 装配 servlet 的流程来说非常重要。
英文注释是 spring 源码中自带的,它提示我们由于 servlet 厂商实现的差异,onStartup 方法会加载我们本不想处理的 class,所以进行了特判。
spring 与我们之前的 demo 不同,并没有在 SpringServletContainerInitializer 中直接对 servlet 和 filter 进行注册,而是委托给了一个陌生的类 WebApplicationInitializer ,WebApplicationInitializer 类便是 spring 用来初始化 web 环境的委托者类,它通常有三个实现类:
WebApplicationInitializer
你一定不会对 dispatcherServlet 感到陌生,AbstractDispatcherServletInitializer#registerDispatcherServlet 便是无 web.xml 前提下创建 dispatcherServlet 的关键代码。
可以去项目中寻找一下 org.springframework:spring-web:version 的依赖,它下面就存在一个 servletContainerInitializer 的扩展,指向了 SpringServletContainerInitializer,这样只要在 servlet3.0 环境下部署,spring 便可以自动加载进行初始化:
SpringServletContainerInitializer
注意,上述这一切特性从 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 的独特之处。
注册方式一:servlet3.0注解+@ServletComponentScan
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,可以采用这样的方式,比较直观。
注册方式二:RegistrationBean
@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中servlet加载流程的源码分析暂时只介绍这两种方式,下面解释下之前卖的关子,为什么说 springboot 没有完全遵守 servlet3.0 规范。讨论的前提是 springboot 环境下使用内嵌的容器,比如最典型的 tomcat。高能预警,以下内容比较烧脑,觉得看起来吃力的朋友可以跳过本节直接看下一节的总结!
Initializer被替换为TomcatStarter
当使用内嵌的 tomcat 时,你会发现 springboot 完全走了另一套初始化流程,完全没有使用前面提到的 SpringServletContainerInitializer,实际上一开始我在各种 ServletContainerInitializer 的实现类中打了断点,最终定位到,根本没有运行到 SpringServletContainerInitializer 内部,而是进入了 TomcatStarter 这个类中。
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中的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
关注打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?