上一篇講解了web.xml如何使用編碼的方式替換掉,但是一直沒有寫web.xml是如何被加載的相關細節,覺得十分有必要寫一篇文章來梳理下。
Web應用部署初始化
當一個web應用被部署到容器(tomcat等),tomcat系統啟動過程中會執行以下處理:
1)部署描述文件(tomcat對應web.xml)中定義的(由<listener>元素標記的)事件監聽器會被創建和初始化;
2)對於所有事件監聽器,如果實現了ServletContextListener接口,將會執行#contextInitialized()方法;
3)部署描述文件中定義的(由<filter>元素標記的)過濾器會被創建和初始化,並調用#init()方法;
4)部署描述文件中定義的(由<servlet>元素標記的)servlet會根據<load-on-startup>的權重按順序創建和初始化,並調用#init()方法。
備注:
1)<load-on-startup>權值如果設置為負整數,則不會在啟動服務器時執行;包含>=0時,會在啟動服務器時執行。
2)具體參考:《Java Servlet(十二):Servlet、Listener、Filter之間的執行流程分析》測試程序部署啟動日志。
Web的初始化流程如下:
Tomcat初始化與處理Http請求
Tomcat初始化
Tomcat初始化解析Web.xml文件
要理清web.xml如何被加載、解析,需要從tomcat源碼入手:
一個Tomcat中只有一個Server,一個Server可以包含多個Service,一個Service只有一個Container,但是可以有多個Connectors,這是因為一個服務可以有多個連接,如同時提供Http和Https鏈接,也可以提供向相同協議不同端口的連接。
在Container用於封裝和管理Servlet,以及具體的處理Request請求,在Conatiner內部包含4個子容器:
1)Engine:引擎,用來管理多個站點,一個Servcie最多只能有一個Engine;
2)Host:代表一個站點,也可以叫虛擬主機,通過配置host就可以添加站點;
3)Context:代表一個應用程序,對應着平時開發的一套程序,或者一個WEB-INF目錄以及下面的web.xml文件;
4)Wrapper:每一個Wrapper封裝這一個Servlet。
Tomca的心臟是兩個組件:Connector和Container(Engine,Host,Context,Wrapper)。一個Container可以選擇多個Connecter,多個Connector和一個Container就形成了一個Service。Service可以對外提供服務,而Server服務器控制整個Tomcat的生命周期。
從上邊分析可以知道Context是代表一個應程序,或者一個WEB-INF目錄以及下邊的web.xml文件,可以從它的作用上可以明白Context對web.xml的加載解析啟動了特別重要的作用。Tomcat的啟動流程:
從上圖可以知道Tomcat中啟動過程中會調用Context的唯一實現類StandardContext的#init()和#start(),在這里Tomcat采用了模板設計模式,StandardContext中實際實現方法對應的是initInternal()和startInternal()。
StandardContext#startInternal()
@Override
protected synchronized void startInternal() throws LifecycleException { // 此處省略代碼... try { if (ok) { // 此處省略代碼... // 1)加載web.xml // Notify our interested LifecycleListeners fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null); // 此處省略代碼... } // 此處省略代碼... // 2)合並參數 // Set up the context init params mergeParameters(); // 此處省略代碼... // 3)listener啟動 // Configure and call application event listeners if (ok) { if (!listenerStart()) { log.error(sm.getString("standardContext.listenerFail")); ok = false; } } // 此處省略代碼... // 4)filter啟動 // Configure and call application filters if (ok) { if (!filterStart()) { log.error(sm.getString("standardContext.filterFail")); ok = false; } } // 此處省略代碼... // 5)加載初始化servlet // Load and initialize all "load on startup" servlets if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail")); ok = false; } } // 此處省略代碼... } finally { // Unbinding thread unbindThread(oldCCL); } // 此處省略代碼... }
1)調用fireLifecycleEvent()
發布一個"configure_start" 事件,在眾多監聽器中有一個ContextConfig監聽器,在ContextConfig監聽到"configure_start" 事件后, 會執行configureStart()方法;在configureStart()方法中執行webConfig()開始web.xml解析;值得一提的是webConfig中調用的兩個方法:
ContextConfig#parseWebXml(contextWebXml, webXml, false)方法:
這個方法中有一個Digester工具,在Tomcat加載server.xml配置文件的時候就是使用了此工具,解析原理異曲同工。 此處使用WebRuleSet規則,將web.xml文件中的配置讀取出來設置到webXml對象中去。
ContextConfig#configureContext(StandardContext context)方法:
將web.xml文件解析出來的各個組件設置到標准servlet上下文StandardContext中去。 其中就包括我們的filter ,servlet,listener。
2)mergeParameters
步驟1)中會將web.xml中的context-param元素設置到context的parameters里,此處則是把parameters設置到servletContext里。
3)啟動listener
步驟1)中會將web.xml中的listener元素設置到context的applicationListeners里,此處則取出listener類名,創建實例,並將listener分為兩類
eventlistener:ServletRequestAttributeListener、ServletRequestListener、HttpSessionIdListener、HttpSessionAttributeListener
lifecyclelistener:ServletContextListener、HttpSessionListener
對於ServletContextListener,會調用listener.contextInitialized(event),並觸發實現了ServletContextListener接口實現類的beforeContextInitialized,afterContextInitialized方法。
4)啟動filter
步驟1)中會將web.xml中的filter元素設置到filter的filterdef里,此處則會實例化filter設置到filterConfigs里。
5)啟動servlet
步驟1)中會將web.xml中的servlet元素封裝成wrapper並調用addChild方法設置到Context里,此處則會檢查是否需要loadonstartup,如果需要則load。
更詳細介紹請參考:《Tomcat(一):Tomcat啟動時加載web.xml》
Tomcat處理Http請求
Tomcat處理Http請求過程流程圖:
假設來自客戶的請求為:http://localhost:8080/test/index.jsp 請求被發送到本機端口8080
1、用戶點擊網頁內容,請求被發送到本機端口8080,被在那里監聽的Coyote HTTP/1.1 Connector獲得。
2、Connector把該請求交給它所在的Service的Engine來處理,並等待Engine的回應。
3、Engine獲得請求localhost/test/index.jsp,匹配所有的虛擬主機Host。
4、Engine匹配到名為localhost的Host(即使匹配不到也把請求交給該Host處理,因為該Host被定義為該Engine的默認主機),名為localhost的Host獲得請求/test/index.jsp,匹配它所擁有的所有的Context。Host匹配到路徑為/test的Context(如果匹配不到就把該請求交給路徑名為“ ”的Context去處理)。
5、path=“/test”的Context獲得請求/index.jsp,在它的mapping table中尋找出對應的Servlet。Context匹配到URL PATTERN為*.jsp的Servlet,對應於JspServlet類。
6、構造HttpServletRequest對象和HttpServletResponse對象,作為參數調用JspServlet的doGet()或doPost().執行業務邏輯、數據存儲等程序。
7、Context把執行完之后的HttpServletResponse對象返回給Host。
8、Host把HttpServletResponse對象返回給Engine。
9、Engine把HttpServletResponse對象返回Connector。
10、Connector把HttpServletResponse對象返回給客戶Browser。
Tomcat哪些組件參與了Http請求處理
Connector相關組件
注意:不同的協議、不同的通信方式,ProtocolHandler會有不同的實現。在Tomcat8.5中,ProtocolHandler類的集成層級如下圖所示:
ProtocolHandler包含三個部件:Endpoint、Processor、Adapter。
1)Endpoint:用來處理底層Socket網絡連接,因此Endpoint是用來實現TCP/IP協議的;Endpoint的抽象實現類AbstractEndpoint里面定義了Acceptor和AsyncTimeout兩個內部類和一個Handler接口。
1.1)Acceptor:用於監聽請求;
1.2)AsyncTimeout:用於檢查異步Request的超時;
1.3)Handler:用於處理接收到的Socket,在內部調用Processor進行處理。
2)Processor:用來將Endpoint接收到Socket封裝成Request,Precessor用來實現Http協議的;
3)Adapter:用來將Request交給Container進行具體處理,Adapter想請求適配到Servlet容器進行具體的處理。
CoyoteAdapter#service()
Adapter
用於連接Connector
和Container
,起到承上啟下的作用。Processor
會調用Adapter.service()
方法。
@Override public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) throws Exception { Request request = (Request) req.getNote(ADAPTER_NOTES); Response response = (Response) res.getNote(ADAPTER_NOTES); // 此處忽略代碼 try { // Parse and set Catalina and configuration specific request parameters postParseSuccess = postParseRequest(req, request, res, response); if (postParseSuccess) { //check valves if we support async request.setAsyncSupported( connector.getService().getContainer().getPipeline().isAsyncSupported()); // Calling the container connector.getService().getContainer().getPipeline().getFirst().invoke( request, response); } // 此處忽略代碼 } catch (IOException e) { // Ignore } finally { // 此處忽略代碼 } }
如果請求可以被傳給容器的Pipeline即當postParseRequest方法返回true時,則由容器繼續處理,在service方法中有connector.getService().getContainer().getPipeline().getFirst().invoke(request, response)這一行:
- Connector調用getService返回StandardService;
- StandardService調用getContainer返回StandardEngine;
- StandardEngine調用getPipeline返回與其關聯的StandardPipeline;
接下來將會調用EnginePipeline#StandardEngineValve->HostPipeline#StandardHostValve->ContextPipeline#StandardContextValve->WrapperPipeline#StandardWrapperValve。
StandardWrapperValve#invoker(...)
/** * Invoke the servlet we are managing, respecting the rules regarding * servlet lifecycle and SingleThreadModel support. * * @param request Request to be processed * @param response Response to be produced * * @exception IOException if an input/output error occurred * @exception ServletException if a servlet error occurred */ @Override public final void invoke(Request request, Response response) throws IOException, ServletException { // 此處省略代碼 StandardWrapper wrapper = (StandardWrapper) getContainer(); Servlet servlet = null; Context context = (Context) wrapper.getParent(); // 此處省略代碼 // 分配一個servlet實例用來處理請求 // Allocate a servlet instance to process this request try { if (!unavailable) { // 調用StandardWrapper#allocate()方法,獲取到servlet實例 servlet = wrapper.allocate(); } } catch (UnavailableException e) { // 此處省略代碼 } catch (ServletException e) { // 此處省略代碼 } catch (Throwable e) { // 此處省略代碼 } // 此處省略代碼 // 為當前請求創建一個過濾器鏈 // Create the filter chain for this request ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); // 為當前請求調用過濾器鏈,注意:這也會調用servlet實例的service()方法 // Call the filter chain for this request // NOTE: This also calls the servlet's service() method try { if ((servlet != null) && (filterChain != null)) { // Swallow output if needed if (context.getSwallowOutput()) { try { SystemLogHandler.startCapture(); if (request.isAsyncDispatching()) { request.getAsyncContextInternal().doInternalDispatch(); } else { filterChain.doFilter(request.getRequest(), response.getResponse()); } } finally { String log = SystemLogHandler.stopCapture(); if (log != null && log.length() > 0) { context.getLogger().info(log); } } } else { if (request.isAsyncDispatching()) { request.getAsyncContextInternal().doInternalDispatch(); } else { filterChain.doFilter (request.getRequest(), response.getResponse()); } } } } catch (ClientAbortException | CloseNowException e) { // 此處省略代碼 } catch (IOException e) { // 此處省略代碼 } catch (UnavailableException e) { // 此處省略代碼 } catch (ServletException e) { // 此處省略代碼 } catch (Throwable e) { // 此處省略代碼 } finally { // 釋放資源 // Release the filter chain (if any) for this request if (filterChain != null) { filterChain.release(); } // Deallocate the allocated servlet instance try { if (servlet != null) { wrapper.deallocate(servlet); } } catch (Throwable e) { // 此處省略代碼 } // If this servlet has been marked permanently unavailable, // unload it and release this instance try { if ((servlet != null) && (wrapper.getAvailable() == Long.MAX_VALUE)) { wrapper.unload(); } } catch (Throwable e) { // 此處省略代碼 } // 此處省略代碼 } }
該StandardWrapperValve正是將Tomcat在處理Http請求過程中,真正調用filterChain(內部執行filter.doFilter())、調用servlet.service(...)代碼的地方。
ApplicationFilterChain#doFilter(...)
/** * Invoke the next filter in this chain, passing the specified request * and response. If there are no more filters in this chain, invoke * the <code>service()</code> method of the servlet itself. * * @param request The servlet request we are processing * @param response The servlet response we are creating * * @exception IOException if an input/output error occurs * @exception ServletException if a servlet exception occurs */ @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; try { java.security.AccessController.doPrivileged( new java.security.PrivilegedExceptionAction<Void>() { @Override public Void run() throws ServletException, IOException { internalDoFilter(req,res); return null; } } ); } catch( PrivilegedActionException pe) { // 此處省略代碼; } } else { internalDoFilter(request,response); } } /** * 職責鏈模式調用filter#doFilter,並在調用最底層調用servlet.service(request,response)。 */ private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // Call the next filter if there is one if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false".equalsIgnoreCase( filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } if( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res, this}; SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal); } else { filter.doFilter(request, response, this); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { // 此處省略代碼 } return; } // We fell off the end of the chain -- call the servlet instance try { // 此處省略代碼 // Use potentially wrapped request from this point if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res}; SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal); } else { servlet.service(request, response); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { // 此處省略代碼 } finally { // 此處省略代碼 } }
采用職責鏈設計模式,在filter最深層調用servlet.service(request,response)。
在Spring+SpringMvc項目中,Tomcat處理Http請求時的流程圖如下:
圖片來自《Tomcat原理系列之二:由點到線,請求主干》
SpringMVC初始化流程
啟動時需要加載部署描述文件(web.xml),我們本章的給出一個比較常見的web.xml為例,用它來分析SpringMVC的啟動流程。
<!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> <display-name>Archetype Created Web Application</display-name> <welcome-file-list> <welcome-file>/index</welcome-file> </welcome-file-list> <!--加載dao/service/一些共享組件--> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext-base.xml, classpath:applicationContext-security.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class> </listener> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <!-- 默認是false --> <param-value>false</param-value> </init-param> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>multipartFilter</filter-name> <filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class> <init-param> <param-name>multipartResolverBeanName</param-name> <param-value>multipartResolver</param-value> </init-param> </filter> <filter-mapping> <filter-name>multipartFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>hiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> <init-param> <param-name>methodParam</param-name> <param-value>_method</param-value> </init-param> </filter> <filter-mapping> <filter-name>hiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--加載springmvc controller viewsolver 等--> <servlet> <servlet-name>spring-security-01</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-security-01-servlet.xml</param-value> </init-param> <init-param> <param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>spring-security-01</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
在Spring+SpringMVC項目被部署到tomcat容器,啟動項目時會去加載web.xml配置文件,會根據web.xml配置內容去進行初始化。
Listener初始化
ContextLoaderListener實現了ServletContextListener,它本身就是一個listener,在Tomcat啟動過程中web.xml加載后,StandardContext#startInternal()方法中調用的StandardContext#listenerStart()中執行的。
public boolean listenerStart() { ... for (int i = 0; i < instances.length; i++) { if (!(instances[i] instanceof ServletContextListener)) continue; ServletContextListener listener = (ServletContextListener) instances[i]; try { fireContainerEvent("beforeContextInitialized", listener); if (noPluggabilityListeners.contains(listener)) { listener.contextInitialized(tldEvent); } else { //執行listener的初始。傳遞ServletContextEvent參數 listener.contextInitialized(event);// } fireContainerEvent("afterContextInitialized", listener); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); fireContainerEvent("afterContextInitialized", listener); getLogger().error (sm.getString("standardContext.listenerStart", instances[i].getClass().getName()), t); ok = false; } } ... }
因為ContextLoaderListener實現ServletContextListener接口,因此上邊調用ServletContextListener#contextInitialized(...)實現代碼就是ContextLoaderListener#contextInitialized(ServletContextEvent event)方法:
/** * Initialize the root web application context. 初始化web */ @Override public void contextInitialized(ServletContextEvent event) { initWebApplicationContext(event.getServletContext()); }
initWebApplicationContext(...)是ContextLoader的方法(ContextLoaderListener不但實現了ServletContextListener接口,還繼承了ContextLoader類),接下來查看ContextLoader#initWebApplicationContext(...)源碼:
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { throw new IllegalStateException( "Cannot initialize context because there is already a root application context present - " + "check whether you have multiple ContextLoader* definitions in your web.xml!"); } servletContext.log("Initializing Spring root WebApplicationContext"); Log logger = LogFactory.getLog(ContextLoader.class); if (logger.isInfoEnabled()) { logger.info("Root WebApplicationContext: initialization started"); } 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); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms"); } return this.context; } catch (RuntimeException | Error ex) { logger.error("Context initialization failed", ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } }
上邊代碼核心方法包含兩個:
1)ContextLoader#createWebApplicationContext(ServletContext sc):
createWebApplicationContext(ServletContext sc)方法用來創建WebApplicationContext實例:
/** * 返回WebApplicationContext的實現類,否則返回默認XmlWebApplicationContext或者通過開發者自定義. * 此實現要求自定義上下文實現ConfigurableWebApplicationContext接口。可以在子類中重寫。 * 此外,{#customizeContext}在刷新上下文之前被調用,從而允許子類對上下文執行自定義修改。 * @param sc 當前servlet上下文 * @return 根WebApplicationContext */ protected WebApplicationContext createWebApplicationContext(ServletContext sc) { Class<?> contextClass = determineContextClass(sc); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]"); } return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); }
ContextLoader#determineContextClass()方法:
/** * 返回WebApplicationContext的實現類,否則返回默認XmlWebApplicationContext或者通過開發者自定義. * @param servletContext 當前servlet上下文 * @return WebApplicationContext的實現類 */ protected Class<?> determineContextClass(ServletContext servletContext) { String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);//"contextClass" if (contextClassName != null) { try { return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader()); } catch (ClassNotFoundException ex) { throw new ApplicationContextException( "Failed to load custom context class [" + contextClassName + "]", ex); } } else { // 否則,讀取ContextLoader.properties中配置的key=WebApplicationContetxt.class.getName()的value,這里返回XmlWebApplicationContext.class.getName() contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName()); try { return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader()); } catch (ClassNotFoundException ex) { throw new ApplicationContextException( "Failed to load default context class [" + contextClassName + "]", ex); } } }
ContextLoader#defaultStrategies屬性:
/** * Name of the class path resource (relative to the ContextLoader class) * that defines ContextLoader's default strategy names. */ private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties"; private static final Properties defaultStrategies; static { // Load default strategy implementations from properties file. // This is currently strictly internal and not meant to be customized // by application developers. try { ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException ex) { throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage()); } }
ContextLoader.properties是spring-web-5.2.0.RELEASE.jar中內的配置文件/org/springframework/web/context/ContextLoader.properties
# Default WebApplicationContext implementation class for ContextLoader. # Used as fallback when no explicit context implementation has been specified as context-param. # Not meant to be customized by application developers. org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
結合ContextLoader.properties中配置,可以得知ContextLoader#defaultStrategies屬性值配置的值就是:{key=WebApplicationContext.class,value=XmlWebApplicationContext.class}。
2)ContextLoader#configureAndRefreshWebApplicationContext(cwac, servletContext):
configureAndRefreshWebApplicationContext(cwac, servletContext)方法作用是‘配置’和‘刷新’WebApplicationContext:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // The application context id is still set to its original default value // -> assign a more useful id based on available information String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null) { wac.setId(idParam); } else { // Generate default id... wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } wac.setServletContext(sc); String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (configLocationParam != null) { wac.setConfigLocation(configLocationParam); } // The wac environment's #initPropertySources will be called in any case when the context // is refreshed; do it eagerly here to ensure servlet property sources are in place for // use in any post-processing or initialization that occurs below prior to #refresh ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); } customizeContext(sc, wac); wac.refresh(); }
wac.setConfigLocation(configLocationParam): CONFIG_LOCATION_PARAM = "contextConfigLocation" 獲取web.xml中<context-param>標簽配置的全局變量,其中key為CONFIG_LOCATION_PARAM,也就是我們配置的相應Bean的xml文件名,並將其放入到WebApplicationContext中
wac是一個接口類:我們知道ac的真正實現類是XmlWebApplicaitonContext,因此wac.refresh()調用就是XmlWebApplicationContext#refresh(),注意XmlWebApplcationContext的refresh()是定義在它的抽象父類接口類AbstractApplicationContext.java中。
下邊看下AbstractApplicationContext#refresh()源碼:
@Override public void refresh() throws BeansException, IllegalStateException { synchronized (this.startupShutdownMonitor) { // Prepare this context for refreshing. prepareRefresh(); // Tell the subclass to refresh the internal bean factory. ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // Prepare the bean factory for use in this context. prepareBeanFactory(beanFactory); try { // Allows post-processing of the bean factory in context subclasses. postProcessBeanFactory(beanFactory); // Invoke factory processors registered as beans in the context. invokeBeanFactoryPostProcessors(beanFactory); // Register bean processors that intercept bean creation. registerBeanPostProcessors(beanFactory); // Initialize message source for this context. initMessageSource(); // Initialize event multicaster for this context. initApplicationEventMulticaster(); // Initialize other special beans in specific context subclasses. onRefresh(); // Check for listener beans and register them. registerListeners(); // Instantiate all remaining (non-lazy-init) singletons. finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event. finishRefresh(); } catch (BeansException ex) { if (logger.isWarnEnabled()) { logger.warn("Exception encountered during context initialization - " + "cancelling refresh attempt: " + ex); } // Destroy already created singletons to avoid dangling resources. destroyBeans(); // Reset 'active' flag. cancelRefresh(ex); // Propagate exception to caller. throw ex; } finally { // Reset common introspection caches in Spring's core, since we // might not ever need metadata for singleton beans anymore... resetCommonCaches(); } } }
上邊refresh()方法初始化bean,就是spring的著名的初始化方法refresh()。在Tomcat啟動時,加載Spring+SpringMVC項目中web.xml定義的ContextLoaderListener,ContextLoaderListener實現ServletContextListener接口,繼承ContextLoader加載器,進而把Tomcat與Spring連接到了一起。
Filter初始化
在監聽器listener初始化完成后,按照文章開始的講解,接下來會進行filter的初始化操作,filter的創建和初始化中沒有涉及IoC容器的相關操作,因此不是本文講解的重點,本文舉例的filter是一個用於編碼用戶請求和響應的過濾器,采用utf-8編碼用於適配中文。
關於Filter的在請求過程中與Servlet調用原理,請參考上邊Tomcat處理Http請求過程。
Servlet(DispatcherServlet)初始化
上邊在介紹Tomcat啟動過程中解析web.xml時,講到Servlet的初始化與調用核心代碼,這里將主要針對啟動這部分進一步介紹,關於調用部分不再闡述。
在Spring+SpringMVC項目中web.xml中servlet配置了DispatcherServlet,Tomcat在啟動過程中DispatcherServlet就是一個普通的Servlet(DispatcherServlet extends FrameworkServlet,FrameworkServlet extends HttpServletBean,HttpServletBean extends HttpServlet,HttpServlet extends GenericServlet,GenericServlet extends Servlet),從上邊結合Tomcat加載啟動web.xml過程可以知道StandardContext#startInternal()調用時,加載並初始化Servlet,調用的方法是StandardContext#loadOnStartup(...)。
/** * Load and initialize all servlets marked "load on startup" in the * web application deployment descriptor. * * @param children Array of wrappers for all currently defined * servlets (including those not declared load on startup) * @return <code>true</code> if load on startup was considered successful */ public boolean loadOnStartup(Container children[]) { // 1)收集到配置<load-on-startup>值大於等於0的servlet,后邊回去初始化它們。 // Collect "load on startup" servlets that need to be initialized TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>(); for (int i = 0; i < children.length; i++) { Wrapper wrapper = (Wrapper) children[i]; int loadOnStartup = wrapper.getLoadOnStartup(); if (loadOnStartup < 0) continue; Integer key = Integer.valueOf(loadOnStartup); ArrayList<Wrapper> list = map.get(key); if (list == null) { list = new ArrayList<>(); map.put(key, list); } list.add(wrapper); } // 2)初始化<load-on-startup>值大於等於0的servlet,通過調用servlet.load()方法。 // Load the collected "load on startup" servlets for (ArrayList<Wrapper> list : map.values()) { for (Wrapper wrapper : list) { try { wrapper.load(); } catch (ServletException e) { getLogger().error(sm.getString("standardContext.loadOnStartup.loadException", getName(), wrapper.getName()), StandardWrapper.getRootCause(e)); // NOTE: load errors (including a servlet that throws // UnavailableException from the init() method) are NOT // fatal to application startup // unless failCtxIfServletStartFails="true" is specified if(getComputedFailCtxIfServletStartFails()) { return false; } } } } return true; }
Wrapper的實現類結構:
其中上邊StandardContext#loadOnStartup(...)中調用的wrapper.load(),load()方法的實現就是StandardWrapper#load()。
@Override public synchronized void load() throws ServletException { instance = loadServlet(); if (!instanceInitialized) { initServlet(instance); } if (isJspServlet) { StringBuilder oname = new StringBuilder(getDomain()); oname.append(":type=JspMonitor"); oname.append(getWebModuleKeyProperties()); oname.append(",name="); oname.append(getName()); oname.append(getJ2EEKeyProperties()); try { jspMonitorON = new ObjectName(oname.toString()); Registry.getRegistry(null, null).registerComponent(instance, jspMonitorON, null); } catch (Exception ex) { log.warn("Error registering JSP monitoring with jmx " + instance); } } }
主要代碼:
1)loadServlet():加載Servlet類,並返回其實例對象;
2)initServlet():如果Servlet實例未初始化,則調用Servlet#init()方法初始化servlet。
public synchronized Servlet loadServlet() throws ServletException { // Nothing to do if we already have an instance or an instance pool if (!singleThreadModel && (instance != null)) return instance; PrintStream out = System.out; if (swallowOutput) { SystemLogHandler.startCapture(); } Servlet servlet; try { long t1=System.currentTimeMillis(); // Complain if no servlet class has been specified if (servletClass == null) { unavailable(null); throw new ServletException (sm.getString("standardWrapper.notClass", getName())); } InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager(); try { servlet = (Servlet) instanceManager.newInstance(servletClass); } catch (ClassCastException e) { // 此處省略代碼 } catch (Throwable e) { // 此處省略代碼 } if (multipartConfigElement == null) { MultipartConfig annotation = servlet.getClass().getAnnotation(MultipartConfig.class); if (annotation != null) { multipartConfigElement = new MultipartConfigElement(annotation); } } // Special handling for ContainerServlet instances // Note: The InstanceManager checks if the application is permitted // to load ContainerServlets if (servlet instanceof ContainerServlet) { ((ContainerServlet) servlet).setWrapper(this); } classLoadTime=(int) (System.currentTimeMillis() -t1); if (servlet instanceof SingleThreadModel) { if (instancePool == null) { instancePool = new Stack<>(); } singleThreadModel = true; } initServlet(servlet); fireContainerEvent("load", this); loadTime=System.currentTimeMillis() -t1; } finally { // 此處省略代碼 } return servlet; }
創建Servlet實例的方法是從Wrapper.loadServlet開始:
1)loadServlet方法要完成的就是獲取servletClass,然后把它交給InstanceManager去創建一個基於servletClass.class的對象。如果這個Servlet配置了jsp-file,那么這個servletClass就是conf/web.xml中定義的JspServlet了。
2)調用initServlet(servlet),在initServlet(...)方法內部調用Servlet#init(),對servlet進行初始化。
3)觸發Container load事件。
private synchronized void initServlet(Servlet servlet) throws ServletException { if (instanceInitialized && !singleThreadModel) return; // Call the initialization method of this servlet try { if( Globals.IS_SECURITY_ENABLED) { boolean success = false; try { Object[] args = new Object[] { facade }; SecurityUtil.doAsPrivilege("init", servlet, classType, args); success = true; } finally { if (!success) { // destroy() will not be called, thus clear the reference now SecurityUtil.remove(servlet); } } } else { servlet.init(facade); } instanceInitialized = true; } catch (UnavailableException f) { // 此處省略代碼 } catch (ServletException f) { // 此處省略代碼 } catch (Throwable f) { // 此處省略代碼 } }
初始化Servlet在StandardWrapper的initServlet方法中,調用Servlet的init()方法,同時把包裝了StandardWrapper對象的StandardWrapperFacade作為ServletConfig傳給Servlet。
Servlet的初始化過程可以通過一張圖來總結,如下所示:
因此DispatcherServlet調用init時,實際上執行的是GenericServlet#init(ServletConfig config)
package javax.servlet; ... public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable { /** * Called by the servlet container to indicate to a servlet that the servlet is being placed into service. See {@link Servlet#init}. * <p>This implementation stores the {@link ServletConfig} object it receives from the servlet container for later use. * When overriding this form of the method, call <code>super.init(config)</code>. * @param config the <code>ServletConfig</code> object that contains configutation information for this servlet * @exception ServletException if an exception occurs that interrupts the servlet's normal operation * @see UnavailableException */ public void init(ServletConfig config) throws ServletException { this.config = config; this.init(); } public void init() throws ServletException { } }
從GenericServlet#init(...)方法的實現上來看,它並未真正執行任何操作,因此我們繼續查看GenericServlet的子類是如何實現servlet初始化操作。
package org.springframework.web.servlet; ... @SuppressWarnings("serial") public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware { /** * Map config parameters onto bean properties of this servlet, and invoke subclass initialization. * @throws ServletException if bean properties are invalid (or required properties are missing), or if subclass initialization fails. */ @Override public final void init() throws ServletException { // Set bean properties from init parameters. PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); if (!pvs.isEmpty()) { try { BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); initBeanWrapper(bw); bw.setPropertyValues(pvs, true); } catch (BeansException ex) { if (logger.isErrorEnabled()) { logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex); } throw ex; } } // Let subclasses do whatever initialization they like. initServletBean(); } /** * Initialize the BeanWrapper for this HttpServletBean, possibly with custom editors. * <p>This default implementation is empty. * @param bw the BeanWrapper to initialize * @throws BeansException if thrown by BeanWrapper methods * @see org.springframework.beans.BeanWrapper#registerCustomEditor */ protected void initBeanWrapper(BeanWrapper bw) throws BeansException { } /** * Subclasses may override this to perform custom initialization. * All bean properties of this servlet will have been set before this method is invoked. * <p>This default implementation is empty. * @throws ServletException if subclass initialization fails */ protected void initServletBean() throws ServletException { } }
1)它將init-param等配置參數封裝到BeanWrapper參數內,並調用HttpServletBase#initBeanWrapper(BeanWrapper bw)來進行實現Bean的包裝初始化;
2)調用HttpServletBase#initServletBean()方法進進一步初始化Servlet bean。
但從initBeanWrapper(...)和initServletBean()方法定義不難發現,這里采用了模板方法設計模式,把真正的實現交給了子類去實現,那么我們不得不區分析它的子類如何去試下了。
在子類FrameworkServlet中對initServletBean()方法進行了重寫,接下來我們來查看FrameworkServlet中如何重寫initServletBean()方法。
package org.springframework.web.servlet; ... @SuppressWarnings("serial") public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware { /** * Overridden method of {@link HttpServletBean}, invoked after any bean properties have been set. Creates this servlet's WebApplicationContext. */ @Override protected final void initServletBean() throws ServletException { getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'"); if (logger.isInfoEnabled()) { logger.info("Initializing Servlet '" + getServletName() + "'"); } long startTime = System.currentTimeMillis(); try { // 這里是重點,用於初始化子ApplicationContext對象,主要是用來加載<servlet/>對應的servletName-servlet.xml文件如:classpath:spring-security-01-servlet.xml this.webApplicationContext = initWebApplicationContext(); initFrameworkServlet(); } catch (ServletException | RuntimeException ex) { logger.error("Context initialization failed", ex); throw ex; } if (logger.isDebugEnabled()) { String value = this.enableLoggingRequestDetails ? "shown which may lead to unsafe logging of potentially sensitive data" : "masked to prevent unsafe logging of potentially sensitive data"; logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value); } if (logger.isInfoEnabled()) { logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms"); } } /** * This method will be invoked after any bean properties have been set and * the WebApplicationContext has been loaded. The default implementation is empty; * subclasses may override this method to perform any initialization they require. * @throws ServletException in case of an initialization exception */ protected void initFrameworkServlet() throws ServletException { } }
從上邊代碼可以發現初始化工作核心方法是 initWebApplicationContext()和initFrameworkServlet(),但是initFrameworkServlet()方法是一個空方法,因此猜測核心代碼應該都在initWebApplicationContext()中實現,那么接着查看initWebApplicationContext()方法。
/** * Initialize and publish the WebApplicationContext for this servlet. * <p>Delegates to {@link #createWebApplicationContext} for actual creation of the context. Can be overridden in subclasses. * @return the WebApplicationContext instance * @see #FrameworkServlet(WebApplicationContext) * @see #setContextClass * @see #setContextConfigLocation */ protected WebApplicationContext initWebApplicationContext() { // 1)獲取由ContextLoaderListener創建的根IoC容器 // 2)獲取根IoC容器有兩種方法,還可通過key直接獲取 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()) { // 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) { // 如果當前Servelt存在一個WebApplicationContext即子IoC容器, // 並且上文獲取的根IoC容器存在,則將根IoC容器作為子IoC容器的父容器 // The context instance was injected without an explicit parent -> set // the root application context (if any; may be null) as the parent cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // 如果當前Servlet不存在一個子IoC容器則去查找一下 // No context instance was injected at construction time -> see if one // has been registered in the servlet context. If one exists, it is assumed // that the parent context (if any) has already been set and that the // user has performed any initialization such as setting the context id wac = findWebApplicationContext(); } if (wac == null) { // 如果仍舊沒有查找到子IoC容器則創建一個子IoC容器 // No context instance is defined for this servlet -> create a local one wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { // 調用子類DispatcherServlet覆蓋的onRefresh方法完成“可變”的初始化過程 // Either the context is not a ConfigurableApplicationContext with refresh // support or the context injected at construction time had already been // refreshed -> trigger initial onRefresh manually here. 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; } /** * Template method which can be overridden to add servlet-specific refresh work. * Called after successful context refresh. * <p>This implementation is empty. * @param context the current WebApplicationContext * @see #refresh() */ protected void onRefresh(ApplicationContext context) { // For subclasses: do nothing by default. }
通過函數名不難發現,該方法的主要作用同樣是創建一個WebApplicationContext對象,即Ioc容器每個Web應用最多只能存在一個根IoC容器,這里創建的則是特定Servlet擁有的子IoC容器。
如果當前Servlet存在一個IoC容器則為其設置根IoC容器作為其父類,並配置刷新該容器,用於構造其定義的Bean,這里的方法與前文講述的根IoC容器類似,同樣會讀取用戶在web.xml中配置的<servlet>中的<init-param>值,用於查找相關的xml配置文件用於構造定義的Bean,這里不再贅述了。如果當前Servlet不存在一個子IoC容器就去查找一個,如果仍然沒有查找到則調用 createWebApplicationContext()方法去創建一個,查看該方法的源碼如下圖所示:
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { Class<?> contextClass = getContextClass(); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException( "Fatal initialization error in servlet with name '" + getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext"); } ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); wac.setParent(parent); String configLocation = getContextConfigLocation(); if (configLocation != null) { wac.setConfigLocation(configLocation); } configureAndRefreshWebApplicationContext(wac); return wac; }
該方法用於創建一個子IoC容器並將根IoC容器做為其父容器,接着進行配置和刷新操作用於構造相關的Bean。至此,根IoC容器以及相關Servlet的子IoC容器已經配置完成,子容器中管理的Bean一般只被該Servlet使用,因此,其中管理的Bean一般是“局部”的,如SpringMVC中需要的各種重要組件,包括Controller、Interceptor、Converter、ExceptionResolver等。相關關系如下圖所示:
package org.springframework.web.servlet; ... @SuppressWarnings("serial") public class DispatcherServlet extends FrameworkServlet { ... /** * This implementation calls {@link #initStrategies}. */ @Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } /** * Initialize the strategy objects that this servlet uses. * <p>May be overridden in subclasses in order to initialize further strategy objects. */ protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); } ... }
onRefresh()方法直接調用了initStrategies()方法,源碼如上,通過函數名可以判斷,該方法用於初始化創建multipartResovle來支持圖片等文件的上傳、本地化解析器、主題解析器、HandlerMapping處理器映射器、HandlerAdapter處理器適配器、異常解析器、視圖解析器、flashMap管理器等,這些組件都是SpringMVC開發中的重要組件,相關組件的初始化創建過程均在此完成。
父子容器
在學習Spring時,我們都是從讀取xml配置文件來構造IoC容器,常用的類有ClassPathXmlApplicationContext類,該類存在一個初始化方法用於傳入xml文件路徑以及一個父容器,我們可以創建兩個不同的xml配置文件並實現如下代碼:
//applicationContext1.xml文件中配置一個id為baseBean的Bean //applicationContext2.xml文件中配置一個id未subBean的Bean public void testParentChhildrenIOC(){ ApplicationContext baseContext = new ClassPathXmlApplicationContext("applicationContext1.xml"); Object obj1 = baseContext.getBean("baseBean"); System.out.println("baseContext Get Bean " + obj1); ApplicationContext subContext = new ClassPathXmlApplicationContext(new String[]{"applicationContext2.xml"}, baseContext); Object obj2 = subContext.getBean("baseBean"); System.out.println("subContext get baseContext Bean " + obj2); Object obj3 = subContext.getBean("subBean"); System.out.println("subContext get subContext Bean " + obj3); //拋出NoSuchBeanDefinitionException異常 Object obj4 = baseContext.getBean("subBean"); System.out.println("baseContext get subContext Bean " + obj4); }
1)首先創建baseContext沒有為其設置父容器,接着可以成功獲取id為baseBean的Bean;
2)接着創建subContext並將baseContext設置為其父容器,subContext可以成功獲取baseBean以及subBean
3)最后試圖使用baseContext去獲取subContext中定義的subBean,此時會拋出異常NoSuchBeanDefinitionException。
由此可見,父子容器類似於類的繼承關系,子類可以訪問父類中的成員變量,而父類不可訪問子類的成員變量,同樣的,子容器可以訪問父容器中定義的Bean,但父容器無法訪問子容器定義的Bean。
參考
《第三章 DispatcherServlet詳解 ——跟開濤學SpringMVC》
《SpringMvc之DispatcherServlet詳解》
《Spring MVC入口Servlet詳解(HttpServletBean,FrameworkServlet,DispatcherServlet )》
《Spring容器 SpringMVC容器 web容器的關系》
《Tomcat源碼分析 (九)----- HTTP請求處理過程(二)》