1. 程式人生 > >struts2流程及原始碼分析

struts2流程及原始碼分析

struts 架構圖

image.png

分析這個架構圖,我們可以從4個部分,也就struts訪問的4個階段的流程來分析

這4個階段包括:Action對映、Action轉發、Action執行、結果返回

首先是Action對映階段

當請求到來的時候,首先是struts的核心過濾器接收到請求,然後通過ActionMapper進行對映

我們以下圖struts配置為例,檢視一下struts在處理這個請求階段的過程:

image.png

StrutsPrepareAndExecuteFilter原始碼中,它本質是一個過濾器,核心的程式碼在doFilter部分,我們把這部分程式碼copy過來如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        try {
            if (excludedPatterns != null && prepare.isUrlExcluded(request, excludedPatterns)) {
                chain.doFilter(request, response);
            } else {
                prepare.setEncodingAndLocale(request, response);
                prepare.createActionContext(request, response);
                prepare.assignDispatcherToThread();
                request = prepare.wrapRequest(request);
                ActionMapping mapping = prepare.findActionMapping(request, response, true);
                if (mapping == null) {
                    boolean handled = execute.executeStaticResourceRequest(request, response);
                    if (!handled) {
                        chain.doFilter(request, response);
                    }
                } else {
                    execute.executeAction(request, response, mapping);
                }
            }
        } finally {
            prepare.cleanupRequest(request);
        }
    }

判斷是否需要struts處理

當請求過來的時候,首先判斷是否應該由struts處理這個請求,如果不是那就應該放行

image.png

我們這個請求肯定是要經過struts處理的,所以應該走else部分:

建立資料中心ActionContext

接下來需要關注的是這行

prepare.createActionContext(request, response);

這是幹什麼呢?從名字上看,就是在建立一個ActionContext資料中心

還記得嗎?這個就是那個包含原生servlet 11個域物件還有值棧的一個大容器(本質上就是map)

可以進去看下這部分的原始碼:

/**
 * Creates the action context and initializes the thread local
 */
public ActionContext createActionContext(HttpServletRequest request, HttpServletResponse response) {
    ActionContext ctx;
    Integer counter = 1;
    Integer oldCounter = (Integer) request.getAttribute(CLEANUP_RECURSION_COUNTER);
    if (oldCounter != null) {
        counter = oldCounter + 1;
    }
    
    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 {
        ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
        stack.getContext().putAll(dispatcher.createContextMap(request, response, null));
        ctx = new ActionContext(stack.getContext());
    }
    request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);
    ActionContext.setContext(ctx);
    return ctx;
}

在初始化階段,關鍵在這裡:

image.png

這三行程式碼,簡單來看,第一步就是建立值棧,第二步就是往Context中準備一些資料

最後看第三步,可以看到ActionContext這個資料中心實際上就是ValueStack的Context

增強包裝request物件

接下來關注的是91行的這句:

image.png

在還沒執行階段,這個request還是原生的org.apache.catalina.connector.RequestFacade型別

image.png

執行完後就變成了org.apache.struts2.dispatcher.StrutsRequestWrapper型別了,這個就是由struts包裝的request物件

image.png

那struts包裝是做了哪些事情呢,可以勘測一下StrutsRequestWrapper原始碼

其實它就包裝(增強)了一個方法getAttribute

可以看下這部分的程式碼:

/**
 * Gets the object, looking in the value stack if not found
 *
 * @param key The attribute key
 */
public Object getAttribute(String key) {
    if (key == null) {
        throw new NullPointerException("You must specify a key value");
    }

    if (disableRequestAttributeValueStackLookup || key.startsWith("javax.servlet")) {
        // don't bother with the standard javax.servlet attributes, we can short-circuit this
        // see WW-953 and the forums post linked in that issue for more info
        return super.getAttribute(key);
    }

    ActionContext ctx = ActionContext.getContext();
    Object attribute = super.getAttribute(key);

    if (ctx != null && attribute == null) {
        boolean alreadyIn = isTrue((Boolean) ctx.get(REQUEST_WRAPPER_GET_ATTRIBUTE));

        // note: we don't let # come through or else a request for
        // #attr.foo or #request.foo could cause an endless loop
        if (!alreadyIn && !key.contains("#")) {
            try {
                // If not found, then try the ValueStack
                ctx.put(REQUEST_WRAPPER_GET_ATTRIBUTE, Boolean.TRUE);
                ValueStack stack = ctx.getValueStack();
                if (stack != null) {
                    attribute = stack.findValue(key);
                }
            } finally {
                ctx.put(REQUEST_WRAPPER_GET_ATTRIBUTE, Boolean.FALSE);
            }
        }
    }
    return attribute;
}

內容很長,但簡單來說就是註釋上的那麼一句話:

Gets the object, looking in the value stack if not found

翻譯過來那就是:這個方法會先從原生request域中去找,如果找不到的話,就會往值棧中去找

前面說過ValueStack包含兩個部分,一個是ROOT棧部分,一個是context部分(從原始碼上來看,這個部分就是ActionContext)。

那麼拆解開來看,我們可以得到以下結論:

當我們在struts中呼叫原生的request.getAttribute()方法時:struts會幫我們從以下幾個域中依次尋找屬性

  1. 原生request域
  2. ValueStack的棧部分
  3. ValueStack的context部分(ActionContext)

我們看一下原始碼當中這個過程的體現:

image.png

很明顯這一步就是從request域中去找,如果找不到就往下:

image.png

上面的那段註釋告訴我們,使用request來尋找ActionContext內容是不需要和OGNL表示式一樣帶#來訪問的,並且也不允許(否則會陷入死迴圈)

簡單來說,原生的怎麼寫就怎麼寫,不需要管裡邊的具體實現

ActionMapping

最後它要做的操作就是把請求轉換為ActionMapping物件,好方便接下來後期處理,

這個過程簡單來說就是把我們訪問的url地址

http://localhost/strutsLearn/customer/list

解析成

ActionMapping{name='list', namespace='/customer', method='null', extension='null', params=null, result=null}

這種形式的物件

上面的形式是可以解析出來的情況,但是我們知道每一次請求也肯定有很多靜態資原始檔,這些都是struts解析不了的,這時候mapping得到的結果就為null

image.png

到這裡,也就是我們第一階段所要完成的任務了,就是通過ActionMapper工具獲取ActionMapping物件。

那麼再往下,就是把ActionMapping交給下一個階段往下執行了。

execute.executeAction(request, response, mapping);

核心代理轉發階段——建立ActionProxy

這個方法只是一箇中間方法,它再具體呼叫下一個

dispatcher.serviceAction(request, response, mapping);

在這裡,關鍵部分是try catch語句中:

try {
    UtilTimerStack.push(timerKey);
    String namespace = mapping.getNamespace();
    String name = mapping.getName();
    String method = mapping.getMethod();

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

首先是提取ActionMapping中的各種資訊:

image.png

接下來就是建立ActionProxy物件了:

ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
namespace, name, method, extraContext, true, false);

在這裡如果深入去看proxy建立過程,其實就包含了根據actionMapping的資訊去尋找配置檔案struts.xml配置項的過程。這裡就不再展開了。

可以看下ActionProxy大概長啥樣

image.png

那麼創建出來後就需要分派給人去執行了,但是執行分兩種情況

image.png

這兩種其實都一樣,我們這裡是第一次初始訪問,沒有結果返回,因此直接進入的是

proxy.execute();
public String execute() throws Exception {
    ActionContext previous = ActionContext.getContext();
    ActionContext.setContext(invocation.getInvocationContext());
    try {
// This is for the new API:
//            return RequestContextImpl.callInContext(invocation, new Callable<String>() {
//                public String call() throws Exception {
//                    return invocation.invoke();
//                }
//            });

        return invocation.invoke();
    } finally {
        if (cleanupContext)
            ActionContext.setContext(previous);
    }
}

這個方法裡,即將進入的就是 invoke 方法了!

return invocation.invoke();

Action執行階段——攔截器執行

呼叫這個方法,也就意味著進入第三階段——Aciton執行階段了

在這個階段,我們知道它即將進入執行N多個攔截器,我們進入看看它裡面的關鍵程式碼:

if (interceptors.hasNext()) {
    final InterceptorMapping interceptor = interceptors.next();
    String interceptorMsg = "interceptor: " + interceptor.getName();
    UtilTimerStack.push(interceptorMsg);
    try {
        resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
    }
    finally {
        UtilTimerStack.pop(interceptorMsg);
    }
} else {
    resultCode = invokeActionOnly();
}

其中interceptors被定義成攔截器容器的迭代器

protected Iterator<InterceptorMapping> interceptors;

這個攔截器容器就是Proxy階段傳過來的

這其中一行程式碼第一眼看上去確實讓人奇怪:

if (interceptors.hasNext())

通常對迭代器的迴圈應該是while這裡為什麼變成if 呢

先不管,繼續往下看:

首先是獲取攔截器容器中的攔截器和攔截器名稱:

final InterceptorMapping interceptor = interceptors.next();
String interceptorMsg = "interceptor: " + interceptor.getName();
UtilTimerStack.push(interceptorMsg);

這個攔截器的型別是:

image.png

Exception攔截器

和struts-default配置攔截器棧的第一個攔截器是一樣的:

image.png

到這裡,看似真的是要遍歷

接下來斷定肯定去執行這個攔截器裡的內容

resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);

獲取到具體的攔截器,呼叫攔截器的intercept方法,注意到的是這裡把this作為引數穿進去了。

我們看看這裡面做了什麼

結果一進來就做了這麼一件事情:

result = invocation.invoke();

這時候就得出結論了:

invocation的invoke方法呼叫interceptor的intercept方法
接著反過來interceptor的intercept方法又調回invocation的invoke方法

這不就形成了另一種形式的遞迴嗎?

所以接下來又回到invocation的invoke方法執行

那個迭代器的結束條件if

if (interceptors.hasNext())

就沒什麼奇怪了。

但是為什麼要這麼設計遞迴了,普通的遍歷不是挺好的嗎?接著往下看。

執行完一輪後,再看攔截器的名字:

image.png

就是Exception的下一個攔截器alias

這個攔截器的invoke方法進行了比較多的複雜處理,但是不管結果如何,最終都會呼叫:

return invocation.invoke();

@Override public String intercept(ActionInvocation invocation) throws Exception {
    ....... 此處省略n行程式碼
    return invocation.invoke();
}

到這裡,這下明白了這麼設計的用意何在了吧。

每個攔截器的invoke方法執行的方式不一樣,通過這種間接遞迴的方式就能把所有不同功能的器件全部執行一遍了。

看完後,不得不說,這種設計的精妙之處啊

Action執行

攔截器執行完成後接下來就到:

resultCode = invokeActionOnly();

只執行Action了

接下來可以推測,它就是去找到我們自己寫的Action中的類去執行去了。

跳出這個過程,我們可以看到resultCode返回結果:

image.png

那麼可以猜想,接下來就會拿著這個結果根據我們定義的配置檔案處理去,也就是進入我們所說的第四階段了

接下來就會去執行結果處理函式

// now execute the result, if we're supposed to
if (proxy.getExecuteResult()) {
    executeResult();
}

好了,到這裡先告一段落,關於後續的結果處理函式後期再補充吧