在內嵌Servlet容器下Spring Boot中如何加載Servlet?

在內嵌Servlet容器下Spring Boot中如何加載Servlet?

0、什麼是ServletContext?

在這裏插入圖片描述
以上是ServletContext源碼描述,基本上就是與Servlet容器(例如常用的Tomcat)通信的對象,該對象被ServletConfig對象持有。我們將Servlet註冊到Servlet容器中,就是通過該對象實現的。

文章主要內容:
1、傳統方法(servlet3.0 以前)
2、servlet3.0新特性 ,提供消除web.xml的特性
3、Spring對Servlet的支持
4、SpringBoot如何加載Servlet,消除web.xml

一、傳統方法(servlet3.0 以前)

這裏的傳統方法,指通過web.xml文件來配置Servlet或Filter的過程,這種方式簡單(理解簡單,使用簡單)粗暴。

基本步驟:
1、寫一個Servlet或Filter
2、通過web.xml配置Servlet或Filter的類全路徑以及映射處理關係

二、servlet3.0新特性,提供消除web.xml的特性

這些新特性的其中一個重要的目的在於簡化 Web 應用的開發和部署。

2.1通過註解簡化web開發

新增註解類:@WebServlet,@WebFilter ,@WebListener;這些註解等價於在web.xml文件中添加的相關配置信息,也就是說,不用在web.xml文件中進行配置了。

  • 思考問題:這些被註解的類(servlet 、filter、listener)如何被識別呢?

2.2通過動態加載servlet 、filter、listener簡化web開發

servlet3.0 規範還提供了更強大的功能,可以在運行時動態註冊 servlet ,filter,listener。
ServletContext 爲動態配置 Servlet 增加了如下方法:
新增Servlet:

public ServletRegistration.Dynamic addServlet(String servletName, String className);

新增Filter

public FilterRegistration.Dynamic addFilter(String filterName, String className);

新增Listener

public void addListener(String className);

還有許多重載的方法,可以通過代碼的方式(code-based)而不是web.xml的方式(xml-based)來動態註冊 servlet ,filter,listener。

  • 思考問題:ServletContext加載servlet ,filter,listener的時機?

2.3Servlet容器啓動時通過ServletContext加載Servlet

容器在啓動時使用 JAR 服務 API(JAR Service API) 來發現 ServletContainerInitializer 的實現類,並且容器將 WEB-INF/lib 目錄下 JAR 包中的類都交給實現類的 onStartup()方法處理,ServletContainerInitializer 是 Servlet 3.0 新增的一個接口。
例如:

public class MyServletContainerInitializer implements ServletContainerInitializer {
  private final static String PROCSS_URL= "/hello";
  @Override
  public void onStartup(Set<Class<?>> c, ServletContext servletContext) {
  
  //創建 helloWorldServlet...
    ServletRegistration.Dynamic servlet = servletContext.addServlet(
            HelloWorldServlet.class.getSimpleName(),
            HelloWorldServlet.class);
    //對指定URL進行處理
    servlet.addMapping(PROCSS_URL);

  //創建 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); 
   //對指定URL進行處理
    filter.addMappingForUrlPatterns(dispatcherTypes, true, PROCSS_URL);
  }
}

爲了保證以上的實現類可以被Servlet容器檢測到,則需要採用SPI(Service Provider Interface)機制來配置一下該實現類,具體來說:
在項目路徑下創建 META-INF/services/javax.servlet.ServletContainerInitializer 文件來做到的,它只包含一行內容:

MyServletContainerInitializer的全路徑名。例如:comg.ct.peng.MyServletContainerInitializer

注意:這麼一串javax.servlet.ServletContainerInitializer這是文件名

這樣當Servlet容器啓動的時候,就會通過代碼的方式來註冊Servlet.Filter了

三、Spring對Servlet特性(ServletContainerInitializer)的支持

Spring 提供了對ServletContainerInitializer的支持,我們可以看到源碼如下:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
	public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				//1 由於 servlet 廠商實現的差異,onStartup 方法會加載我們本不想處理的 class,所以進行了特判。
				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);
		//2 加載servletContext
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

}

解釋:

  • ServletContainerInitializer的實現類上使用 @HandlesTypes 註解來指定希望被處理的類,過濾掉不希望給 onStartup() 處理的類。這裏表示,只希望在WEB-INF/lib 目錄中處理WebApplicationInitializer類型的類。
  • spring 並沒有在 SpringServletContainerInitializer 中直接對 servlet 和filter 進行註冊,而是委託給了一個陌生的類 WebApplicationInitializer,WebApplicationInitializer 類便是 spring 用來初始化 web 環境的委託者類。

典型的非web.xml配置如SpringMVC中的DispatcherServlet,並是在AbstractDispatcherServletInitializer中被註冊到Servlet容器中的。AbstractDispatcherServletInitializer是WebApplicationInitializer 的一個實現類。

四、SpringBoot如何註冊Servlet,消除web.xml

4.1 方式一:Servlet新增的註解+ServletComponentScan

//Servlet
@WebServlet("/hello")
public class HelloWorldServlet extends HttpServlet{}

//Filter
@WebFilter("/hello/*")
public class HelloWorldFilter implements Filter {}

//SpringBoot啓動類
@SpringBootApplication
@ServletComponentScan
public class SpringBootServletApplication {

   public static void main(String[] args) {
      SpringApplication.run(SpringBootServletApplication.class, args);
   }
}

注意:從ServletComponentScan的描述中我可以看到,該方式只適合在SpringBoot內嵌容器中使用。

Enables scanning for Servlet components ({@link WebFilter filters}, {@link WebServlet servlets}, and {@link WebListener listeners}). Scanning is only performed when using an embedded Servlet container

4.2 方式二:RegistrationBean

RegistrationBean源碼描述:Base class for Servlet 3.0+ based registration beans.
Spring Boot提供的針對Servlet 3.0+容器的註冊bean基類。該類的目的是用於向Servlet容器(Tomcat,Jetty等)註冊工作組件,比如Servlet,Filter或者EventListener。
這是一個抽象基類,實現了接口ServletContextInitializer。
ServletContextInitializer約定了當前RegistrationBean會在Servlet啓動時被調用方法#onStartup。

可以看一下UML
在這裏插入圖片描述
RegistrationBean實現了ServletContextInitializer,其下有Servlet,Filter,Listener的實現了。
如果大家對SpringSecurity有所熟悉,我們可以看到DelegatingFilterProxyRegistrationBean這個類,就是註冊我們DelegatingFilterProxy的關鍵。
典型的使用方式:

@Bean
public ServletRegistrationBean helloWorldServlet() {
    ServletRegistrationBean helloWorldServlet = new ServletRegistrationBean();
    //添加映射
    myServlet.addUrlMappings("/hello");
    //添加Servlet
    myServlet.setServlet(new HelloWorldServlet());
    return helloWorldServlet;
}
  • 問題:這些RegistrationBean如何被處理的呢?

五、Spring Boot加載Servlet的深度分析(內嵌容器)

我們知道Servlet容器提供ServletContainerInitializer接口,這樣通過代碼的形式爲應用添加Servlet提供了很好的途徑。我們觀察ServletContainerInitializer的實現類的UML:
在這裏插入圖片描述
其中需要注意:

  • 當使用SpringBoot內嵌Servlet容器時,則Servlet註冊機制就是TomcatStarter.
  • 當使用外部Servlet容器時,則Servlet註冊機制就是採用的SpringServletContainerInitializer

我們主要討論的是當使用SpringBoot內嵌Servlet容器時,Servlet的註冊機制。

5.1 突破口TomcatStarter

查看TomcatStarter中的onStartup方法:

/**
 * {@link ServletContainerInitializer} used to trigger {@link ServletContextInitializer
 * ServletContextInitializers} and track startup errors.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @since 1.2.1
 */
class TomcatStarter implements ServletContainerInitializer {
	private static final Log logger = LogFactory.getLog(TomcatStarter.class);
	//1 初始化器 很重要
	private final ServletContextInitializer[] initializers;
	private volatile Exception startUpException;
	TomcatStarter(ServletContextInitializer[] initializers) {
		this.initializers = initializers;
	}
		@Override
	public void onStartup(Set<Class<?>> classes, ServletContext servletContext)
			throws ServletException {
		try {
			for (ServletContextInitializer initializer : this.initializers) {
				//加載Servlet初始化信息的核心部分
				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());
			}
		}
	}

	public Exception getStartUpException() {
		return this.startUpException;
	}

}

5.2 ServletContextInitializer 是什麼?

Spring Boot提供的在Servlet 3.0+環境中用於程序化配置ServletContext的接口。該接口ServletContextInitializer主要被RegistrationBean實現用於往ServletContext容器中註冊Servlet,Filter或者EventListener。這些ServletContextInitializer的設計目的主要是用於這些實例被Spring IoC容器管理。這些ServletContextInitializer實例不會被SpringServletContainerInitializer檢測,因此不會被Servlet容器自動啓動。

簡而言之:用來向Spring的IOC容器中註冊Servlet,Filter或者EventListener。

  • 問題:爲什麼要將Servlet,Filter或者EventListener交個Spring IOC容器管理?
    Servlet容器由Spring IOC容器管理,Servlet容器需要的組件,自然也會交由Spring IOC容器管理
    在這裏插入圖片描述

5.3 TomcatStarter 使用了哪些ServletContextInitializer ?

我們通過debug可以看到這些ServletContextInitializer
在這裏插入圖片描述
從命名來看EmbeddedWebApplicationContext $1@6348這是EmbeddedWebApplicationContext的一個匿名類。由於調用規則比較繁瑣,直接給出結論:
1、當TomcatStarter 調用onStartup()方法時,會遍歷所有的ServletContextInitializer 的onStartup(servletContext)方法;
2、當ServletContextInitializer 的onStartup(servletContext)方法被執行的時候,會在Spring IOC容器中去搜索到了所有的 RegisterBean 並按照順序加載到 ServletContext 中。

5.4 TomcatStarter中的ServletContextInitializer 是何時得到的?

TomcatStarter,不是通過SPI方式加載的,我們猜測是直接new出來的,通過搜索源碼可以看到,在TomcatEmbeddedServletContainerFactory中的configureContext()中創建了該對象。
在這裏插入圖片描述
TomcatEmbeddedServletContainerFactory是怎麼創建的呢?
在這裏插入圖片描述

六、總結

這篇文章參考了大神徐靖峯的文章,後面有原文鏈接,真的非常感謝!
文中關於在外部容器中運行的情況後續有時間再研究一下,
文中留的幾個問題我想大家應該已經有了自己的答案了哇!

  • 思考問題:這些被註解的類(servlet 、filter、listener)如何被識別呢?
  • 思考問題:ServletContext加載servlet ,filter,listener的時機?
  • 問題:爲什麼要將Servlet,Filter或者EventListener交個Spring IOC容器管理?
  • 思考問題:當使用外部Servlet容器運行web程序時,Servlet註冊機制又是什麼呢?

優秀博文參考:Spring 揭祕 – 尋找遺失的 web.xml

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章