以往對於Struts2都是從網上查閱其工做原理及用法,沒有作過深刻了解,近期因爲工做須要,看了一下Struts2-core的源碼,趁熱打鐵將其中的流程梳理一下,和你們分享。這次使用的版本爲Struts2.5.2java
1. Struts2入口:StrutsPrepareAndExecuteFilterweb
要使用Struts,須要在web.xml配置StrutsPrepareAndExecuteFilter,如圖:apache
<filter> <filter-name>action2</filter-name> <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>action2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
StrutsPrepareAndExecuteFilter是一個過濾器,實現了StrutsStatics和Filter接口,對於過濾器,你們都很熟悉,用來攔截咱們指定的請求,在doFilter方法中作一些處理,以下圖,filter中主要包含init、doFilter、destroy方法。緩存
2. 配置文件初始化:init方法tomcat
public void init(FilterConfig filterConfig) throws ServletException { InitOperations init = new InitOperations(); Dispatcher dispatcher = null; try { FilterHostConfig config = new FilterHostConfig(filterConfig); init.initLogging(config); dispatcher = init.initDispatcher(config); init.initStaticContentLoader(config, dispatcher); prepare = new PrepareOperations(dispatcher); execute = new ExecuteOperations(dispatcher); this.excludedPatterns = init.buildExcludedPatternsList(dispatcher); postInit(dispatcher, filterConfig); } finally { if (dispatcher != null) { dispatcher.cleanUpAfterInit(); } init.cleanup(); } }
當程序啓動的時候,web容器(如:tomcat)會調用init方法,對一些Struts的配置進行初始化,在代碼中,init調用initDispatcher(config)方法初始化了dispatcher,dispatcher的做用是對來自於客戶端的請求進行分發和處理,初始化的內容包括:init-param的參數,default.properties,struts-default.xml,struts-plugin.xml,struts.xml等等。篇幅有限,此處代碼再也不贅述,dispatcher初始化方法序列圖以下:安全
3. action請求處理:doFilter方法app
接下來是重頭戲了,doFilter用來對攔截的請求作處理。首先先看下代碼:ide
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; try { String uri = RequestUtils.getUri(request); //判斷url是否在exclude中,若存在則跳過此filter if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) { LOG.trace("Request {} is excluded from handling by Struts, passing request to other filters", uri); chain.doFilter(request, response); } else { LOG.trace("Checking if {} is a static resource", uri); //判斷是否訪問靜態資源 boolean handled = execute.executeStaticResourceRequest(request, response); if (!handled) { LOG.trace("Assuming uri {} as a normal action", uri); prepare.setEncodingAndLocale(request, response); prepare.createActionContext(request, response); prepare.assignDispatcherToThread(); request = prepare.wrapRequest(request); //解析request,生成actionMapping ActionMapping mapping = prepare.findActionMapping(request, response, true); if (mapping == null) { LOG.trace("Cannot find mapping for {}, passing to other filters", uri); chain.doFilter(request, response); } else { LOG.trace("Found mapping {} for {}", mapping, uri); //處理action請求 execute.executeAction(request, response, mapping); } } } } finally { prepare.cleanupRequest(request); } }
在代碼中,首先判斷url是否配置了例外,若在excludedPatterns中,則跳過,直接執行後面的filter。不然會調用execute.executeStaticResourceRequest(request, response),判斷請求的是否爲靜態資源。在doFilter中會把請求分爲靜態資源和action兩類作處理post
(1)靜態資源訪問ui
下面來看下executeStaticResourceRequest是如何處理的。
public boolean executeStaticResourceRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // there is no action in this request, should we look for a static resource? String resourcePath = RequestUtils.getServletPath(request); if ("".equals(resourcePath) && null != request.getPathInfo()) { resourcePath = request.getPathInfo(); } StaticContentLoader staticResourceLoader = dispatcher.getContainer().getInstance(StaticContentLoader.class); if (staticResourceLoader.canHandle(resourcePath)) {//判斷是否能處理 //查找靜態資源 staticResourceLoader.findStaticResource(resourcePath, request, response); // The framework did its job here return true; } else { // this is a normal request, let it pass through return false; } }
如上述代碼判斷,核心代碼僅兩處:
1) staticResourceLoader.canHandle(resourcePath)
判斷是否能處理,以下顯而易見,請求中包含/struts/和/static/路徑即認爲是訪問靜態資源
public boolean canHandle(String resourcePath) { return serveStatic && (resourcePath.startsWith("/struts/") || resourcePath.startsWith("/static/")); }
2)staticResourceLoader.findStaticResource(resourcePath, request, response); 查找靜態資源
public void findStaticResource(String path, HttpServletRequest request, HttpServletResponse response) throws IOException { String name = cleanupPath(path); for (String pathPrefix : pathPrefixes) { //找到靜態資源路徑 URL resourceUrl = findResource(buildPath(name, pathPrefix)); if (resourceUrl != null) { InputStream is = null; try { //check that the resource path is under the pathPrefix path String pathEnding = buildPath(name, pathPrefix); if (resourceUrl.getFile().endsWith(pathEnding)) is = resourceUrl.openStream(); } catch (IOException ex) { // just ignore it continue; } //not inside the try block, as this could throw IOExceptions also if (is != null) { //得到輸入流並處理 process(is, path, request, response); return; } } } response.sendError(HttpServletResponse.SC_NOT_FOUND); }
經過findResource找到靜態資源路徑,並獲取到InputStream,調用process方法處理。(此處能夠經過init-param配置packages,指定靜態資源路徑),若是容許緩存的話,在process方法中會根據報文頭中的If-Modified-Since判斷客戶端緩存是否爲最新的,若最新則直接返回304,若不是,則將輸出流返回。
protected void process(InputStream is, String path, HttpServletRequest request, HttpServletResponse response) throws IOException { if (is != null) { Calendar cal = Calendar.getInstance(); // check for if-modified-since, prior to any other headers long ifModifiedSince = 0; try { ifModifiedSince = request.getDateHeader("If-Modified-Since"); } catch (Exception e) { LOG.warn("Invalid If-Modified-Since header value: '{}', ignoring", request.getHeader("If-Modified-Since")); } long lastModifiedMillis = lastModifiedCal.getTimeInMillis(); long now = cal.getTimeInMillis(); cal.add(Calendar.DAY_OF_MONTH, 1); long expires = cal.getTimeInMillis(); //ifModifiedSince爲客戶端記錄的最後訪問時間 //我的認爲應該是ifModifiedSince > lastModifiedMillis,爲何? if (ifModifiedSince > 0 && ifModifiedSince <= lastModifiedMillis) { // not modified, content is not sent - only basic // headers and status SC_NOT_MODIFIED response.setDateHeader("Expires", expires); response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); is.close(); return; } // set the content-type header String contentType = getContentType(path); if (contentType != null) { response.setContentType(contentType); } if (serveStaticBrowserCache) { // set heading information for caching static content response.setDateHeader("Date", now); response.setDateHeader("Expires", expires); response.setDateHeader("Retry-After", expires); response.setHeader("Cache-Control", "public"); response.setDateHeader("Last-Modified", lastModifiedMillis); } else { response.setHeader("Cache-Control", "no-cache"); response.setHeader("Pragma", "no-cache"); response.setHeader("Expires", "-1"); } try { copy(is, response.getOutputStream()); } finally { is.close(); } } } /** * Copy bytes from the input stream to the output stream. * * @param input * The input stream * @param output * The output stream * @throws IOException * If anything goes wrong */ protected void copy(InputStream input, OutputStream output) throws IOException { final byte[] buffer = new byte[4096]; int n; while (-1 != (n = input.read(buffer))) { output.write(buffer, 0, n); } output.flush(); }
(2)action請求處理
接下來繼續回到doFilter方法中,若handled返回false,則會按照action進行處理。此處咱們只需重點關注下面兩行代碼:
ActionMapping mapping = prepare.findActionMapping(request, response, true);
execute.executeAction(request, response, mapping);
1)ActionMapping
在findActionMapping中,調用ActionMapper的getMapping方法將request請求解析成ActionMapping,其中包含action的name,method,namespace等信息。
public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) { ActionMapping mapping = new ActionMapping(); String uri = RequestUtils.getUri(request); int indexOfSemicolon = uri.indexOf(";"); uri = (indexOfSemicolon > -1) ? uri.substring(0, indexOfSemicolon) : uri; uri = dropExtension(uri, mapping); if (uri == null) { return null; } //得到action的name和namespace parseNameAndNamespace(uri, mapping, configManager); //得到parameters handleSpecialParameters(request, mapping); //parseActionName方法中,判斷是否開啓了DMI,開啓後纔會處理name!method請求 return parseActionName(mapping); } protected ActionMapping parseActionName(ActionMapping mapping) { if (mapping.getName() == null) { return null; } //判斷是否開啓了DMI(動態方法綁定) if (allowDynamicMethodCalls) { // handle "name!method" convention. String name = mapping.getName(); int exclamation = name.lastIndexOf("!"); if (exclamation != -1) { mapping.setName(name.substring(0, exclamation)); mapping.setMethod(name.substring(exclamation + 1)); } } return mapping; }
值得注意的是,在2.5版本中,Struts默認關閉了DMI,能夠經過設置<constant name="struts.enable.DynamicMethodInvocation" value="true"/>來開啓,具體邏輯可參考代碼片斷中註釋
2)executeAction
public void serviceAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) throws ServletException { //建立上下文 Map<String, Object> extraContext = createContextMap(request, response, mapping); // If there was a previous value stack, then create a new copy and pass it in to be used by the new Action ValueStack stack = (ValueStack) request.getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY); boolean nullStack = stack == null; if (nullStack) { ActionContext ctx = ActionContext.getContext(); if (ctx != null) { stack = ctx.getValueStack(); } } if (stack != null) { extraContext.put(ActionContext.VALUE_STACK, valueStackFactory.createValueStack(stack)); } String timerKey = "Handling request from Dispatcher"; try { UtilTimerStack.push(timerKey); String namespace = mapping.getNamespace(); String name = mapping.getName(); String method = mapping.getMethod(); //生成actionProxy,並持有ActionInvocation的實例 ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy( namespace, name, method, extraContext, true, false); request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack()); // if the ActionMapping says to go straight to a result, do it! if (mapping.getResult() != null) { Result result = mapping.getResult(); result.execute(proxy.getInvocation()); } else { //執行action proxy.execute(); } // If there was a previous value stack then set it back onto the request if (!nullStack) { request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack); } } catch (ConfigurationException e) { logConfigurationException(request, e); sendError(request, response, HttpServletResponse.SC_NOT_FOUND, e); } catch (Exception e) { if (handleException || devMode) { sendError(request, response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e); } else { throw new ServletException(e); } } finally { UtilTimerStack.pop(timerKey); } }
在executeAction中實際上調用的是dispatcher的serviceAction()方法,先會調用createContextMap建立上下文,而後調用ActionProxyFactory的createActionProxy)生成actionProxy,在建立過程當中,會建立ActionInvocation,並持有它。
protected void prepare() { String profileKey = "create DefaultActionProxy: "; try { UtilTimerStack.push(profileKey); config = configuration.getRuntimeConfiguration().getActionConfig(namespace, actionName); if (config == null && unknownHandlerManager.hasUnknownHandlers()) { config = unknownHandlerManager.handleUnknownAction(namespace, actionName); } if (config == null) { throw new ConfigurationException(getErrorMessage()); } //若找不到method,默認執行excute方法 resolveMethod(); //判斷method是否容許被執行 if (config.isAllowedMethod(method)) { //獲取配置的攔截器interceptors invocation.init(this); } else { throw new ConfigurationException(prepareNotAllowedErrorMessage()); } } finally { UtilTimerStack.pop(profileKey); } }
在2.5版本中Struts增長了安全驗證,會判斷method是否容許被執行,在struts2-core的struts-default.xml中默認配置了execute,input,back,cancel,browse,save,delete,list,index,即只容許這些method執行。
<global-allowed-methods>execute,input,back,cancel,browse,save,delete,list,index</global-allowed-methods>
能夠經過配置<global-allowed-methods>regex:.*</global-allowed-methods>放開限制。以後invocation的init方法會獲取配置的interceptors,至此action的執行準備工做就完成了,接下來回到serviceAction中,看action是如何被執行的。
public String execute() throws Exception { ActionContext nestedContext = ActionContext.getContext(); ActionContext.setContext(invocation.getInvocationContext()); String retCode = null; String profileKey = "execute: "; try { UtilTimerStack.push(profileKey); //關鍵代碼,執行invocation retCode = invocation.invoke(); } finally { if (cleanupContext) { ActionContext.setContext(nestedContext); } UtilTimerStack.pop(profileKey); } return retCode; }
在serviceAction中會調用proxy.execute(),咱們的攔截器和action將會在此方法中被執行。如上面代碼片斷,咱們看到核心的代碼是執行了invocation.invoke(),那麼invoke又是如何處理的呢?
/** * @throws ConfigurationException If no result can be found with the returned code */ public String invoke() throws Exception { String profileKey = "invoke: "; try { UtilTimerStack.push(profileKey); if (executed) { throw new IllegalStateException("Action has already executed"); } if (interceptors.hasNext()) { //-----step 1 得到一個攔截器 final InterceptorMapping interceptor = interceptors.next(); String interceptorMsg = "interceptor: " + interceptor.getName(); UtilTimerStack.push(interceptorMsg); try { //-----step 2 執行攔截器 resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this); } finally { UtilTimerStack.pop(interceptorMsg); } } else { //-----step 3 若是hasNext爲false,執行action resultCode = invokeActionOnly(); } // this is needed because the result will be executed, then control will return to the Interceptor, which will // return above and flow through again if (!executed) { if (preResultListeners != null) { LOG.trace("Executing PreResultListeners for result [{}]", result); for (Object preResultListener : preResultListeners) { PreResultListener listener = (PreResultListener) preResultListener; String _profileKey = "preResultListener: "; try { UtilTimerStack.push(_profileKey); listener.beforeResult(this, resultCode); } finally { UtilTimerStack.pop(_profileKey); } } } // now execute the result, if we're supposed to if (proxy.getExecuteResult()) { //-----step 4 最後執行result返回 executeResult(); } executed = true; } return resultCode; } finally { UtilTimerStack.pop(profileKey); } }
注意上述代碼片斷中的中文註釋,主要涉及4個步驟,得到攔截器,執行攔截器,執行action,執行result,按照如此流程action請求就執行完了,可是多個攔截器的話,對於hasNext,咱們並無看到指望的遞歸或者循環將全部的攔截器執行,那麼Struts是如何處理的呢?別急,咱們先看攔截器的intercept方法是如何執行的,Interceptor的實現類有不少,隨便拿一個來看吧,以ActionAutowiringInterceptor爲例,看一下它的intercept方法,以下:
@Override public String intercept(ActionInvocation invocation) throws Exception { if (!initialized) { ApplicationContext applicationContext = (ApplicationContext) ActionContext.getContext().getApplication().get( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); if (applicationContext == null) { LOG.warn("ApplicationContext could not be found. Action classes will not be autowired."); } else { setApplicationContext(applicationContext); factory = new SpringObjectFactory(); factory.setApplicationContext(getApplicationContext()); if (autowireStrategy != null) { factory.setAutowireStrategy(autowireStrategy); } } initialized = true; } if (factory != null) { Object bean = invocation.getAction(); factory.autoWireBean(bean); ActionContext.getContext().put(APPLICATION_CONTEXT, context); } return invocation.invoke(); }
咱們先不關心攔截器要實現什麼功能,在上述代碼中會發現方法傳遞了invocation參數,而且在return以前執行了invocation.invoke(),如此就又回到了invovation中,會繼續獲取攔截器執行。那麼結果就呼之欲出了,invocation和Interceptor經過傳參和持有實例變量,從而互相調用來達到遞歸的效果。
以前看Struts的執行流程圖一直有個誤解,認爲struts的攔截器會被執行兩次,進入和退出都會執行一次攔截器。今天看到源碼才明白,invoke前面的代碼會在action以前執行,後面的代碼會在action執行以後再被執行,這也就解釋了上圖中攔截器進出的順序問題。執行順序以下圖所示:
在執行完全部的interceptor以後,就是執行action,而後執行executeResult返回了。在executeResult中隨意看了一個VelocityResult類,大體流程就是獲取模版,賦值,而後返回輸出流之類。有興趣的同窗能夠繼續跟一下代碼。
4. 總結
本文以web.xml爲入口,對Struts2的執行流程進行了簡單的介紹,不得不說Struts的代碼仍是很牛的,在看源碼的過程當中學到了不少知識,對Struts2的執行流程有了清晰的認識。第一次寫這麼長的文章,但願不是那麼雜亂不堪,能對看此文章的人有所幫助。