Spring Web零xml配置原理以及父子容器關(guān)系詳解
前言
在使用Spring和SpringMVC的老版本進(jìn)行開(kāi)發(fā)時(shí),我們需要配置很多的xml文件,非常的繁瑣,總是讓用戶自行選擇配置也是非常不好的。基于約定大于配置的規(guī)定,Spring提供了很多注解幫助我們簡(jiǎn)化了大量的xml配置;但是在使用SpringMVC時(shí),我們還會(huì)使用到WEB-INF/web.xml,但實(shí)際上我們是完全可以使用Java類來(lái)取代xml配置的,這也是后來(lái)SpringBoott的實(shí)現(xiàn)原理。本篇就來(lái)看看Spring是如何實(shí)現(xiàn)完全的零XML配置。
正文
先來(lái)看一下原始的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>
這里各個(gè)配置的作用簡(jiǎn)單說(shuō)下,context-param是加載我們主的sping.xml配置,比如一些bean的配置和開(kāi)啟注解掃描等;listener是配置監(jiān)聽(tīng)器,Tomcat啟動(dòng)會(huì)觸發(fā)監(jiān)聽(tīng)器調(diào)用;servlet則是配置我們自定義的Servlet實(shí)現(xiàn),比如DispatcherServlet。還有其它很多配置就不一一說(shuō)明了,在這里主要看到記住context-param和servlet配置,這是SpringIOC父子容器的體現(xiàn)。
在之前的I文章中講過(guò)IOC容器是以父子關(guān)系組織的,但估計(jì)大部分人都不能理解,除了看到復(fù)雜的繼承體系,并沒(méi)有看到父容器作用的體現(xiàn),稍后來(lái)分析。
了解了配置,我們就需要思考如何替換掉這些繁瑣的配置。實(shí)際上Tomcat提供了一個(gè)規(guī)范,有一個(gè)ServletContainerInitializer接口:
public interface ServletContainerInitializer { void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException; }
Tomcat啟動(dòng)時(shí)會(huì)調(diào)用該接口實(shí)現(xiàn)類的onStartup方法,這個(gè)方法有兩個(gè)參數(shù),第二個(gè)不用說(shuō),主要是第一個(gè)參數(shù)什么?從哪里來(lái)?另外我們自定義的實(shí)現(xiàn)類又怎么讓Tomcat調(diào)用呢?
首先解答最后一個(gè)問(wèn)題,這里也是利用SPI來(lái)實(shí)現(xiàn)的,因此我們實(shí)現(xiàn)了該接口后,還需要在META-INF.services下配置。其次,這里傳入的第一個(gè)參數(shù)也是我們自定義的擴(kuò)展接口的實(shí)現(xiàn)類,我們可以通過(guò)我們自定義的接口實(shí)現(xiàn)很多需要在啟動(dòng)時(shí)做的事,比如加載Servlet,但是Tomcat又是怎么知道我們自定義的接口是哪個(gè)呢?
這就需要用到@HandlesTypes注解,該注解就是標(biāo)注在ServletContainerInitializer的實(shí)現(xiàn)類上,其值就是我們擴(kuò)展的接口,這樣Tomcat就知道需要傳入哪個(gè)接口實(shí)現(xiàn)類到這個(gè)onStartup方法了。
來(lái)看一個(gè)簡(jiǎn)單的實(shí)現(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ò)這個(gè)規(guī)范我們就能實(shí)現(xiàn)Spring的零xml配置啟動(dòng),直接來(lái)看Spring是如何做的。根據(jù)上面所說(shuō)我們可以在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); } } }
核心的實(shí)現(xiàn)就是WebApplicationInitializer,先看看其繼承體系
AbstractReactiveWebInitializer不用管,主要看另外一邊,但是都是抽象類,也就是說(shuō)真的實(shí)例也是由我們自己實(shí)現(xiàn),但需要我們實(shí)現(xiàn)什么呢?我們一般直接繼承AbstractAnnotationConfigDispatcherServletInitializer類,有四個(gè)抽象方法需要我們實(shí)現(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 { }
看到這兩個(gè)類上的注解應(yīng)該不陌生了吧,父容器掃描裝載了所有不帶@Controller注解的類,子容器則相反,但需要對(duì)象時(shí)首先從當(dāng)前容器中找,如果沒(méi)有則從父容器中獲取,為什么要這么設(shè)計(jì)呢?
直接放到一個(gè)容器中不行么?先思考下, 稍后解答?;氐給nStartup方法中,直接回調(diào)用到AbstractDispatcherServletInitializer類:
public void onStartup(ServletContext servletContext) throws ServletException { super.onStartup(servletContext); //注冊(cè)DispatcherServlet registerDispatcherServlet(servletContext); }
先是調(diào)用父類:
public void onStartup(ServletContext servletContext) throws ServletException { registerContextLoaderListener(servletContext); } protected void registerContextLoaderListener(ServletContext servletContext) { //創(chuàng)建spring上下文,注冊(cè)了SpringContainer WebApplicationContext rootAppContext = createRootApplicationContext(); if (rootAppContext != null) { //創(chuàng)建監(jiān)聽(tīng)器 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)建了一個(gè)AnnotationConfigWebApplicationContext對(duì)象,并將我們的配置類SpringContainer注冊(cè)了進(jìn)去。接著創(chuàng)建Tomcat啟動(dòng)加載監(jiān)聽(tīng)器ContextLoaderListener,該監(jiān)聽(tīng)器有一個(gè)contextInitialized方法,會(huì)在Tomcat啟動(dòng)時(shí)調(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; } }
可以看到就是去初始化容器,這個(gè)和之前分析xml解析是一樣的,主要注意這里封裝了ServletContext對(duì)象,并將父容器設(shè)置到了該對(duì)象中。
父容器創(chuàng)建完成后自然就是子容器的創(chuàng)建,來(lái)到registerDispatcherServlet方法:
protected void registerDispatcherServlet(ServletContext servletContext) { String servletName = getServletName(); Assert.hasLength(servletName, "getServletName() must not return null or empty"); //創(chuàng)建springmvc的上下文,注冊(cè)了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."); } /* * 如果該元素的值為負(fù)數(shù)或者沒(méi)有設(shè)置,則容器會(huì)當(dāng)Servlet被請(qǐng)求時(shí)再加載。 如果值為正整數(shù)或者0時(shí),表示容器在應(yīng)用啟動(dòng)時(shí)就加載并初始化這個(gè)servlet, 值越小,servlet的優(yōu)先級(jí)越高,就越先被加載 * */ 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)建了一個(gè)AnnotationConfigWebApplicationContext對(duì)象,不同的只是這里注冊(cè)的配置類就是我們的Servlet配置了。然后創(chuàng)建了DispatcherServlet對(duì)象,并將上下文對(duì)象設(shè)置了進(jìn)去。
看到這你可能會(huì)疑惑,既然父子容器創(chuàng)建的都是相同類的對(duì)象,何來(lái)的父子容器之說(shuō)?
別急,這個(gè)在初始化該上文時(shí)就明白了。但是這里的初始化入口在哪呢?沒(méi)有看到任何監(jiān)聽(tīng)器的創(chuàng)建和調(diào)用。
實(shí)際上這里的上下文對(duì)象初始化是在Servlet初始化時(shí)實(shí)現(xiàn)的,即init方法,直接來(lái)到HttpServletBean的init方法(分析SpringMVC源碼時(shí)講過(guò)):
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() { //這里會(huì)從servletContext中獲取到父容器,就是通過(guò)監(jiān)聽(tīng)器加載的容器 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; }
看到這里想你也應(yīng)該明白了,首先從ServletContext中拿到父容器,然后設(shè)置到當(dāng)前容器的parent中,實(shí)現(xiàn)了父子容器的組織,而這樣設(shè)計(jì)好處我想也是很清楚的,子容器目前裝載的都是MVC的配置和Bean,簡(jiǎn)單點(diǎn)說(shuō)就是Controller,父容器中都是Service,Controller是依賴于Service的,如果不構(gòu)建這樣的層級(jí)關(guān)系并優(yōu)先實(shí)例化父容器,你怎么實(shí)現(xiàn)Controller層的依賴注入成功呢?
總結(jié)
本篇結(jié)合之前的文章,分析了SpringMVC零XML配置的實(shí)現(xiàn)原理,也補(bǔ)充了之前未分析到父子容器關(guān)系,讓我們能從細(xì)節(jié)上更加全面的理解SpringIOC的實(shí)現(xiàn)原理,相信看完本篇對(duì)于SpringBoot的實(shí)現(xiàn)你也會(huì)有自己的想法。希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- springboot 在xml里讀取yml的配置信息的示例代碼
- 在Spring Boot中加載XML配置的完整步驟
- SpringMvc web.xml配置實(shí)現(xiàn)原理過(guò)程解析
- SpringBoot集成JmsTemplate(隊(duì)列模式和主題模式)及xml和JavaConfig配置詳解
- spring通過(guò)導(dǎo)入jar包和配置xml文件啟動(dòng)的步驟詳解
- Spring boot AOP通過(guò)XML配置文件聲明的方法
- Spring基于xml文件配置Bean過(guò)程詳解
- Spring實(shí)戰(zhàn)之使用ClassPathResource加載xml資源示例
- Spring如何基于xml實(shí)現(xiàn)聲明式事務(wù)控制
相關(guān)文章
Java開(kāi)發(fā)中POJO和JSON互轉(zhuǎn)時(shí)如何忽略隱藏字段的問(wèn)題
這篇文章主要介紹了Java開(kāi)發(fā)中POJO和JSON互轉(zhuǎn)時(shí)如何忽略隱藏字段的問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02深入理解Java8新特性之Lambda表達(dá)式的基本語(yǔ)法和自定義函數(shù)式接口
Lambda 表達(dá)式,也可稱為閉包,它是推動(dòng) Java 8 發(fā)布的最重要新特性。Lambda 允許把函數(shù)作為一個(gè)方法的參數(shù)(函數(shù)作為參數(shù)傳遞進(jìn)方法中)。使用 Lambda 表達(dá)式可以使代碼變的更加簡(jiǎn)潔緊湊2021-11-11SpringBoot URL帶有特殊字符([]/{}等),報(bào)400錯(cuò)誤的解決
這篇文章主要介紹了SpringBoot URL帶有特殊字符([]/{}等),報(bào)400錯(cuò)誤的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02SpringBoot實(shí)現(xiàn)簡(jiǎn)易支付寶網(wǎng)頁(yè)支付功能
小編最近實(shí)現(xiàn)一個(gè)功能基于springboot程序的支付寶支付demo,非常不錯(cuò)適合初學(xué)者入門學(xué)習(xí)使用,今天把SpringBoot實(shí)現(xiàn)簡(jiǎn)易支付寶網(wǎng)頁(yè)支付功能的示例代碼分享給大家,感興趣的朋友參考下吧2021-10-10詳解Spring中singleton?bean如何同時(shí)服務(wù)多個(gè)請(qǐng)求
這篇文章主要介紹了詳解Spring中singleton?bean如何同時(shí)服務(wù)多個(gè)請(qǐng)求2023-02-02springboot對(duì)壓縮請(qǐng)求的處理方法
這篇文章主要介紹了springboot對(duì)壓縮請(qǐng)求的處理,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05Java Lambda表達(dá)式的方法引用和構(gòu)造器引用實(shí)例分析
這篇文章主要介紹了Java Lambda表達(dá)式的方法引用和構(gòu)造器引用,結(jié)合實(shí)例形式分析了Lambda表達(dá)式的方法引用和構(gòu)造器引用相關(guān)原理、用法及操作注意事項(xiàng),需要的朋友可以參考下2019-09-09java工具類之實(shí)現(xiàn)java獲取文件行數(shù)
這篇文章主要介紹了一個(gè)java工具類,可以取得當(dāng)前項(xiàng)目中所有java文件總行數(shù),代碼行數(shù),注釋行數(shù),空白行數(shù),需要的朋友可以參考下2014-03-03詳解hashCode()和equals()的本質(zhì)區(qū)別和聯(lián)系
這篇文章主要介紹了詳解hashCode()和equals()的本質(zhì)區(qū)別和聯(lián)系,本文先對(duì)兩種方法作了介紹,然后對(duì)二者聯(lián)系進(jìn)行分析,具有一定參考價(jià)值,需要的朋友可以了解下。2017-09-09