1. 程式人生 > >struts2執行流程

struts2執行流程

寫在前面:struts2在web應用層面屬於表示層框架,在MVC開發模式中屬於C(Controller控制器),負責對M(Model模型)和V(View檢視)進行解耦。struts2是在struts1和webwork的技術基礎上進行了合併的全新的框架。struts2雖然和struts1在名字上很相似,但是卻不是後者的升級版。struts2其實是以另一個表示層框架webwork為核心,採用攔截器的機制來處理使用者的請求,這樣的設計也使得業務邏輯控制器能夠與ServletAPI完全脫離開,所以struts2也可以理解為webwork的更新產品。從它們的處理請求的執行流程就可以看出相似點。

直觀感受一下:

webwork:
這裡寫圖片描述

struts2:

這裡寫圖片描述

struts2和webwork都是通過一個FilterDispatcher過濾器來匹配客戶端傳送的所有請求(當然,現在struts2的過濾器名是StrutsPreparedAndExecuteFilter),不同於struts1是通過一個servlet來匹配所有請求,好了,這裡先簡單的瞭解一下struts2和struts1的區別, 在文章末尾會詳細的介紹struts2和struts1的區別,開始進入主題。

struts2的執行流程(結合流程圖分析):

  1. 客戶端傳送一個HTTP請求

  2. 該請求被struts2的核心過濾器StrutsPreparedAndExecuteFilter匹配(只要是在過濾器的url-pattern中配置了/*,那麼任何請求都會進入該過濾器,無論該請求是否需要struts2來處理),當然,在進入這個過濾器之前會依次進入在web.xml中配置的位置在struts2過濾器之前的其他Filter或Servlet

  3. struts2的過濾器會詢問(形象一點的說法,其實就是呼叫方法)ActionMapper該請求是否有與之對應的業務控制類,如果沒有,則放行,如果有,進入下一步執行流程

  4. struts2通過ActionProxy例項化ActionInvocation,當然在這之前ActionProxy還會通過ConfigurationManager按序載入struts2的配置檔案:default.properties, struts-default.xml, struts.properties, struts.xml…(先載入struts預設的,然後才是自己定義的),正是因為載入了這些配置檔案所以struts才能找到相應的攔截器以及業務控制類。

  5. ActionProxy初始化一個ActionInvocation並通過它的invoke來正式執行一系列的攔截器以及Action,在執行完Action之後會根據使用的模板(jsp, velocity, freemarker…)組裝結果集Result,渲染頁面

  6. 返回給客戶端響應

接下來詳細的分析一下:

1. 客戶端傳送一個HTTP請求

可能是一個登陸請求,也可能是查詢某個功能列表的請求…

2. StrutsPreparedAndExecuteFilter過濾器攔截該請求

過濾器攔截到該請求的時候會呼叫doFilter方法,如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    // 1.將ServletRequest和ServletResponse物件轉換為HttpServletRequest和HttpServletResponse物件
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;

    try {
        // 2.對不由struts2處理的請求放行,這個excludedPatterns是一個List<Pattern>集合,裡面儲存了不被struts2的過濾器匹配的url
        if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
            chain.doFilter(request, response);
        } else {
            // 3.設定請求和相應的編碼以及國際化的相關資訊
            prepare.setEncodingAndLocale(request, response);
            // 4.建立一個Action的上下文,並初始化一個本地執行緒
            // 原始碼註釋:Creates the action context and initializes the thread local
            prepare.createActionContext(request, response);
            // 5.把dispatcher指派給本地執行緒
            // 原始碼註釋:Assigns the dispatcher to the dispatcher thread local
            prepare.assignDispatcherToThread();
            // 6.包裝一下request防止它是一個multipart/form-data型別的請求
            // 原始碼註釋:Wrap request first, just in case it is multipart/form-data
            request = prepare.wrapRequest(request);
            // 7.查詢ActionMapping資訊(包括name,namespace,method,extention,params,result)
            ActionMapping mapping = prepare.findActionMapping(request, response, true);
            // 8.沒有找到請求對應的業務控制類
            if (mapping == null) {
                boolean handled = execute.executeStaticResourceRequest(request, response);
                if (!handled) {
                    chain.doFilter(request, response);
                }
            } else {
            // 9.找到了對應的業務控制類那就去執行該Action
                execute.executeAction(request, response, mapping);
            }
        }
    } finally {
        // 10.釋放掉這個Request所佔用的一些記憶體空間
        prepare.cleanupRequest(request);
    }
}

2.1. 首先將ServletRequest和ServletResponse物件轉換為HttpServletRequest和HttpServletResponse物件

2.2. 判斷是否設定了不被struts2過濾器攔截的請求

if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
    chain.doFilter(request, response);
}

這個excludedPatterns是一個List< Pattern >集合,裡面包含了不被struts2過濾器攔截的url。看這一句:prepare.isUrlExcluded(request, excludedPatterns),判斷這個請求裡面是否包含這樣的url,跟進原始碼檢視一具體的實現:

public boolean isUrlExcluded( HttpServletRequest request, List<Pattern> excludedPatterns ) {
    if (excludedPatterns != null) {
        // 1.獲取當前請求中的uri
        String uri = RequestUtils.getUri(request);
        // 2.檢視集合中是否有與之匹配的,有就返回true
        for ( Pattern pattern : excludedPatterns ) {
            if (pattern.matcher(uri).matches()) {
                return true;
            }
        }
    }
    return false;
}

知道了攔截器是通過excludedPatterns來判斷哪個url不被攔截,那麼這個excludedPatterns的值是從哪裡來的呢?初步猜測是在StrutsPreparedAndExecuteFilter初始化(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(filterConfig.getServletContext(), dispatcher);
        execute = new ExecuteOperations(filterConfig.getServletContext(), dispatcher);
        // 就是這裡,建立了不匹配的url列表
        this.excludedPatterns = init.buildExcludedPatternsList(dispatcher);

        postInit(dispatcher, filterConfig);
    } finally {
        if (dispatcher != null) {
            dispatcher.cleanUpAfterInit();
        }
        init.cleanup();
    }
}

跟進buildExcludedPatternsList方法:

public List<Pattern> buildExcludedPatternsList( Dispatcher dispatcher ) {
    // 由此可知是struts2是讀取了STRUTS_ACTION_EXCLUDE_PATTERN常量的值來判斷哪些請求不需要匹配
    return buildExcludedPatternsList(dispatcher.getContainer().getInstance(String.class, StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN));
}

private List<Pattern> buildExcludedPatternsList( String patterns ) {
    if (null != patterns && patterns.trim().length() != 0) {
        List<Pattern> list = new ArrayList<Pattern>();
        String[] tokens = patterns.split(",");
        for ( String token : tokens ) {
            list.add(Pattern.compile(token.trim()));
        }
        return Collections.unmodifiableList(list);
    } else {
        return null;
    }
}

struts2是根據STRUTS_ACTION_EXCLUDE_PATTERN常量的值來判斷哪些請求不需要匹配,所以我們如果想要設定某些請求不被struts2匹配就可以設定這個常量struts.action.excludePattern,多個pattern之間用逗號隔開(pattern的寫法和web.xml中配置的類似)

2.3 設定請求和響應的編碼以及國際化的相關資訊

prepare.setEncodingAndLocale(request, response),是由Dispatcher在準備的過程中完成的,原始碼如下:

/**
 * Sets the request encoding and locale on the response
 * 設定請求和響應的國際化編碼
 */
public void setEncodingAndLocale(HttpServletRequest request, HttpServletResponse response) {
    dispatcher.prepare(request, response);
}

跟進prepare方法一看究竟:

/**
 * Prepare a request, including setting the encoding and locale.
 *
 * @param request The request
 * @param response The response
 */
public void prepare(HttpServletRequest request, HttpServletResponse response) {
    String encoding = null;
    if (defaultEncoding != null) {
        encoding = defaultEncoding;
    }
    // check for Ajax request to use UTF-8 encoding strictly http://www.w3.org/TR/XMLHttpRequest/#the-send-method
    if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
        encoding = "UTF-8";
    }

    Locale locale = null;
    if (defaultLocale != null) {
        locale = LocalizedTextUtil.localeFromString(defaultLocale, request.getLocale());
    }

    if (encoding != null) {
        applyEncoding(request, encoding);
    }

    if (locale != null) {
        response.setLocale(locale);
    }

    if (paramsWorkaroundEnabled) {
        request.getParameter("foo"); // simply read any parameter (existing or not) to "prime" the request
    }
}

設定編碼的具體實現。嗯,程式碼寫的蠻好…

2.4 建立一個Action的上下文,並初始化一個本地執行緒

/**
 * Creates the action context and initializes the thread local
 */
public ActionContext createActionContext(HttpServletRequest request, HttpServletResponse response) {
    ActionContext ctx;
    // 計數器,作用:記錄這個Action被訪問的次數,與後續釋放ActionContext的記憶體空間有關,初始化為1
    Integer counter = 1;
    Integer oldCounter = (Integer) request.getAttribute(CLEANUP_RECURSION_COUNTER);
    // 如果request域裡面已經有一個計數器,說明這個Action已經被例項化呼叫過了,那麼就將這個計數器的值加1
    if (oldCounter != null) {
        counter = oldCounter + 1;
    }
    // 和上面的計數器類似,嘗試從request獲取這個Action的上下文物件,如果存在就直接使用這個ActionContext
    ActionContext oldContext = ActionContext.getContext();
    if (oldContext != null) {
        // detected existing context, so we are probably in a forward
        ctx = new ActionContext(new HashMap<String, Object>(oldContext.getContextMap()));
    } else {
        // 不存在就建立一個值棧(儲存了與這個Action相關的一些屬性)和一個ActionContext
        ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
        stack.getContext().putAll(dispatcher.createContextMap(request, response, null, servletContext));
        ctx = new ActionContext(stack.getContext());
    }
    // 把這個計數器放到request域裡面
    request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);
    // 跟進這個方法後有這麼個註釋:Sets the action context for the current thread(把這個Action上下文物件放入當前執行緒)
    ActionContext.setContext(ctx);
    return ctx;
}
2.4.1 建立Action計數器

這個計數器可是有點用處:它用來記錄一個Action被呼叫的次數。那麼為什麼要記錄它被呼叫的次數呢?這裡先提前看一下doFilter方法的最後一步:prepare.cleanupRequest(request);這一步是用來清理掉該次請求所佔用的記憶體,跟進原始碼:

/**
 * Cleans up a request of thread locals
 */
public void cleanupRequest(HttpServletRequest request) {
    // 獲取request域中計數器
    Integer counterVal = (Integer) request.getAttribute(CLEANUP_RECURSION_COUNTER);
    if (counterVal != null) {
        // 這個計數器的值不為空就把它減一
        counterVal -= 1;
        // 重新放入request域,用CLEARUP_RECURSION_COUNTER這個常量儲存
        request.setAttribute(CLEANUP_RECURSION_COUNTER, counterVal);
        // 關鍵:如果這個計數器在減一之後的值仍然大於0,那麼就不釋放它所佔用的記憶體,記錄一條日誌就直接返回了
        if (counterVal > 0 ) {
            if (log.isDebugEnabled()) {
                log.debug("skipping cleanup counter="+counterVal);
            }
            return;
        }
    }
    // 否則就clearUp掉
    // always clean up the thread request, even if an action hasn't been executed
    try {
        dispatcher.cleanUpRequest(request);
    } finally {
        ActionContext.setContext(null);
        Dispatcher.setInstance(null);
    }
}

相信看到這裡,大家大致已經明白了這個計數器存在的意義:記錄Action被請求的次數,如果請求的次數非常頻繁,說明這個Action被呼叫的次數非常多,那麼就暫時不釋放掉它所佔用的記憶體,反之,如果只請求了一次或者是幾次,那麼在這個Action執行完畢後就會釋放掉它所佔用的記憶體。

2.4.2 建立ActionContext

跟進原始碼看一下具體的實現:

public class ActionContext implements Serializable {

    static ThreadLocal<ActionContext> actionContext = new ThreadLocal<ActionContext>();

    // 處理請求的Action的name
    public static final String ACTION_NAME = "com.opensymphony.xwork2.ActionContext.name";

    // 與這個Action相關的值棧
    public static final String VALUE_STACK = ValueStack.VALUE_STACK;

    // session相關
    public static final String SESSION = "com.opensymphony.xwork2.ActionContext.session";

    // application域
    public static final String APPLICATION = "com.opensymphony.xwork2.ActionContext.application";
    ...

這個ActionContext儲存了一些與當前Action相關的資訊。

2.5 把dispatcher指派給本地執行緒-prepare.assignDispatcherToThread()

其實從上一步我們就不難看出struts2為每一個Action建立一個執行緒,這也體現了struts2相比於struts1的優勢:執行緒安全,因為每一個Action都由一個單獨的執行緒來負責,不存在共享資料,所以安全。我們進入PrepareOperations類的assignDispatcherToThread()方法的原始碼看一下:

public void assignDispatcherToThread() {
    // 將當前的這個dispatcher例項化
    Dispatcher.setInstance(dispatcher);
}

再進入Dispatcher的setInstance方法中看一下:

public static void setInstance(Dispatcher instance) {
    // 呼叫了這個Dispatcher的instance屬性的set方法
    // ps:這個instance是一個ThreadLocal物件
    Dispatcher.instance.set(instance);
}

再進入set方法中看一下:

public void set(T value) {
Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

終於找到根源了,獲取到當前的執行緒,把dispatcher放入當前執行緒中。那麼這個Dispatcher有什麼作用呢?看一下原始碼,這個原始碼有點多,就摘抄一個重要的方法來看下吧:

/**
 * 初始化一系列的配置檔案:default.properties, struts-default.xml, struts.properties, struts.xml...檔案,並且是按順序載入
 */
public void init() {

    if (configurationManager == null) {
        configurationManager = createConfigurationManager(DefaultBeanSelectionProvider.DEFAULT_BEAN_NAME);
    }

    try {
        init_FileManager();
        init_DefaultProperties(); // [1]default.properties
        init_TraditionalXmlConfigurations(); // [2]struts-default.xml...
        init_LegacyStrutsProperties(); // [3]struts.properties
        init_CustomConfigurationProviders(); // [5]struts.xml
        init_FilterInitParameters() ; // [6]初始化過濾器配置的引數
        init_AliasStandardObjects() ; // [7]別名什麼的..這裡就不深究了

        Container container = init_PreloadConfiguration();
        container.inject(this);
        init_CheckWebLogicWorkaround(container);

        if (!dispatcherListeners.isEmpty()) {
            for (DispatcherListener l : dispatcherListeners) {
                l.dispatcherInitialized(this);
            }
        }
    } catch (Exception ex) {
        if (LOG.isErrorEnabled())
            LOG.error("Dispatcher initialization failed", ex);
        throw new StrutsException(ex);
    }
}

Dispatcher可以用來初始化一系列的配置檔案,並且是按序載入。

2.6 包裝一下request

request = prepare.wrapRequest(request);

wrapRequest方法的原始碼如下:

public HttpServletRequest wrapRequest(HttpServletRequest oldRequest) throws ServletException {
    HttpServletRequest request = oldRequest;
    try {
        // Wrap request first, just in case it is multipart/form-data
        // 為了防止這個請求是一個multipart/form-data(上傳檔案)型別的請求
        // parameters might not be accessible through before encoding (ww-1278)
        // 因為這種型別請求的引數如果不經過處理可能獲取不到
        request = dispatcher.wrapRequest(request, servletContext);
    } catch (IOException e) {
        throw new ServletException("Could not wrap servlet request with MultipartRequestWrapper!", e);
    }
    return request;
}

為了防止這個請求是一個multipart/form-data(上傳檔案)型別的請求,將它包裝一下。因為這種型別請求的引數如果不經過處理可能獲取不到。繼續進入dispatcher.wrapRequest的原始碼中:

public HttpServletRequest wrapRequest(HttpServletRequest request, ServletContext servletContext) throws IOException {
    // don't wrap more than once
    // 只包裝一次
    if (request instanceof StrutsRequestWrapper) {
        return request;
    }

    String content_type = request.getContentType();
    // 如果請求型別不為空並且是multipart/form-data型別的請求,那麼就包裝一下
    if (content_type != null && content_type.contains("multipart/form-data")) {
        MultiPartRequest mpr = getMultiPartRequest();
        LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
        request = new MultiPartRequestWrapper(mpr, request, getSaveDir(servletContext), provider);
    } else {
        request = new StrutsRequestWrapper(request, disableRequestAttributeValueStackLookup);
    }

    return request;
}

2.7 查詢該請求相關的資訊actionMapping

prepare.findActionMapping(request, response, true),原始碼如下:

public ActionMapping findActionMapping(HttpServletRequest request, HttpServletResponse response, boolean forceLookup) {
    ActionMapping mapping = (ActionMapping) request.getAttribute(STRUTS_ACTION_MAPPING_KEY);
    if (mapping == null || forceLookup) {
        try {
            // 這裡就開始詢問ActionMaper是否存在與該請求對應的業務控制類,詳見下面的第三大步
            mapping = dispatcher.getContainer().getInstance(ActionMapper.class).getMapping(request, dispatcher.getConfigurationManager());
            if (mapping != null) {
                request.setAttribute(STRUTS_ACTION_MAPPING_KEY, mapping);
            }
        } catch (Exception ex) {
            dispatcher.sendError(request, response, servletContext, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex);
        }
    }

    return mapping;
}

發現這個ActionMapping其實是通過ActionMapper的getMapping方法來獲得的

ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager);

發現這個ActionMapper是一個介面,這裡我啟動我的專案debug發現是呼叫了它的實現類:DefaultActionMapper的getMapping方法,實現細節如下:

tips:在eclipse中,按Ctrl+T可以檢視當前類的子類或者實現類

public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) {
    ActionMapping mapping = new ActionMapping();
    // 通過request工具類獲取當前請求的uri
    String uri = RequestUtils.getUri(request);

    int indexOfSemicolon = uri.indexOf(";");
    uri = (indexOfSemicolon > -1) ? uri.substring(0, indexOfSemicolon) : uri;
    // 去掉字尾.action...
    uri = dropExtension(uri, mapping);
    if (uri == null) {
        return null;
    }
    // 解析出configManager裡面設定的actionName和namespace並放入ActionMapping
    parseNameAndNamespace(uri, mapping, configManager);
    handleSpecialParameters(request, mapping);
    return parseActionName(mapping);
}

在getMaping方法中,獲得當前請求中的uri以及配置檔案中配置的action的name和namespace,看一下這個parseNameAndNamespace方法的實現細節:

protected void parseNameAndNamespace(String uri, ActionMapping mapping, ConfigurationManager configManager) {
    String namespace, name;
    int lastSlash = uri.lastIndexOf("/");
    ...
    // 將解析出來的name和namespace放入mapping
    mapping.setNamespace(namespace);
    mapping.setName(cleanupActionName(name));
}

從倒數兩行程式碼可以看出了該方法的最終目的:將解析出來的name和namespace放入mapping,交給呼叫者來根據這個actionMapping判斷請求是否有對應的業務控制類

2.8 沒有找到請求對應的業務控制類所進行的操作

說明並沒有為這個請求配置相應的業務控制類Action,就說明這個請求可能是一個靜態的資源請求,於是就有了如下程式碼:

if (mapping == null) {
   boolean handled = execute.executeStaticResourceRequest(request, response);
   // 這裡會根據上一步的返回值來確定是否執行
    if (!handled) {
        chain.doFilter(request, response);
    }
}

檢視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?
    // 老外還挺逗:這個請求沒有對應的action來處理,我們應不應該看看它是不是請求一個靜態資源?
    String resourcePath = RequestUtils.getServletPath(request);

    if ("".equals(resourcePath) && null != request.getPathInfo()) {
        resourcePath = request.getPathInfo();
    }

    StaticContentLoader staticResourceLoader = dispatcher.getContainer().getInstance(StaticContentLoader.class);
    // 如果這個請求請求的資源在這個專案的資源路徑下,返回true
    if (staticResourceLoader.canHandle(resourcePath)) {
        staticResourceLoader.findStaticResource(resourcePath, request, response);
        // The framework did its job here
        // 屬於struts2管轄範圍的靜態資源,由struts2來處理
        return true;

    } else {
        // this is a normal request, let it pass through
        // 一個普通的請求,放行
        return false;
    }
}

簡單點說就是:這個靜態資源在專案中存在就返回,不存在就放行,交給其他的過濾器處理。

2.9 如果找到了這個請求對應的業務控制類Action

那就呼叫ExecuteOperations的executeAction方法去執行這個Action,原始碼如下:

public void executeAction(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) throws ServletException {
  dispatcher.serviceAction(request, response, servletContext, mapping);
}

它又呼叫了Dispatcher的serviceAction方法,原始碼如下:

public void serviceAction(HttpServletRequest request, HttpServletResponse response, ServletContext context,
                      ActionMapping mapping) throws ServletException {

    Map<String, Object> extraContext = createContextMap(request, response, mapping, context);

    // 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);
        // 獲取ActionMapping中的namespace,name,method
        String namespace = mapping.getNamespace();
        String name = mapping.getName();
        String method = mapping.getMethod();

        Configuration config = configurationManager.getConfiguration();
        // 建立ActionProxy代理
        ActionProxy proxy = config.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!
        // 如果mapping中有結果集,那麼就去執行結果集
        if (mapping.getResult() != null) {
            Result result = mapping.getResult();
            result.execute(proxy.getInvocation());
        } else {
            // 代理開始執行
            proxy.execute();
        }

        // If there was a previous value stack then set it back onto the request
        // 之前已經有值棧了,就把它放入當前request域裡面
        if (!nullStack) {
            request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);
        }
    } catch (ConfigurationException e) {
        logConfigurationException(request, e);
        sendError(request, response, context, HttpServletResponse.SC_NOT_FOUND, e);
    } catch (Exception e) {
        if (handleException || devMode) {
            sendError(request, response, context, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
        } else {
            throw new ServletException(e);
        }
    } finally {
        UtilTimerStack.pop(timerKey);
    }
}

這一步主要做了三件事:建立Action的代理,封裝結果集Result,設定值棧。

實際上是通過呼叫DefaultActionProxyFactory的createActionProxy方法來建立的Action的代理

public ActionProxy createActionProxy(String namespace, String actionName, String methodName, Map<String, Object> extraContext, boolean executeResult, boolean cleanupContext) {
    // 建立ActionInvocation負責迭代攔截器和執行Action
    ActionInvocation inv = new DefaultActionInvocation(extraContext, true);
    // 放入容器中
    container.inject(inv);
    // 呼叫這個過載的createActionProxy方法
    return createActionProxy(inv, namespace, actionName, methodName, executeResult, cleanupContext);
}

在方法返回的時候呼叫過載的createActionProxy方法,進入原始碼看一下:

public ActionProxy createActionProxy(ActionInvocation inv, String namespace, String actionName, String methodName, boolean executeResult, boolean cleanupContext) {

    DefaultActionProxy proxy = new DefaultActionProxy(inv, namespace, actionName, methodName, executeResult, cleanupContext);
    container.inject(proxy);
    // 這裡,proxy開始準備
    proxy.prepare();
    return proxy;
}

看這裡:proxy.prepare();,proxy開始準備,繼續跟原始碼:

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());
        }

        resolveMethod();

        if (!config.isAllowedMethod(method)) {
            throw new ConfigurationException("Invalid method: " + method + " for action " + actionName);
        }
        // 關鍵:開始初始化invocation
        invocation.init(this);

    } finally {
        UtilTimerStack.pop(profileKey);
    }
}

準備的關鍵程式碼就是初始化invocation,點進去一看又是一個介面,Ctrl+T,選中DefaultActionInvocation這個實現類,檢視它的init方法:

public void init(ActionProxy proxy) {
    this.proxy = proxy;
    Map<String, Object> contextMap = createContextMap();

    // Setting this so that other classes, like object factories, can use the ActionProxy and other
    // contextual information to operate
    ActionContext actionContext = ActionContext.getContext();

    if (actionContext != null) {
        actionContext.setActionInvocation(this);
    }
    // 建立Action(關鍵)
    createAction(contextMap);

    if (pushAction) {
        stack.push(action);
        contextMap.put("action", action);
    }

    invocationContext = new ActionContext(contextMap);
    invocationContext.setName(proxy.getActionName());

    // get a new List so we don't get problems with the iterator if someone changes the list
    // 看到了吧,在這裡獲得了與action相關的攔截器,共19個
    List<InterceptorMapping> interceptorList = new ArrayList<InterceptorMapping>(proxy.getConfig().getInterceptors());
    // 迭代(就是執行所有的攔截器)
    interceptors = interceptorList.iterator();
}

終於找到了建立Action的程式碼了,繼續跟進原始碼:

protected void createAction(Map<String, Object> contextMap) {
    // load action
    String timerKey = "actionCreate: " + proxy.getActionName();
    try {
        UtilTimerStack.push(timerKey);
        // 可以看到在這裡通過工廠類來建立Action
        action = objectFactory.buildAction(proxy.getActionName(), proxy.getNamespace(), proxy.getConfig(), contextMap);
    } catch (InstantiationException e) {
        ...
}

經過這一步,終於通過了DefaultActionInvocation建立了Action。

然後我們回到上一步,成功建立Action之後就獲取與之相關的攔截器列表,並用一個list集合裝起來,依次迭代它們。

// get a new List so we don't get problems with the iterator if someone changes the list
List<InterceptorMapping> interceptorList = new ArrayList<InterceptorMapping>(proxy.getConfig().getInterceptors());
interceptors = interceptorList.iterator();

攔截器執行完畢後再執行Action,再封裝結果集,再出攔截器,給客戶端響應。

2.10 最終Action執行完畢一定要clear掉

防止記憶體洩漏(記憶體洩漏是指分配出去的記憶體不再使用,但是無法回收),當然在clear的時候還是要根據前面提到的計數器來判斷是否清除。

struts1和struts2的區別

  1. struts1的業務控制類必須繼承ActionSupport,struts2可以不用繼承
  2. struts1是單例的,存線上程安全問題,struts2是多例的,不存線上程安全問題
  3. struts1的業務控制類需要依賴servletAPI,struts2不需要
  4. struts1對於頁面請求的引數是通過一個ActionForm表單來收集的,struts2直接通過攔截器注入
  5. struts1的業務流程是固定的(可以參考我的另一篇部落格struts1原理),struts2可以通過攔截器改變這個流程
  6. struts1是通過servlet來匹配所有的請求,struts2是通過filter來匹配所有的請求