這一次搞懂Spring Web零xml配置原理以及父子容器關係

夜勿語發表於2020-06-20

前言

在使用Spring和SpringMVC的老版本進行開發時,我們需要配置很多的xml檔案,非常的繁瑣,總是讓使用者自行選擇配置也是非常不好的。基於約定大於配置的規定,Spring提供了很多註解幫助我們簡化了大量的xml配置;但是在使用SpringMVC時,我們還會使用到WEB-INF/web.xml,但實際上我們是完全可以使用Java類來取代xml配置的,這也是後來SpringBoott的實現原理。本篇就來看看Spring是如何實現完全的零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是配置監聽器,Tomcat啟動會觸發監聽器呼叫;servlet則是配置我們自定義的Servlet實現,比如DispatcherServlet。還有其它很多配置就不一一說明了,在這裡主要看到記住context-paramservlet配置,這是SpringIOC父子容器的體現。在之前的I文章中講過IOC容器是以父子關係組織的,但估計大部分人都不能理解,除了看到複雜的繼承體系,並沒有看到父容器作用的體現,稍後來分析。
瞭解了配置,我們就需要思考如何替換掉這些繁瑣的配置。實際上Tomcat提供了一個規範,有一個ServletContainerInitializer介面:

public interface ServletContainerInitializer {
    void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
}

Tomcat啟動時會呼叫該介面實現類的onStartup方法,這個方法有兩個引數,第二個不用說,主要是第一個引數什麼?從哪裡來?另外我們自定義的實現類又怎麼讓Tomcat呼叫呢?
首先解答最後一個問題,這裡也是利用SPI來實現的,因此我們實現了該介面後,還需要在META-INF.services下配置。其次,這裡傳入的第一個引數也是我們自定義的擴充套件介面的實現類,我們可以通過我們自定義的介面實現很多需要在啟動時做的事,比如載入Servlet,但是Tomcat又是怎麼知道我們自定義的介面是哪個呢?這就需要用到@HandlesTypes註解,該註解就是標註在ServletContainerInitializer的實現類上,其值就是我們擴充套件的介面,這樣Tomcat就知道需要傳入哪個介面實現類到這個onStartup方法了。來看一個簡單的實現:

@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給我們提供的規範,通過這個規範我們就能實現Spring的零xml配置啟動,直接來看Spring是如何做的。
根據上面所說我們可以在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);
		}
	}

}

核心的實現就是WebApplicationInitializer,先看看其繼承體系
在這裡插入圖片描述
AbstractReactiveWebInitializer不用管,主要看另外一邊,但是都是抽象類,也就是說真的例項也是由我們自己實現,但需要我們實現什麼呢?我們一般直接繼承AbstractAnnotationConfigDispatcherServletInitializer類,有四個抽象方法需要我們實現:

    //父容器
    @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};
    }

這裡主要注意getRootConfigClassesgetServletConfigClasses方法,分別載入父、子容器:

@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方法中,直接回撥用到AbstractDispatcherServletInitializer類:

	public void onStartup(ServletContext servletContext) throws ServletException {
		super.onStartup(servletContext);
		//註冊DispatcherServlet
		registerDispatcherServlet(servletContext);
	}

先是呼叫父類:

	public void onStartup(ServletContext servletContext) throws ServletException {
		registerContextLoaderListener(servletContext);
	}

	protected void registerContextLoaderListener(ServletContext servletContext) {

		//建立spring上下文,註冊了SpringContainer
		WebApplicationContext rootAppContext = createRootApplicationContext();
		if (rootAppContext != null) {
			//建立監聽器
			ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
			listener.setContextInitializers(getRootApplicationContextInitializers());
			servletContext.addListener(listener);
		}
	}

然後呼叫createRootApplicationContext建立父容器:

	protected WebApplicationContext createRootApplicationContext() {
		Class<?>[] configClasses = getRootConfigClasses();
		if (!ObjectUtils.isEmpty(configClasses)) {
			AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
			context.register(configClasses);
			return context;
		}
		else {
			return null;
		}
	}

可以看到就是建立了一個AnnotationConfigWebApplicationContext物件,並將我們的配置類SpringContainer註冊了進去。接著建立Tomcat啟動載入監聽器ContextLoaderListener,該監聽器有一個contextInitialized方法,會在Tomcat啟動時呼叫。

	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物件,並將父容器設定到了該物件中。
父容器建立完成後自然就是子容器的建立,來到registerDispatcherServlet方法:

	protected void registerDispatcherServlet(ServletContext servletContext) {
		String servletName = getServletName();
		Assert.hasLength(servletName, "getServletName() must not return null or empty");

		//建立springmvc的上下文,註冊了MvcContainer類
		WebApplicationContext servletAppContext = createServletApplicationContext();
		Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");

		//建立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.");
		}

		/*
		* 如果該元素的值為負數或者沒有設定,則容器會當Servlet被請求時再載入。
			如果值為正整數或者0時,表示容器在應用啟動時就載入並初始化這個servlet,
			值越小,servlet的優先順序越高,就越先被載入
		* */
		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;
	}

這裡也是建立了一個AnnotationConfigWebApplicationContext物件,不同的只是這裡註冊的配置類就是我們的Servlet配置了。然後建立了DispatcherServlet物件,並將上下文物件設定了進去。看到這你可能會疑惑,既然父子容器建立的都是相同類的物件,何來的父子容器之說?別急,這個在初始化該上文時就明白了。但是這裡的初始化入口在哪呢?沒有看到任何監聽器的建立和呼叫。實際上這裡的上下文物件初始化是在Servlet初始化時實現的,即init方法,直接來到HttpServletBeaninit方法(分析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中獲取到父容器,就是通過監聽器載入的容器
		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中,實現了父子容器的組織,而這樣設計好處我想也是很清楚的,子容器目前裝載的都是MVC的配置和Bean,簡單點說就是Controller,父容器中都是Service,Controller是依賴於Service的,如果不構建這樣的層級關係並優先例項化父容器,你怎麼實現Controller層的依賴注入成功呢?

總結

本篇結合之前的文章,分析了SpringMVC零XML配置的實現原理,也補充了之前未分析到父子容器關係,讓我們能從細節上更加全面的理解SpringIOC的實現原理,相信看完本篇對於SpringBoot的實現你也會有自己的想法。

相關文章