相關文章
《Servlet3.0之四:動態注冊和Servlet容器初始化》
《SpringBoot中通過SpringBootServletInitializer如何實現組件加載》
《SpringMVC之五:自定義DispatcherServlet配置及配置額外的 servlets 和 filters》
一、web容器如何初始化第三方組件(servlet、filters)
在web容器啟動時為提供給第三方組件機會做一些初始化的工作,例如注冊servlet或者filtes等,servlet規范中通過ServletContainerInitializer
實現此功能。每個框架要使用ServletContainerInitializer
就必須在對應的jar包的META-INF/services 目錄創建一個名為javax.servlet.ServletContainerInitializer
的文件,文件內容指定具體的ServletContainerInitializer
實現類,那么,當web容器啟動時就會運行這個初始化器做一些組件內的初始化工作。一般伴隨着ServletContainerInitializer
一起使用的還有HandlesTypes
注解,通過HandlesTypes
可以將感興趣的一些類注入到ServletContainerInitializerde
的onStartup()方法作為參數傳入。
ServletContainerInitializer的源碼:
package javax.servlet; public interface ServletContainerInitializer { void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException; }
容器啟動時會自動掃描當前服務中ServletContainerInitializer的實現類,並調用其onStartup方法,其參數Set<Class<?>> c,可通過在實現類上聲明注解進行注入。(例如:可通過在實現類上聲明注解javax.servlet.annotation.HandlesTypes(WebApplicationInitializer.class)注解自動注入,@HandlesTypes會自動掃描項目中所有的WebApplicationInitializer.class的實現類,並將其全部注入Set。)
Tomcat容器的ServletContainerInitializer
機制的實現,主要交由Context容器和ContextConfig監聽器共同實現,ContextConfig監聽器負責在容器啟動時讀取每個web應用的WEB-INF/lib
目錄下包含的jar包的META-INF/services/javax.servlet.ServletContainerInitializer
,以及web根目錄下的META-INF/services/javax.servlet.ServletContainerInitializer
,通過反射完成這些ServletContainerInitializer
的實例化,然后再設置到Context容器中,最后Context容器啟動時就會分別調用每個ServletContainerInitializer
的onStartup方法,並將感興趣的類作為參數傳入。
圖-1
基本的實現機制如圖,首先通過ContextConfig監聽器遍歷每個jar包或web根目錄的META-INF/services/javax.servlet.ServletContainerInitializer
文件,根據讀到的類路徑實例化每個ServletContainerInitializer
;然后再分別將實例化好的ServletContainerInitializer
設置進Context容器中;最后Context容器啟動時分別調用所有ServletContainerInitializer
對象的onStartup方法。
假如讀出來的內容為com.seaboat.mytomcat.CustomServletContainerInitializer
,則通過反射實例化一個CustomServletContainerInitializer
對象,這里涉及到一個@HandlesTypes
注解的處理,被它標明的類需要作為參數值傳入到onStartup方法。如下例子:
@HandlesTypes({ HttpServlet.class,Filter.class }) public class CustomServletContainerInitializer implements ServletContainerInitializer { public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException { for(Class c : classes) System.out.println(c.getName()); } }
其中@HandlesTypes
標明的HttpServlet和Filter兩個class被注入到了onStartup方法。所以這個注解也是需要在ContextConfig監聽器中處理。前面已經介紹了注解的實現原理,由於有了編譯器的協助,我們可以方便地通過ServletContainerInitializer
的class對象中獲取到HandlesTypes對象,進而再獲取到注解聲明的類數組,如
HandlesTypes ht =servletContainerInitializer.getClass().getAnnotation(HandlesTypes.class); Class<?>[] types = ht.value();
即可獲取到HttpServlet和Filter的class對象數組,后面Context容器調用CustomServletContainerInitializer
對象的onStartup方法時作為參數傳入。至此,即完成了servlet規范的ServletContainerInitializer
初始化器機制。
二、Spring中初始化組件(servlet、filters)的過程
2.1、Spring中是如何初始化組件(servlet、filters)--SpringServletContainerInitializer
根據一所述在 Servlet 3.0 的環境中,容器會在 classpath 中尋找繼承了 javax.servlet.ServletContainerInitializer
接口的類,用它來配置 servlet 容器。 Spring 提供了一個繼承這個接口的類 SpringServletContainerInitializer
,在這個類中,它會尋找任何繼承了 WebApplicationInitializer
接口的類並用其來配置 servlet 容器。
先看看SpringServletContainerInitializer.java的源碼:
package org.springframework.web; @HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { //Spring框架中繼承 @Override public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>(); 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) 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); } } }
2.2、Spring中是如何初始化組件(servlet、filters)--WebApplicationInitializer
Spring將Servlet配置相關的轉給了WebApplicationInitializer,
Spring 3.2 提供了一個繼承了 WebApplicationInitializer
接口的基類 AbstractAnnotationConfigDispatcherServletInitializer
。所以,你的 servlet 配置類只需要繼承 AbstractAnnotationConfigDispatcherServletInitializer
,就會被發現而用於 servlet 容器的配置。
圖-2
***DispatcherServlet VS ContextLoaderListener 在 Spring MVC 中存在兩種應用上下文:DispatcherServlet 創建的和攔截器 ContextLoaderListener 創建的上下文: 1、DispatcherServlet:加載包含 web 組件的 bean,比如 controllers,view resolvers 和 hanlder mappings。 2、ContextLoaderListener:加載其他 bean,通常是一些中間層和數據層的組件(比如數據庫配置 bean 等)。
回到AbstractAnnotationConfigDispatcherServletInitializer
,在 AbstractAnnotationConfigDispatcherServletInitializer
中 DispatcherServlet
和 ContextLoaderListener
都會被創建,而基類中的方法就可用來創建不同的應用上下文:
getServletConfigClasses()
:定義DispatcherServlet
應用上下文中的 beansgetRootConfigClasses()
:定義攔截器ContextLoaderListener
應用上下文中的 beans
Note:為了使用 AbstractAnnotationConfigDispatcherServletInitializer
必須保證 web 服務器支持 Servlet 3.0 標准(如 tomcat 7 或更高版本) 。
圖-3
三、自定義 DispatcherServlet 配置
因為我們使用 AbstractAnnotationConfigDispatcherServletInitializer
來配置 DispatcherServlet
,同時可以通過 customizeRegistration()
方法來對DispatcherServlet進行額外的配置
。
通過 ServletRegistration.Dynamic
參數配置 DispatcherServlet
的 load-on-startup 優先級 setLoadOnStartup(int loadOnStartup)
,設置初始化參數 setInitParameters()
等。具體查看文檔 ServletRegistration.Dynamic。
在AbstractAnnotationConfigDispatcherServletInitializer將DispatcherServlet注冊到Servlet容 器中之后,就會調用customizeRegistration(),並將Servlet注冊后得到的 Registration.Dynamic傳遞進來。通過重載customizeRegistration()方法,我們可 以對DispatcherServlet進行額外的配置。
看AbstractDispatcherServletInitializer的部分源碼
protected void registerDispatcherServlet(ServletContext servletContext) { String servletName = getServletName(); Assert.hasLength(servletName, "getServletName() must not return empty or null"); WebApplicationContext servletAppContext = createServletApplicationContext(); Assert.notNull(servletAppContext, "createServletApplicationContext() did not return an application " + "context for servlet [" + servletName + "]"); FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext); dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet); Assert.notNull(registration, "Failed to register servlet with name '" + servletName + "'." + "Check if there is another servlet registered under the same name."); 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); }
例如,在本章稍后的內容中(7.2節),我們將會看到如何在Spring MVC中處理multipart請求和文 件上傳。如果計划使用Servlet 3.0對multipart配置的支持,那么需要使 用DispatcherServlet的registration來啟用multipart請求。我們可以重 載customizeRegistration()方法來設置MultipartConfigElement,如下所示:
@Override protected void customizeRegistration(Dynamic registration) { registration.setMultipartConfig(new MultipartConfigElement("/tmp/spittr/uploads")); super.customizeRegistration(registration); }
借助customizeRegistration()方法中的ServletRegistration.Dynamic,我們能 夠完成多項任務,包括通過調用setLoadOnStartup()設置load-on-startup優先級,通過 setInitParameter()設置初始化參數,通過調用setMultipartConfig()配置Servlet 3.0對multipart的支持。在前面的樣例中,我們設置了對multipart的支持,將上傳文件的臨時存 儲目錄設置在“/tmp/spittr/uploads”中。
四、配置額外的 servlets 和 filters
按照AbstractAnnotationConfigDispatcherServletInitializer的定義,它會創 建DispatcherServlet和ContextLoaderListener。如上圖-3所示。但是,如果你想注冊其他的 Servlet、Filter或Listener的話,那該怎么辦呢?
基於Java的初始化器(initializer)的一個好處就在於我們可以定義任意數量的初始化器類。因 此,如果我們想往Web容器中注冊其他組件的話,只需創建一個新的初始化器就可以了。最簡 單的方式就是實現Spring的WebApplicationInitializer接口。 例如,如下的程序清單展現了如何創建WebApplicationInitializer實現並注冊一個 Servlet。
使用 java 配置 servlet 的一個好處(不同於 web.xml)就是:可以定義任意數量的初始化類。所以,如果需要定義額外的 servlets 或 filters,只需要創建額外的初始化類。在 Spring MVC 中可以通過繼承 WebApplicationInitializer
接口來實現。
接下來,我們定義一個新的 servlet:
package com.dxz.mvcdemo1.config.servlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration.Dynamic; import org.springframework.web.WebApplicationInitializer; public class MyServletInitializer implements WebApplicationInitializer { public void onStartup(ServletContext servletContext) throws ServletException { //定義額外的 servlet Dynamic myServlet = servletContext.addServlet("myServlet", MyServlet.class); myServlet.addMapping("/custom/**"); //定義filter javax.servlet.FilterRegistration.Dynamic filter = servletContext.addFilter("myFilter", MyFilter.class); filter.addMappingForUrlPatterns(null, false, "/custom/*"); } }
當然,你也可以用來定義 filters 和 listeners:
如上面的紅底部分。
在AbstractAnnotationConfigDispatcherServletInitializer
中還有一種快捷方式:
如果你需要為 DispatcherServlet
添加 filter 的話,就不用這么麻煩了,你只要重寫 AbstractAnnotationConfigDispatcherServletInitializer
類的 getServletFilters()
方法就行了:
@Override protected Filter[] getServletFilters() { return new Filter[] { new MyFilter() }; }
不需要 mapping,因為會自動 mapping 到 DispatcherServlet
上,通過返回多個 filter,可以添加多個 filter。