文章目錄
1、前言
在《SpringMVC學習(五)——零配置實現SpringMVC》這篇文章中我們沒有使用Spring的配置實現了一個正常的SpringMVC的功能,裏面核心的一個點就是使用了WebApplicationInitializer
,那這篇文章就詳細說明一下這個接口的作用。
2、WebApplicationInitializer的定義
從起初的Spring配置文件,到後來的Spring支持註解到後來的SpringBoot,Spring框架在一步步的使用註解的方式來去除Spring的配置的發展過程。WebApplicationInitializer
就是取代web.xml
配置的一個接口。
public interface WebApplicationInitializer {
void onStartup(ServletContext var1) throws ServletException;
}
通過覆蓋接口提供的onStartup
方法我們可以往Servlet容器
裏面添加我們需要的servlet
、listener
等,並且在Servlet容器
啓動的過程中就會加載這個接口的實現類,從而起到和web.xml
相同的中作用,從而可以替代以前在web.xml中所做的配置。
3、實現原理
我們首先可以從Spring源碼中找到SpringServletContainerInitializer
實現類。
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
LinkedList initializers = new LinkedList();
Iterator var4;
if(webAppInitializerClasses != null) {
var4 = webAppInitializerClasses.iterator();
while(var4.hasNext()) {
Class initializer = (Class)var4.next();
if(!initializer.isInterface() && !Modifier.isAbstract(initializer.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(initializer)) {
try {
initializers.add((WebApplicationInitializer)initializer.newInstance());
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}
if(initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
var4 = initializers.iterator();
while(var4.hasNext()) {
WebApplicationInitializer initializer1 = (WebApplicationInitializer)var4.next();
initializer1.onStartup(servletContext);
}
}
}
}
這個類上面有@HandlesTypes({WebApplicationInitializer.class})
這個註解,這個註解的作用是將其value中配置的一些類放入到ServletContainerInitializer
。
initializers.add((WebApplicationInitializer)initializer.newInstance());
最後通過循環去執行WebApplicationInitializer
中的onStartup
方法來實現裏面的具體的具體的邏輯。
while(var4.hasNext()) {
WebApplicationInitializer initializer1 = (WebApplicationInitializer)var4.next();
initializer1.onStartup(servletContext);
}
那問題來了,Tomcat容器怎麼知道先執行這個SpringServletContainerInitializer
類?
這裏涉及一個知識點SPI機制
,SPI
全稱爲 Service Provider Interface
,是一種服務發現機制。SPI機制
是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態爲接口替換實現類。正因此特性,我們可以很容易的通過 SPI機制
爲我們的程序提供拓展功能。
Tomcat啓動過程中查找所有的ServletContainerInitializer
實現類然後添加到StandardContext的initializers
集合中,然後執行裏面的onStartup
方法。
Spring中SPI就是通過SpringServletContainerInitializer
類來實現的
關於Web應用的啓動過程在《一個基於註解配置的Web項目的啓動流程分析》這篇文章寫的很好。
4、利用SPI我們能做什麼?
可以加一些自己的啓動配置信息,把自己的Servlet打成jar包放到Tomcat服務器或者其他工程中執行。我們可以實現一個自己的SPI接口。
4.1、定義一個MyWebAppInitializer
public interface MyWebAppInitializer {
void loadOnStart(ServletContext var1) throws ServletException;
}
4.2、定義一個MySpringServletContainerInitializer
- 實現
ServletContainerInitializer
接口 - 修改
@HandlesTypes
爲我們自己定義的MyWebAppInitializer.class
- 實例化
MyWebAppInitializer
的實現類,並且調用接口的loadOnStart
方法
@HandlesTypes(MyWebAppInitializer.class)
public class MySpringServletContainerInitializer implements ServletContainerInitializer{
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletcontext)
throws ServletException {
if (set != null) {
for (Class<?> waiClass : set) {
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
MyWebAppInitializer.class.isAssignableFrom(waiClass)) {
try {
//創建MyWebAppInitializer實現類的對象,並調用loadOnStart方法
((MyWebAppInitializer) waiClass.newInstance()).loadOnStart(servletcontext);
} catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
}
}
4.3、寫一個MyWebAppInitializer
的實現類
public class MyWebApplicationInitializerTest implements MyWebAppInitializer{
@Override
public void loadOnStart(ServletContext servletContext){
System.out.println("啓動執行MyWebApplicationInitializerTest的loadOnStart方法");
//註冊一個爲名字call的servlet
ServletRegistration.Dynamic servletReg = servletContext.addServlet("call", CallServlet.class);
servletReg.setLoadOnStartup(1);
servletReg.addMapping("/call");
}
}
其中裏面的CallServlet.java
如下
public class CallServlet extends HttpServlet {
private static final long serialVersionUID = 3684613967452881093L;
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
resp.getWriter().write(name + ", if you like me, please call me!");
}
}
4.4、添加配置文件javax.servlet.ServletContainerInitializerr
添加位置爲:src -> main -> resources-> META-INF-> services ->javax.servlet.ServletContainerInitializerr
內容:com.leo.spi.MySpringServletContainerInitializer
4.5、啓動項目測試
啓動日誌:
啓動執行MyWebApplicationInitializerTest的loadOnStart方法
瀏覽器測試:http://localhost:8080/springmvc/call?name=leo825
上面也提到了,可以把這個SPI的方式打成jar包在其他項目或者直接在Tomcat容器這種運行。
本文的相關源碼請參考:chapter-5-springmvc-zero-configuration
spi代碼包路徑:com.leo.spi
https://gitee.com/leo825/spring-framework-learning-example.git