淺析Struts2.5.2源碼

    以往對於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的執行流程有了清晰的認識。第一次寫這麼長的文章,但願不是那麼雜亂不堪,能對看此文章的人有所幫助。

相關文章
相關標籤/搜索