摘要
博客詳細(xì)介紹如何與Servlet 3.0 的ServletContainerInitializer機(jī)制協(xié)作,通過(guò)編程式的手段,完成servlet容器配置,無(wú)需web.xml,編寫(xiě)web應(yīng)用。
一、傳統(tǒng)Web應(yīng)用
想從無(wú)到有自己搭建一個(gè)web服務(wù)器?
-- 基本不可能
這是一個(gè)造輪子的過(guò)程,而且難度還相當(dāng)大。日常所說(shuō)的實(shí)現(xiàn)Web應(yīng)用服務(wù),其實(shí)就是利用現(xiàn)有的成熟輪子(web容器,如熟知的tomcat等),再遵循Servlet規(guī)范,構(gòu)建三大組件(Servlet, Filter, Listener)以對(duì)外提供自定義HTTP服務(wù)的過(guò)程。
1)web服務(wù)啟動(dòng),main()方法在哪里?
我們?cè)诰帉?xiě)web應(yīng)用時(shí),是不會(huì)編寫(xiě)main方法的。眾所周知,java的啟動(dòng)入口是main()方法,web服務(wù)啟動(dòng)時(shí),其實(shí)是啟動(dòng)web容器的main方法,tomcat容器的啟動(dòng)入口在 org.apache.catalina.startup.Bootstrap#main。一個(gè)web服務(wù)變得可用的過(guò)程,不稱(chēng)做“啟動(dòng)”,而叫“部署”,部署到web容器。
2)傳統(tǒng)web服務(wù)部署
傳統(tǒng)的web應(yīng)用服務(wù)(有web.xml),按照servlet規(guī)范,web容器在啟動(dòng)過(guò)程中,會(huì)在約定的目錄結(jié)構(gòu)中,尋找web.xml文件,來(lái)初始化三大組件。這個(gè)約定的目錄結(jié)構(gòu)是:
root
|-[META-INF]
|-WEB-INF
|-classes
|-lib
|-web.xml
頁(yè)面,資源等可以放在根目錄下,或者根目錄下的自定義目錄中。
web應(yīng)用的部署過(guò)程,就是把對(duì)應(yīng)的資源文件,按照上述servlet規(guī)范約定的目錄一一放置正確,然后web容器啟動(dòng)的過(guò)程。
部署約定詳細(xì)說(shuō)明參考 Apache Tomcat 7 Deployment
3)ServletContext: Servlet, Filter, Listener
ServletContext定義了一個(gè)方法集合,Servlet使用這些方法與servlet容器交互,例如獲取文件的MIME類(lèi)型,分發(fā)請(qǐng)求或者寫(xiě)日志。一個(gè)JVM中的web應(yīng)用有且僅有一個(gè)servletConext。
典型的web.xml配置,會(huì)定義處理請(qǐng)求的servlet,過(guò)濾請(qǐng)求的過(guò)濾器Filter以及監(jiān)聽(tīng)各種變化的監(jiān)聽(tīng)器Listener。web容器啟動(dòng)時(shí),通過(guò)分析web.xml文件,這些定義的組件都會(huì)加入到servletContext中,以提供功能完備的HTTP服務(wù)。
所以如果忽略業(yè)務(wù)細(xì)節(jié),web應(yīng)用的開(kāi)發(fā)過(guò)程,簡(jiǎn)單的說(shuō)就是將這幾類(lèi)組件加入到servletContext,配置servletContext的過(guò)程。
二、編程式Web應(yīng)用
Servlet3.0以前,配置servletContext通過(guò)web.xml。Servlet3.0開(kāi)始,提供一種編程式的機(jī)制,允許第三方庫(kù)偵測(cè)到web容器啟動(dòng)階段,并且開(kāi)展一些必要servlet, filter, listener的編程式注冊(cè)操作(參考 Servlet3.0規(guī)范#8.2.4 共享庫(kù) / 運(yùn)行時(shí)可插拔性)。
利用這種機(jī)制,第三方組件如Spring MVC,可以以編程的方式配置servletContext。
1)Servlet3.0 ServletContainerInitializer
具體到實(shí)現(xiàn),Servlet3.0提供了接口javax.servlet.ServletContainerInitializer,如下:
ServletContainerInitializer{
onStartup(Set<Class<?>>, ServletContext):void
}
第三方組件實(shí)現(xiàn)這個(gè)接口,其onStartup()方法將會(huì)在容器啟動(dòng)階段被容器調(diào)用。這樣,第三方組件得以獲取容器的ServletContext,編程式的往ServletContext中加入需要的組件,配置ServletContext。流程很簡(jiǎn)單,如下:

2)ServletContainerInitializer 接口實(shí)現(xiàn)發(fā)現(xiàn)
那么,第三方組件的ServletContainerInitializer實(shí)現(xiàn)如何被發(fā)現(xiàn)?
根據(jù)Servlet3.0規(guī)范,ServletContainerInitializer 接口的實(shí)現(xiàn)將被運(yùn)行時(shí)的服務(wù)查找機(jī)制或語(yǔ)義上與它等價(jià)的容器特定機(jī)制發(fā)現(xiàn)。 從設(shè)計(jì)角度來(lái)看,這是一種松耦合的優(yōu)秀設(shè)計(jì)思想,容器不依賴(lài)具體組件的ServletContainerInitializer 實(shí)現(xiàn)。
實(shí)現(xiàn)ServletContainerInitializer 的第三方組件,在 META-INF/services 目錄下創(chuàng)建名為javax.servlet.ServletContainerInitializer文件,并寫(xiě)入實(shí)現(xiàn)的全限定類(lèi)名。web容器使用SPI或者等價(jià)的發(fā)現(xiàn)機(jī)制發(fā)現(xiàn)實(shí)現(xiàn)類(lèi)。
java SPI是jdk提供的一種接口實(shí)現(xiàn)發(fā)現(xiàn)機(jī)制,詳細(xì)見(jiàn)最后的擴(kuò)展章節(jié)
2)javax.servlet.annotation.HandlesTypes注解
ServletContainerInitializer 的onStartup(Set<Class<?>>, ServletContext):void方法有兩個(gè)參數(shù),其中第二個(gè)是由容器傳遞ServletContext,第一個(gè)Class<?>集合是注解HandlesTypes指定的,ServletContainerInitializer “感興趣”的類(lèi)型。
注解如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {
Class<?>[] value();
}
web容器必須將以下類(lèi)的類(lèi)對(duì)象,組織成集合,傳遞給onStartup()方法:
- 繼承@HandlesTypes指定類(lèi)的子類(lèi)(包括抽象類(lèi))
- 實(shí)現(xiàn)@HandlesTypes指定接口的子類(lèi)(含抽象類(lèi)),繼承@HandlesTypes指定接口的子接口
- 被@HandlesTypes指定注解修飾的類(lèi)/接口
利用這個(gè)特性,ServletContainerInitializer 的實(shí)現(xiàn)可以自定義其servletContext流程。
三、編程式ServletContext配置實(shí)踐
有了以上理論知識(shí),是時(shí)候?qū)嵺`一下了。
1)ServletContainerInitializer的自定義實(shí)現(xiàn)
@HandlesTypes(MyHandlesTypes.class)
public class CustomizedServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
System.out.println(c);
}
}
發(fā)現(xiàn)
src/main/resource下的 META-INF/services/目錄中新建文件javax.servlet.ServletContainerInitializer,內(nèi)容寫(xiě)入:demo.spring.mvc.framework.my.servlet.CustomizedServletContainerInitializer

2)HandlesTypes-感興趣的類(lèi)
這里,感興趣的類(lèi)是一個(gè)注解類(lèi)型:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyHandlesTypes {
}
凡是被該注解修飾的類(lèi),都會(huì)出現(xiàn)在onStartup()的參數(shù)集合中
3)啟動(dòng)
按照約定的目錄,將應(yīng)用部署到tomcat容器,并以調(diào)試模式啟動(dòng)tomcat容器??梢钥吹剑?/p>

此處可以配置servlet,filter,listener并且加入到servletConetxt即以編程的方式可完成的配置。
四、Spring MVC的編程式實(shí)現(xiàn)
Spring MVC也提供了編程方式配置servletContext。
1)SpringServletContainerInitializer
查看spring web包,SPI下的javax.servlet.ServletContainerInitializer文件在約定目錄下。

其中內(nèi)容為:
org.springframework.web.SpringServletContainerInitializer
這個(gè)類(lèi)就是javax.servlet.ServletContainerInitializer的實(shí)現(xiàn),web容器啟動(dòng)時(shí)將會(huì)調(diào)用其onStartup()方法。
2)WebApplicationInitializer
打開(kāi)SpringServletContainerInitializer的源碼,其感興趣的類(lèi)用是:
@HandlesTypes(WebApplicationInitializer.class)
SpringServletContainerInitializer的onStartup實(shí)現(xiàn)中:
- 遍歷集合
Set<Class<?>>,其中非接口,非抽象類(lèi),并且繼承自WebApplicationInitializer的類(lèi),使用反射實(shí)例化,并加入一個(gè)WebApplicationInitializer的集合; - 使用基于
org.springframework.core.annotation.Order的排序工具,將上述集合排序 - 遍歷上述集合,逐一調(diào)用其
void onStartup(ServletContext)方法
由此可知,在Spring MVC中,只要實(shí)現(xiàn)了接口org.springframework.web.WebApplicationInitializer,就可以以編程的方式,配置servletContext了。
因此編程式Spring MVC的入口是自定義的WebApplicationInitializer接口實(shí)現(xiàn)的onStartup()方法,可以在其中完成Spring容器的初始化等基本操作。
3)實(shí)例
以下是一個(gè)基于Spring MVC,編程式配置ServletContext的實(shí)例:
public class WebInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(MyMvcConfig.class); // 基于注解的spring配置
ctx.setServletContext(servletContext);
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
}
}
四、總結(jié)
得益于Servlet3.0的ServletContainerInitializer機(jī)制,第三方組件可使用編程的方式配置ServletContext。
編程式配置步驟如下:
- 實(shí)現(xiàn)ServletContainerInitializer接口,在onStartup方法中配置servletContext,加入servlet, filter, listener組件;
- 基于容器ServletContainerInitializer實(shí)現(xiàn)發(fā)現(xiàn)機(jī)制,在/META-INF/services/下新建文件;
javax.servlet.ServletContainerInitializer并寫(xiě)入具體實(shí)現(xiàn)的全限定類(lèi)名; - 按照約定的部署目錄,部署web應(yīng)用,啟動(dòng)web容器即可。
使用Spring MVC方式則更簡(jiǎn)單,省略了服務(wù)發(fā)現(xiàn)的配置,步驟如下:
- 實(shí)現(xiàn)接口WebApplicationInitializer,在onStartup方法中配置servletContext,加入servlet, filter, listener組件;
- 按照約定的部署目錄,部署web應(yīng)用,啟動(dòng)web容器即可。
Talk is cheap. Show you the code
源碼地址:
code_based_spring_mvc
擴(kuò)展
JAVA SPI 服務(wù)發(fā)現(xiàn)
SPI全稱(chēng)Service Provider Interface。不經(jīng)常出現(xiàn)在日常編碼中,因?yàn)檫@是針對(duì)廠商或者第三方插件提供的一種服務(wù)發(fā)現(xiàn)機(jī)制。JDK中java.util.ServiceLoader類(lèi)對(duì)此有詳細(xì)的介紹。
SPI是松耦合以及可插拔原則的實(shí)踐。相同的功能,往往有不同的實(shí)現(xiàn)方案,如日志功能,有l(wèi)og4j, log4j2, logback等實(shí)現(xiàn)。良好的面向?qū)ο笤O(shè)計(jì)經(jīng)驗(yàn),是不依賴(lài)具體的實(shí)現(xiàn),而依賴(lài)他們共同的接口,即基于接口編程。如果依賴(lài)了具體實(shí)現(xiàn),那么當(dāng)需要更換到另一種實(shí)現(xiàn)時(shí)(如log4j更換到log4j2),就不得不修改每一處依賴(lài)代碼;而基于接口的編程,只要將具體實(shí)現(xiàn)替換即可。這是使用日志時(shí)推薦基于使用Facade模式的common-logging或者slf4j的原因。因此需要一種接口實(shí)現(xiàn)的發(fā)現(xiàn)機(jī)制,SPI就是這樣一種接口實(shí)現(xiàn)發(fā)現(xiàn)機(jī)制,與IOC思想相似,將依賴(lài)的控制權(quán)轉(zhuǎn)移出調(diào)用者。
SPI規(guī)定,使用方依賴(lài)接口;第三方組件提供接口的實(shí)現(xiàn),并在jar包的/META-INF/services/目錄下創(chuàng)建一個(gè)與接口全限定名稱(chēng)相同的文件,文件內(nèi)容是接口實(shí)現(xiàn)的全限定名稱(chēng)。
調(diào)用方使用第三方組件時(shí),在類(lèi)路徑下遍歷所有的/META-INF/services/,尋找符合接口的具體實(shí)現(xiàn),通過(guò)反射實(shí)例化實(shí)現(xiàn),完成接口實(shí)現(xiàn)的發(fā)現(xiàn)與注入。
REFER TO
- Servlet3.0研究之ServletContainerInitializer接口
- 一個(gè)基于注解配置的Web項(xiàng)目的啟動(dòng)流程分析
- JAVA SPI
- Servlet3.0 - ServletContainerInitializer注冊(cè)JAVA組件
- Servlet3.0規(guī)范#8.2.4章節(jié): 共享庫(kù) / 運(yùn)行時(shí)可插拔性