浅析Struts2.5.2源码

    以往对于Struts2都是从网上查阅其工作原理及用法,没有做过深入了解,近期由于工作需要,看了一下Struts2-core的源码,趁热打铁将其中的流程梳理一下,和大家分享。此次使用的版本为Struts2.5.2

    1. Struts2入口:StrutsPrepareAndExecuteFilter

    要使用Struts,需要在web.xml配置StrutsPrepareAndExecuteFilter,如图:

    <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方法

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方法

    接下来是重头戏了,doFilter用来对拦截的请求做处理。首先先看下代码:

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两类做处理

(1)静态资源访问

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

转载于:https://my.oschina.net/blueSky4Java/blog/804417

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值