Spring Boot中如何基于代码的形式去配置Servlet容器-刘宇
- 一、简单介绍
- 二、ServletContainerInitializer
- 2.1、简单介绍
- 2.2、它的实现类在Spring中有哪些
- 三、war包部署
- 3.1、Web容器如何调用ServletContainerInitializer的实现类
- 3.2、SpringServletContainerInitializer
- 3.3、WebApplicationInitializer
- 3.4、小结
- 3.4.1、流程梳理
- 3.4.2、总结
- 四、jar包部署
- 4.1、TomcatStarter
- 4.2、ServletContextInitializer
- 4.3、如何调用TomcatStarter这个实现类
- 4.4、小结
- 五、如何通过代码的形式配置Servlet、Filter
- 5.1、通过Servlet提供的注解进行添加
- 5.2、使用RegistrationBean进行添加
作者:刘宇
CSDN博客地址:
有部分资料参考,如有侵权,请联系删除。如有不正确的地方,烦请指正,谢谢。
一、简单介绍
- 从Servlet3.0规范起,我们发现web.xml这个文件已经不是必须存在的了,而是可以通过注解的方式进行配置了。那边在Spring Boot中它是如何基于代码的形式去配置容器的呢?
- Spring Boot的应用它的部署有两种形式,分别是war包部署和jar包部署,所以我们也要分两种情况去分开讨论。
二、ServletContainerInitializer
2.1、简单介绍
- 简称SCI,它是servlet3.0规范中提供的一个接口。ServletContainerInitializer的实现类的注册必须是包含在jar文件中META-INF/services/javax.servlet.ServletContainerInitializer文件中,这样兼容Srevlet3.0的web容器在启动的时候才会扫描到并调用实现了ServletContainerInitializer接口的实现类。
- 实现类可以通过在类上添加@HandleTypes注解来处理他感兴趣的类。
package javax.servlet;
import java.util.Set;
public interface ServletContainerInitializer {
//Set<Class<?>> c:感兴趣的类都存放在这里,就是@HandleTypes筛选出来的类
//ServletContext ctx:servlet上下文
void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}
2.2、它的实现类在Spring中有哪些
- 如下图所示,他一共有5个实现类,其中我们关注的是SpringServletContainerInitializer和TomcatStarter这两个实现类,因为Spring就是通过这两个实现类分别实现了war包部署和jar包部署的容器配置。
三、war包部署
3.1、Web容器如何调用ServletContainerInitializer的实现类
- 我们看org.springframework.spring-web这包下,你会发现在META-INF/services目录下有个javax.servlet.ServletContainerInitializer的文件。
该文件的内容就是Spring实现了javax.servlet.ServletContainerInitializer这个接口的实现类的类路径。这样的话只要web容器兼容servlet3.0,那么在启动的时候就会自动调用该实现类SpringServletContainerInitializer,并自动调用其下的onStartup方法。
3.2、SpringServletContainerInitializer
- 它是ServletContainerInitializer的实现类,主要用于基于代码的形式去配置servlet容器,它会使用到Spring的WebApplicationInitializer,利用SPI的机制去取代web.xml或者与之共存。
- SpringServletContainerInitializer会将ServletContext传递给任何用户定义的WebApplicationInitializer。然后让WebApplicationInitializer去完成具体的Servlet容器的工作。
package org.springframework.web;
//这个注解意思是只处理WebApplicationInitializer类或其子类
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
//webAppInitializerClasses:这个集合就是@HandlesTypes筛选出WebApplicationInitializer类或其子类的类集合
//servletContext:servlet上下文
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
//创建一个空的类型为WebApplicationInitializer的集合
List<WebApplicationInitializer> initializers = Collections.emptyList();
//判断是否存在WebApplicationInitializer的类型。
if (webAppInitializerClasses != null) {
//初始化容器
initializers = new ArrayList<>(webAppInitializerClasses.size());
//对set集合中的WebApplicationInitializer进行实例化
for (Class<?> 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)
ReflectionUtils.accessibleConstructor(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");
//进行排序,主要是对注解@Order进行生效
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
//分别调用他们的onStartUp方法,去完成servlet容器的初始化
initializer.onStartup(servletContext);
}
}
}
3.3、WebApplicationInitializer
- 它是一个SPI接口,并且只有一个方法onStartup(ServletContext),它主要是去完成由SpringServletInitializer委托给他的初始化Servlet容器的任务。
- 实际上,SpringServletInitializer并没有去完成Servlet容器的初始化工作,具体工作都是由用户定义的WebApplicationInitializer实现类去完成的。
- 像Spring的DispatcherServlet、listeners、ContextLoaderListener、filters都是通过WebApplicationInitializer的实现类来完成的。
package org.springframework.web;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
3.4、小结
3.4.1、流程梳理
- 通过SpringServletContainerInitializer来复杂对容器启动时的相关组件进行初始化
- 到底要初始化哪些组件则是通过Servlet规范中所提供的注解HandlesTypes来指定的
- 在SpringServletContainerInitializer中,其HandlesTypes注解则明确指定为了WebApplicationInitializer.class类型
- 在SpringServletContainerInitializer的onStartup方法中,则主要是完成了一些验证与组件装配的工作。
- 在SpringServletContainerInitializer的onStartup方法中,由于某些容器并未遵循Servlet规范,导致虽然明确指定了HandlesTypes注解的类型为WebApplicationInitializer.class类型,但还是可能会存在将一些非法类型传递过来的情况;所以,该方法还是对传递进来的具体类型进行了细致的判断,只有符合条件的类型才会被纳入到List集合中。
- 当以上判断完成之后,List就是接下里需要进行初始化的组件了。
- 最后通过遍历List集合,取出其中的每一个WebApplicationInitializer对象,执行他们的onStartup方法,完成组件的启动初始化。
3.4.2、总结
SpringServletContainerInitializer在整个初始化过程找那个,其扮演的角色实际上就是委托或者代理的角色,真正完成初始化工作的是那些WebApplicationInitializer实现类。
四、jar包部署
4.1、TomcatStarter
- 它是Servlet3.0规范中ServletContainerInitializer接口的实现类
- 它和SpringServletContainerInitializer所处的角色类似,都是将Servlet的具体初始化传递给下方的一个实现类,不同的是它是Spring自己new出来的,而不像SpringServletContainerInitializer是Web容器通过SPI自动调用的。
- 作用:当我们使用SpringBoot中main方法去启动项目或者将项目打包成jar包去启动的时候,这个时候根本没有web容器去调用SpringServletContainerInitializer实现初始化,而是只能通过Spring内嵌的Tomcat的方法去运行项目,这个时候就会调用TomcatStarter来启动内置Tomcat并做好一系列配置工作。
package org.springframework.boot.web.embedded.tomcat;
class TomcatStarter implements ServletContainerInitializer {
private static final Log logger = LogFactory.getLog(TomcatStarter.class);
//ServletContextInitializer数组,用于具体配置ServletContext的类
private final ServletContextInitializer[] initializers;
private volatile Exception startUpException;
//获取到所有ServletContextInitializer类
TomcatStarter(ServletContextInitializer[] initializers) {
this.initializers = initializers;
}
@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
try {
for (ServletContextInitializer initializer : this.initializers) {
//分别执行他们的onStartup方法,去完成配置servletContext的工作
initializer.onStartup(servletContext);
}
}
catch (Exception ex) {
this.startUpException = ex;
// Prevent Tomcat from logging and re-throwing when we know we can
// deal with it in the main thread, but log for information here.
if (logger.isErrorEnabled()) {
logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
+ ex.getMessage());
}
}
}
Exception getStartUpException() {
return this.startUpException;
}
}
4.2、ServletContextInitializer
- 这个接口主要用于以编程的形式去配置一个Servlet3.0+的context。它和WebApplicationInitializer类似,但是与之不同的是,实现了ServletContextInitializer接口的类不会被SpringServletContainerInitializer自动识别到,所以也就不会被Servlet容器自动启动。这个接口的设计目的是为了让ServletContextInitializer被Spring容器所管理,而并非Servlet容器。
- ServletContextInitializer和WebApplicationInitializer所处的角色类似,都是去具体完成Servlet的初始化工作的。
package org.springframework.boot.web.servlet;
@FunctionalInterface
public interface ServletContextInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
4.3、如何调用TomcatStarter这个实现类
我们发现TomcatStarter这个实现类实现了ServletContainerInitializer接口,那么是不是意味着在这个jar包中也存在一个META-INF/services/javax.servlet.ServletContainerInitializer文件呢?
如上图所示,显而易见,并没有,那么Spring是如何调用这个实现类的呢?
通过debug,我们发现是TomcatStarter是由AbstractServletWebServerFactory调用的,而它又是一个抽象的工厂类,然后我们发现它下面由三个子类,分别是:JettyServletWebServerFactory、TomcatServletWebServerFactory、UndertowServletWebServerFactory。
忽然恍然大悟,这三个分别代表中不同的web容器,于是我们打开TomcatServletWebServerFactory,发现这里面就是创建内置Tomcat容器的,并且找到了实例化TomcatStarter的代码,到这里答案就出来了,TomcatStarter是被Spring实例化出来的。
4.4、小结
- 对于Spring Boot应用来说,它并未使用SpringServletContainerInitializer来进行容器的初始化,而是使用了TomcatStarter进行的。
- TomcatStarter存在三点因素使它无法通过SPI机制进行初始化:它没有不带参数的构造方法;它的声明并非public类;其所在jar包没有META-INF.services目录,当然也就不存在名为javax.servlet.ServletContainerInitializer的文件了。
- 所有TomcatStarter是无法通过SPI机制进行查找并实例化的
- 本质上,TomcatStarter是通过Spring Boot框架new出来的
- 与SpringServletContainerInitializer类似,TomcatStarter在容器的初始化过程中也是扮演着一个委托或是代理的角色,真正执行初始化动作的实际上是由它所持有的ServletContextInitializer的onStartup方法来完成的。
五、如何通过代码的形式配置Servlet、Filter
5.1、通过Servlet提供的注解进行添加
- 通过@WebServlet、@WebFilter、@WebListener进行添加Servlet、Filter、Listener。
- 其中需要在Spring Boot的Application类上添加@ServletComponentScan才行,需要将这些组件扫描到。
下面就只演示一下Servlet是使用方法,其他Filter和Listener使用方法大差不差。注:值得注意的是,通过这种方式添加Filter时,打包成war包在项目启动时就会出错,Filter和Listener没有异常,从而得知这种方法并不可靠。
- MyServlet.java
package com.brycen.backend.servlet;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName: MyServlet
* @description: TODO
* @author: liuy
* @Version: V1.0
* @create: 2021-07-01
**/
@WebServlet("/demo")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("success");
}
}
- DemoApplication.java
package com.brycen;
@SpringBootApplication
@ServletComponentScan
public class TrucksApplication {
public static void main(String[] args) {
SpringApplication.run(TrucksApplication.class, args);
}
}
- 运行结果:
5.2、使用RegistrationBean进行添加
我们可以通过RegistrationBean来注册Servlet、Filter、Listener,它是一个抽象类,它下面有具体实现Servlet、Filter、Listener的子类ServletRegistrationBean、FilterRegistrationBean、ListenerRegistrationBean等等。下面我们就只演示Servlet、Filter,Listener用法和其差不多。
- MyServlet2.java
package com.brycen.backend.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @ClassName: MyServlet
* @description: TODO
* @author: liuy
* @Version: V1.0
* @create: 2021-07-01
**/
public class MyServlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().print("success2");
}
}
- MyFilter2.java
package com.brycen.backend.filter;
import javax.servlet.*;
import java.io.IOException;
/**
* @ClassName: MyFilter
* @description: TODO
* @author: liuy
* @Version: V1.0
* @create: 2021-07-01
**/
public class MyFilter2 implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("do filter2");
filterChain.doFilter(servletRequest,servletResponse);
}
}
- MyConfig.java
package com.brycen.backend.config;
import com.ideas.trucks.backend.filter.MyFilter2;
import com.ideas.trucks.backend.servlet.MyServlet2;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName: Myconfig
* @description: TODO
* @author: liuy
* @Version: V1.0
* @create: 2021-07-01
**/
@Configuration
public class MyConfig {
@Bean
public ServletRegistrationBean myServlet(){
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
servletRegistrationBean.addUrlMappings("/demo2");
servletRegistrationBean.setServlet(new MyServlet2());
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean myFilter(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
List<String> urls = new ArrayList<>();
urls.add("/*");
filterRegistrationBean.setUrlPatterns(urls);
filterRegistrationBean.setFilter(new MyFilter2());
return filterRegistrationBean;
}
}
- 运行结果: