Spring Web零xml配置原理以及父子容器關系詳解
前言
在使用Spring和SpringMVC的老版本進行開發(fā)時,我們需要配置很多的xml文件,非常的繁瑣,總是讓用戶自行選擇配置也是非常不好的?;诩s定大于配置的規(guī)定,Spring提供了很多注解幫助我們簡化了大量的xml配置;但是在使用SpringMVC時,我們還會使用到WEB-INF/web.xml,但實際上我們是完全可以使用Java類來取代xml配置的,這也是后來SpringBoott的實現(xiàn)原理。本篇就來看看Spring是如何實現(xiàn)完全的零XML配置。
正文
先來看一下原始的web.xml配置:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <context-param> <param-name>contextConfigLocation</param-name> <param-value> <!--加載spring配置--> classpath:spring.xml </param-value> </context-param> <context-param> <param-name>webAppRootKey</param-name> <param-value>ServicePlatform.root</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> <!--<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>--> </listener> <servlet> <servlet-name>spring-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <!--springmvc的配置文件--> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-dispatcher.xml</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring-dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
這里各個配置的作用簡單說下,context-param是加載我們主的sping.xml配置,比如一些bean的配置和開啟注解掃描等;listener是配置監(jiān)聽器,Tomcat啟動會觸發(fā)監(jiān)聽器調(diào)用;servlet則是配置我們自定義的Servlet實現(xiàn),比如DispatcherServlet。還有其它很多配置就不一一說明了,在這里主要看到記住context-param和servlet配置,這是SpringIOC父子容器的體現(xiàn)。
在之前的I文章中講過IOC容器是以父子關系組織的,但估計大部分人都不能理解,除了看到復雜的繼承體系,并沒有看到父容器作用的體現(xiàn),稍后來分析。
了解了配置,我們就需要思考如何替換掉這些繁瑣的配置。實際上Tomcat提供了一個規(guī)范,有一個ServletContainerInitializer接口:
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
}
Tomcat啟動時會調(diào)用該接口實現(xiàn)類的onStartup方法,這個方法有兩個參數(shù),第二個不用說,主要是第一個參數(shù)什么?從哪里來?另外我們自定義的實現(xiàn)類又怎么讓Tomcat調(diào)用呢?
首先解答最后一個問題,這里也是利用SPI來實現(xiàn)的,因此我們實現(xiàn)了該接口后,還需要在META-INF.services下配置。其次,這里傳入的第一個參數(shù)也是我們自定義的擴展接口的實現(xiàn)類,我們可以通過我們自定義的接口實現(xiàn)很多需要在啟動時做的事,比如加載Servlet,但是Tomcat又是怎么知道我們自定義的接口是哪個呢?
這就需要用到@HandlesTypes注解,該注解就是標注在ServletContainerInitializer的實現(xiàn)類上,其值就是我們擴展的接口,這樣Tomcat就知道需要傳入哪個接口實現(xiàn)類到這個onStartup方法了。
來看一個簡單的實現(xiàn):
@HandlesTypes(LoadServlet.class)
public class MyServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
Iterator var4;
if (set != null) {
var4 = set.iterator();
while (var4.hasNext()) {
Class<?> clazz = (Class<?>) var4.next();
if (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()) && LoadServlet.class.isAssignableFrom(clazz)) {
try {
((LoadServlet) clazz.newInstance()).loadOnstarp(servletContext);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
public interface LoadServlet {
void loadOnstarp(ServletContext servletContext);
}
public class LoadServletImpl implements LoadServlet {
@Override
public void loadOnstarp(ServletContext servletContext) {
ServletRegistration.Dynamic initServlet = servletContext.addServlet("initServlet", "org.springframework.web.servlet.DispatcherServlet");
initServlet.setLoadOnStartup(1);
initServlet.addMapping("/init");
}
}
這就是Tomcat給我們提供的規(guī)范,通過這個規(guī)范我們就能實現(xiàn)Spring的零xml配置啟動,直接來看Spring是如何做的。根據(jù)上面所說我們可以在spring-web工程下找到META-INF/services/javax.servlet.ServletContainerInitializer配置:
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
if (webAppInitializerClasses != null) {
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");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
核心的實現(xiàn)就是WebApplicationInitializer,先看看其繼承體系

AbstractReactiveWebInitializer不用管,主要看另外一邊,但是都是抽象類,也就是說真的實例也是由我們自己實現(xiàn),但需要我們實現(xiàn)什么呢?我們一般直接繼承AbstractAnnotationConfigDispatcherServletInitializer類,有四個抽象方法需要我們實現(xiàn):
//父容器
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{SpringContainer.class};
}
//SpringMVC配置子容器
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{MvcContainer.class};
}
//獲取DispatcherServlet的映射信息
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
// filter配置
@Override
protected Filter[] getServletFilters() {
MyFilter myFilter = new MyFilter();
CorsFilter corsFilter = new CorsFilter();
return new Filter[]{myFilter,corsFilter};
}
這里主要注意getRootConfigClasses和getServletConfigClasses方法,分別加載父、子容器:
@ComponentScan(value = "com.dark",excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
})
public class SpringContainer {
}
@ComponentScan(value = "com.dark",includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
},useDefaultFilters = false)
public class MvcContainer {
}
看到這兩個類上的注解應該不陌生了吧,父容器掃描裝載了所有不帶@Controller注解的類,子容器則相反,但需要對象時首先從當前容器中找,如果沒有則從父容器中獲取,為什么要這么設計呢?
直接放到一個容器中不行么?先思考下, 稍后解答。回到onStartup方法中,直接回調(diào)用到AbstractDispatcherServletInitializer類:
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
//注冊DispatcherServlet
registerDispatcherServlet(servletContext);
}
先是調(diào)用父類:
public void onStartup(ServletContext servletContext) throws ServletException {
registerContextLoaderListener(servletContext);
}
protected void registerContextLoaderListener(ServletContext servletContext) {
//創(chuàng)建spring上下文,注冊了SpringContainer
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
//創(chuàng)建監(jiān)聽器
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
listener.setContextInitializers(getRootApplicationContextInitializers());
servletContext.addListener(listener);
}
}
然后調(diào)用createRootApplicationContext創(chuàng)建父容器:
protected WebApplicationContext createRootApplicationContext() {
Class<?>[] configClasses = getRootConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(configClasses);
return context;
}
else {
return null;
}
}
可以看到就是創(chuàng)建了一個AnnotationConfigWebApplicationContext對象,并將我們的配置類SpringContainer注冊了進去。接著創(chuàng)建Tomcat啟動加載監(jiān)聽器ContextLoaderListener,該監(jiān)聽器有一個contextInitialized方法,會在Tomcat啟動時調(diào)用。
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
long startTime = System.currentTimeMillis();
try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
return this.context;
}
}
可以看到就是去初始化容器,這個和之前分析xml解析是一樣的,主要注意這里封裝了ServletContext對象,并將父容器設置到了該對象中。
父容器創(chuàng)建完成后自然就是子容器的創(chuàng)建,來到registerDispatcherServlet方法:
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");
//創(chuàng)建springmvc的上下文,注冊了MvcContainer類
WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
//創(chuàng)建DispatcherServlet
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
if (registration == null) {
throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
"Check if there is another servlet registered under the same name.");
}
/*
* 如果該元素的值為負數(shù)或者沒有設置,則容器會當Servlet被請求時再加載。
如果值為正整數(shù)或者0時,表示容器在應用啟動時就加載并初始化這個servlet,
值越小,servlet的優(yōu)先級越高,就越先被加載
* */
registration.setLoadOnStartup(1);
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());
Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}
customizeRegistration(registration);
}
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
Class<?>[] configClasses = getServletConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
context.register(configClasses);
}
return context;
}
這里也是創(chuàng)建了一個AnnotationConfigWebApplicationContext對象,不同的只是這里注冊的配置類就是我們的Servlet配置了。然后創(chuàng)建了DispatcherServlet對象,并將上下文對象設置了進去。
看到這你可能會疑惑,既然父子容器創(chuàng)建的都是相同類的對象,何來的父子容器之說?
別急,這個在初始化該上文時就明白了。但是這里的初始化入口在哪呢?沒有看到任何監(jiān)聽器的創(chuàng)建和調(diào)用。
實際上這里的上下文對象初始化是在Servlet初始化時實現(xiàn)的,即init方法,直接來到HttpServletBean的init方法(分析SpringMVC源碼時講過):
public final void init() throws ServletException {
...省略
// Let subclasses do whatever initialization they like.
initServletBean();
}
protected final void initServletBean() throws ServletException {
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
}
protected WebApplicationContext initWebApplicationContext() {
//這里會從servletContext中獲取到父容器,就是通過監(jiān)聽器加載的容器
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
//容器加載
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = findWebApplicationContext();
}
if (wac == null) {
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
看到這里想你也應該明白了,首先從ServletContext中拿到父容器,然后設置到當前容器的parent中,實現(xiàn)了父子容器的組織,而這樣設計好處我想也是很清楚的,子容器目前裝載的都是MVC的配置和Bean,簡單點說就是Controller,父容器中都是Service,Controller是依賴于Service的,如果不構建這樣的層級關系并優(yōu)先實例化父容器,你怎么實現(xiàn)Controller層的依賴注入成功呢?
總結
本篇結合之前的文章,分析了SpringMVC零XML配置的實現(xiàn)原理,也補充了之前未分析到父子容器關系,讓我們能從細節(jié)上更加全面的理解SpringIOC的實現(xiàn)原理,相信看完本篇對于SpringBoot的實現(xiàn)你也會有自己的想法。希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Java開發(fā)中POJO和JSON互轉(zhuǎn)時如何忽略隱藏字段的問題
這篇文章主要介紹了Java開發(fā)中POJO和JSON互轉(zhuǎn)時如何忽略隱藏字段的問題,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-02-02
深入理解Java8新特性之Lambda表達式的基本語法和自定義函數(shù)式接口
Lambda 表達式,也可稱為閉包,它是推動 Java 8 發(fā)布的最重要新特性。Lambda 允許把函數(shù)作為一個方法的參數(shù)(函數(shù)作為參數(shù)傳遞進方法中)。使用 Lambda 表達式可以使代碼變的更加簡潔緊湊2021-11-11
SpringBoot URL帶有特殊字符([]/{}等),報400錯誤的解決
這篇文章主要介紹了SpringBoot URL帶有特殊字符([]/{}等),報400錯誤的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
SpringBoot實現(xiàn)簡易支付寶網(wǎng)頁支付功能
小編最近實現(xiàn)一個功能基于springboot程序的支付寶支付demo,非常不錯適合初學者入門學習使用,今天把SpringBoot實現(xiàn)簡易支付寶網(wǎng)頁支付功能的示例代碼分享給大家,感興趣的朋友參考下吧2021-10-10
詳解Spring中singleton?bean如何同時服務多個請求
這篇文章主要介紹了詳解Spring中singleton?bean如何同時服務多個請求2023-02-02
java工具類之實現(xiàn)java獲取文件行數(shù)
這篇文章主要介紹了一個java工具類,可以取得當前項目中所有java文件總行數(shù),代碼行數(shù),注釋行數(shù),空白行數(shù),需要的朋友可以參考下2014-03-03
詳解hashCode()和equals()的本質(zhì)區(qū)別和聯(lián)系
這篇文章主要介紹了詳解hashCode()和equals()的本質(zhì)區(qū)別和聯(lián)系,本文先對兩種方法作了介紹,然后對二者聯(lián)系進行分析,具有一定參考價值,需要的朋友可以了解下。2017-09-09

