在內嵌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