以往对于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的执行流程有了清晰的认识。第一次写这么长的文章,但愿不是那么杂乱不堪,能对看此文章的人有所帮助。