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方法。

springboot 与servlet springboot与servlet关系_容器

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、流程梳理

  1. 通过SpringServletContainerInitializer来复杂对容器启动时的相关组件进行初始化
  2. 到底要初始化哪些组件则是通过Servlet规范中所提供的注解HandlesTypes来指定的
  3. 在SpringServletContainerInitializer中,其HandlesTypes注解则明确指定为了WebApplicationInitializer.class类型
  4. 在SpringServletContainerInitializer的onStartup方法中,则主要是完成了一些验证与组件装配的工作。
  5. 在SpringServletContainerInitializer的onStartup方法中,由于某些容器并未遵循Servlet规范,导致虽然明确指定了HandlesTypes注解的类型为WebApplicationInitializer.class类型,但还是可能会存在将一些非法类型传递过来的情况;所以,该方法还是对传递进来的具体类型进行了细致的判断,只有符合条件的类型才会被纳入到List集合中。
  6. 当以上判断完成之后,List就是接下里需要进行初始化的组件了。
  7. 最后通过遍历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文件呢?

springboot 与servlet springboot与servlet关系_springboot 与servlet_02

如上图所示,显而易见,并没有,那么Spring是如何调用这个实现类的呢?

通过debug,我们发现是TomcatStarter是由AbstractServletWebServerFactory调用的,而它又是一个抽象的工厂类,然后我们发现它下面由三个子类,分别是:JettyServletWebServerFactory、TomcatServletWebServerFactory、UndertowServletWebServerFactory。

springboot 与servlet springboot与servlet关系_容器_03

忽然恍然大悟,这三个分别代表中不同的web容器,于是我们打开TomcatServletWebServerFactory,发现这里面就是创建内置Tomcat容器的,并且找到了实例化TomcatStarter的代码,到这里答案就出来了,TomcatStarter是被Spring实例化出来的

springboot 与servlet springboot与servlet关系_java_04

4.4、小结

  1. 对于Spring Boot应用来说,它并未使用SpringServletContainerInitializer来进行容器的初始化,而是使用了TomcatStarter进行的。
  2. TomcatStarter存在三点因素使它无法通过SPI机制进行初始化:它没有不带参数的构造方法;它的声明并非public类;其所在jar包没有META-INF.services目录,当然也就不存在名为javax.servlet.ServletContainerInitializer的文件了。
  3. 所有TomcatStarter是无法通过SPI机制进行查找并实例化的
  4. 本质上,TomcatStarter是通过Spring Boot框架new出来的
  5. 与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;
    }
}
  • 运行结果: