Struts源碼閱讀心得之logic:notPresent篇

來源:互聯網
上載者:User
Struts是Apache Group的一個優秀的MVC的前台架構,目前也使用的非常廣泛。
但僅僅停留在使用的層次上是沒有挑戰性的,研究其源碼才是真正樂趣所在。
本文以一個很小的例子入手,將這個例子中牽涉到的Struts源碼做了一次剖析,
豁然發現就在這個不起眼的例子中,也有許多值得學習的東西,故將其整理成文,目的旨在拋磚引玉,希望有興趣者同樂。

本文的例子就來自Struts內建的一個Example(struts-example.war)。在這個例子中的第一個JSP(index.jsp),一開始就有這樣的一段代碼:
==============================================

Code: Select all
<logic:notPresent name="database" scope="application">
  <font color="red">
    ERROR:  User database not loaded -- check servlet container logs
    for error messages.
  </font>
  <hr>
</logic:notPresent>

==============================================

從文檔上的描述來看,這段代碼主要有如下的作用:
1、由於本sample的資料庫是一個database.xml檔案,所以在JSP的一開頭就用這段代碼來驗證database.xml檔案是否已被裝載成功
2、如果裝載不成功,那麼將列印中間那一段出錯資訊
OK,由以上的描述,很自然的會想到,這個標籤大概做的事情可能就是在/WEB-INF/目錄下尋找database.xml,成功就返回成功,錯誤就
返回失敗,如此如此,順理成章
可是Struts的原始碼是否就是按照這樣的邏輯寫的呢?答案顯然是否
下面就開始層層剖析Struts的源碼

首先當然是從這個標籤類入手,開啟org.apache.struts.taglib.logic.NotPresentTag類,發現如下代碼:
==============================================

Code: Select all
public class NotPresentTag extends PresentTag {

    // ------------------------------------------------------ Protected Methods

    /**
     * Evaluate the condition that is being tested by this particular tag,
     * and return <code>true</code> if the nested body content of this tag
     * should be evaluated, or <code>false</code> if it should be skipped.
     * This method must be implemented by concrete subclasses.
     *
     * @exception JspException if a JSP exception occurs
     */
    protected boolean condition() throws JspException {

        return (condition(false));

    }
}

==============================================
看來封裝的不錯,初步估計應該是這樣:
1、NotPresentTag繼承自PresentTag類,但Tag標籤類必須有doStartTag這樣的方法啊,那自然就是在PresentTag類裡面有了
2、有一個condition方法,看來是PresentTag類的doStartTag方法調用了condition方法,正好這裡利用物件導向的“多態”,調用到了這個方法,而不是
PresentTag中的condition方法

繼續,開啟PresentTag類,看到如下代碼:
==============================================

Code: Select all
public class PresentTag extends ConditionalTagBase {

//。。。中間有省略

/**
     * Evaluate the condition that is being tested by this particular tag,
     * and return <code>true</code> if the nested body content of this tag
     * should be evaluated, or <code>false</code> if it should be skipped.
     * This method must be implemented by concrete subclasses.
     *
     * @exception JspException if a JSP exception occurs
     */
    protected boolean condition() throws JspException {

        return (condition(true));

    }

    /**
     * Evaluate the condition that is being tested by this particular tag,
     * and return <code>true</code> if the nested body content of this tag
     * should be evaluated, or <code>false</code> if it should be skipped.
     * This method must be implemented by concrete subclasses.
     *
     * @param desired Desired outcome for a true result
     *
     * @exception JspException if a JSP exception occurs
     */
    protected boolean condition(boolean desired) throws JspException {
        // Evaluate the presence of the specified value
        boolean present = false;
        HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
       
        if (cookie != null) {
            present = this.isCookiePresent(request);
           
        } else if (header != null) {
            String value = request.getHeader(header);
            present = (value != null);
           
        } else if (name != null) {
            present = this.isBeanPresent();
           
        } else if (parameter != null) {
            String value = request.getParameter(parameter);
            present = (value != null);
           
        } else if (role != null) {
            StringTokenizer st = new StringTokenizer(role, ROLE_DELIMITER, false);
            while (!present && st.hasMoreTokens()) {
                present = request.isUserInRole(st.nextToken());
            }
           
        } else if (user != null) {
            Principal principal = request.getUserPrincipal();
            present = (principal != null) && user.equals(principal.getName());
           
        } else {
            JspException e = new JspException
                (messages.getMessage("logic.selector"));
            TagUtils.getInstance().saveException(pageContext, e);
            throw e;
        }

        return (present == desired);

    }

    /**
     * Returns true if the bean given in the <code>name</code> attribute is found.
     * @since Struts 1.2
     */
    protected boolean isBeanPresent() {
        Object value = null;
        try {
            if (this.property != null) {
                value = TagUtils.getInstance().lookup(pageContext, name, this.property, scope);
            } else {
                value = TagUtils.getInstance().lookup(pageContext, name, scope);
            }
        } catch (JspException e) {
            value = null;
        }
       
        return (value != null);
    }

    /**
     * Returns true if the cookie is present in the request.
     * @since Struts 1.2
     */
    protected boolean isCookiePresent(HttpServletRequest request) {
        Cookie cookies[] = request.getCookies();
        if (cookies == null) {
            return false;
        }
       
        for (int i = 0; i < cookies.length; i++) {
            if (this.cookie.equals(cookies.getName())) {
                return true;
            }
        }

        return false;
    }

==============================================
通過這段代碼,又看到了如下的東西:
1、PresentTag又繼承自ConditionalTagBase,抽象的比較精彩
2、代碼中的一大堆變數如name, cookie, role等等,這些正好是logic:notPresent元素的屬性
3、這些變數沒有在PresengTag中定義,看來是定義在了ConditionalTagBase裡面了
4、留心看name這個屬性的處理,因為我們研究的那段代碼(第一段代碼),屬性中最關鍵的也就是那個name="database"了
看到她調用了isBeanPresent這個方法,而這個方法又調用了TagUtils.getInstance().lookup這個方法

OK,到此,可以看到,研究方向轉移到了TagUtils.getInstance().lookup這個方法,在開始看這個方法之前,還是把ConditionalTagBase
這個類也看一看,說不定還有意外的收穫,如下:
==============================================
public abstract class ConditionalTagBase extends TagSupport {

//。。。中間省略了那些屬性(name, role, scope, user等等元素屬性)的get/set方法
//這些get/set方法將被自動調用,所以第一段代碼中的name="database"中,database這個值將被自動賦給name變數

Code: Select all
/**
     * Perform the test required for this particular tag, and either evaluate
     * or skip the body of this tag.
     *
     * @exception JspException if a JSP exception occurs
     */
    public int doStartTag() throws JspException {

        if (condition())
            return (EVAL_BODY_INCLUDE);
        else
            return (SKIP_BODY);

    }

    /**
     * Evaluate the remainder of the current page normally.
     *
     * @exception JspException if a JSP exception occurs
     */
    public int doEndTag() throws JspException {

        return (EVAL_PAGE);

    }

    /**
     * Release all allocated resources.
     */
    public void release() {

        super.release();
        cookie = null;
        header = null;
        name = null;
        parameter = null;
        property = null;
        role = null;
        scope = null;
        user = null;

    }

    // ------------------------------------------------------ Protected Methods

    /**
     * Evaluate the condition that is being tested by this particular tag,
     * and return <code>true</code> if the nested body content of this tag
     * should be evaluated, or <code>false</code> if it should be skipped.
     * This method must be implemented by concrete subclasses.
     *
     * @exception JspException if a JSP exception occurs
     */
    protected abstract boolean condition() throws JspException;

==============================================
由以上的代碼,雖然沒有意外收穫,但是確認了不少之前的想法,如下:
1、condition這個方法是一個抽象方法,具體實現在PresentTag和NotPresentTag類中
2、doStartTag方法中調用了condition方法,導致了PresentTag和NotPresentTag類中condition方法的被調用

猜測已被確認,接下來繼續看TagUtils.getInstance().lookup這個方法,找到原始碼如下:
==============================================

Code: Select all
/**
     * Locate and return the specified bean, from an optionally specified
     * scope, in the specified page context.  If no such bean is found,
     * return <code>null</code> instead.  If an exception is thrown, it will
     * have already been saved via a call to <code>saveException()</code>.
     *
     * @param pageContext Page context to be searched
     * @param name Name of the bean to be retrieved
     * @param scopeName Scope to be searched (page, request, session, application)
     *  or <code>null</code> to use <code>findAttribute()</code> instead
     * @return JavaBean in the specified page context
     * @exception JspException if an invalid scope name
     *  is requested
     */
    public Object lookup(PageContext pageContext, String name, String scopeName)
            throws JspException {

        if (scopeName == null) {
            return pageContext.findAttribute(name);
        }

        try {
            return pageContext.getAttribute(name, instance.getScope(scopeName));

        } catch (JspException e) {
            saveException(pageContext, e);
            throw e;
        }

    }

    /**
     * Locate and return the specified property of the specified bean, from
     * an optionally specified scope, in the specified page context.  If an
     * exception is thrown, it will have already been saved via a call to
     * <code>saveException()</code>.
     *
     * @param pageContext Page context to be searched
     * @param name Name of the bean to be retrieved
     * @param property Name of the property to be retrieved, or
     *  <code>null</code> to retrieve the bean itself
     * @param scope Scope to be searched (page, request, session, application)
     *  or <code>null</code> to use <code>findAttribute()</code> instead
     * @return property of specified JavaBean
     *
     * @exception JspException if an invalid scope name
     *  is requested
     * @exception JspException if the specified bean is not found
     * @exception JspException if accessing this property causes an
     *  IllegalAccessException, IllegalArgumentException,
     *  InvocationTargetException, or NoSuchMethodException
     */
    public Object lookup(
            PageContext pageContext,
            String name,
            String property,
            String scope)
            throws JspException {

        // Look up the requested bean, and return if requested
        Object bean = lookup(pageContext, name, scope);
        if (bean == null) {
            JspException e = null;
            if (scope == null) {
                e = new JspException(messages.getMessage("lookup.bean.any", name));
            } else {
                e =
                        new JspException(
                                messages.getMessage("lookup.bean", name, scope));
            }
            saveException(pageContext, e);
            throw e;
        }

        if (property == null) {
            return bean;
        }

        // Locate and return the specified property
        try {
            return PropertyUtils.getProperty(bean, property);

        } catch (IllegalAccessException e) {
            saveException(pageContext, e);
            throw new JspException(
                    messages.getMessage("lookup.access", property, name));

        } catch (InvocationTargetException e) {
            Throwable t = e.getTargetException();
            if (t == null) {
                t = e;
            }
            saveException(pageContext, t);
            throw new JspException(
                    messages.getMessage("lookup.target", property, name));

        } catch (NoSuchMethodException e) {
            saveException(pageContext, e);
            throw new JspException(
                    messages.getMessage("lookup.method", property, name));
        }

    }

==============================================

由上,可以看到:
1、TagUtils是一個Singleton的類
2、代碼Object bean = lookup(pageContext, name, scope);的用意顯然是在尋找name變數綁定的對象
3、2步驟尋找的過程是首先查看scope變數是否為空白,如果為空白,將調用pageContext.findAttribute方法
4、findAttribute方法將依次在page, request, session, application四個範圍內尋找name變數綁定的對象(來自Servlet API文檔的解釋)
5、如果scope變數不為空白(在我們的例子中,scope="Application"),那麼直接調用pageContext.getAttribute方法,在scope裡面尋找
name變數綁定的對象
6、如果這個對象找到了,並且property屬性沒有被賦值(在我們的例子中確實沒有賦值),那麼直接將對象返回
7、如果對象不為空白的返回到了PresentTag的condition(boolean desire)方法,那麼最終結果就是存在,notPresent這個標籤裡面的出錯資訊
就不會被顯示

通過以上的分析,可以看到,現在的關鍵問題已經轉移到看Application範圍內是否有一個對象綁定一個key叫做"database"(看步驟5的描述)
但是這個事情Struts的這個Example程式又是在什麼時候做的呢?
通過觀察和分析,發現在struts-config配置中,將這項工作作為了一個PlugIn進行了配置,如下:
==============================================

Code: Select all
<plug-in className="org.apache.struts.webapp.example.memory.MemoryDatabasePlugIn">
    <set-property property="pathname" value="/WEB-INF/database.xml"/>
  </plug-in>

==============================================
事到如今,終於離目標越來越近了,MemoryDatabasePlugIn這個類在Tomcat啟動的時候啟動,讀取了database.xml檔案,
將其中的值讀取出來並構建出了一個對象,然後將這個對象綁定到database這個key,放在了Application範圍內的一個容器中
這樣的解釋,終於可以和前面研究的結果相銜接起來了,不是嗎?:)
迫不及待的開啟MemoryDatabasePlugIn類,看到了如下代碼:
==============================================

Code: Select all
    /**
     * Initialize and load our initial database from persistent storage.
     *
     * @param servlet The ActionServlet for this web application
     * @param config The ApplicationConfig for our owning module
     *
     * @exception ServletException if we cannot configure ourselves correctly
     */
    public void init(ActionServlet servlet, ModuleConfig config)
        throws ServletException {

        log.info("Initializing memory database plug in from '" +
                 pathname + "'");

        // Remember our associated configuration and servlet
        this.config = config;
        this.servlet = servlet;

        // Construct a new database and make it available
        database = new MemoryUserDatabase();
        try {
            String path = calculatePath();
            if (log.isDebugEnabled()) {
                log.debug(" Loading database from '" + path + "'");
            }
            database.setPathname(path);
            database.open();
        } catch (Exception e) {
            log.error("Opening memory database", e);
            throw new ServletException("Cannot load database from '" +
                                       pathname + "'", e);
        }

        // Make the initialized database available
        servlet.getServletContext().setAttribute(Constants.DATABASE_KEY,
                                                 database);

        // Setup and cache other required data
        setupCache(servlet, config);

    }

==============================================
果然不出所料,這段代碼做了如下的事情:
1、產生了MemoryUserDatabase這個對象,並調用其setPathname和open方法,讀取database.xml的值並填充到成員變數中
2、將這個對象綁定到了ServletContext中,Constants.DATABASE_KEY這個常量就是"database"
3、ServletContext是Servlet的一個上下文環境,也是ServletConfig的一個提供者。ServletConfig中定義了Servlet和Servlet容器
溝通時的一些資訊(如Servlet的方法定義,參數等等)。這個上下文在一個JVM、一個Web應用中一般只會存在一份,所以顯然是
Application範圍內的東西

至此,終於搞清楚了Struts中這個logic:notPresent標籤的整個運行過程,和我們的想像不完全相同的是,Struts是將database
的配置在啟動的時候就讀取出來然後綁定到一個對象;然後這個標籤中的代碼去嘗試尋找這個對象,如果找到,表明database裝載
成功,否則就出錯。

另外,有一個收穫是,標籤的書寫中,scope="Application"可以不寫,因為如果不寫scope屬性值,Struts會依次在page, request, session,
application四個範圍內進行尋找,一樣可以找到這個對象,但有些地方還是有點不妥的,下面將提到。
依次尋找這個動作是通過pageContext類的一個抽象方法findAttribute方法來完成的,由於是抽象方法,所以這個方法的具體實現應該在Servlet
容器的原始碼中,也就是說,這個方法的實現是和容器相關的。另外,依次尋找這個動作肯定沒有指定scope尋找來得快,所以效能上也會有影響
我想這就是Struts的這個example中將scope手動指定的緣故吧。
OK,讓我們刨根問底,再來看findAttribute方法的實現,這裡摘錄的是Tomcat4.1的原始碼,這個方法的實現在Tomcat的
org.apache.jasper.runtime.PageContextImpl這個類中,代碼如下:
==============================================

Code: Select all
public Object findAttribute(String name) {
        Object o = attributes.get(name);
        if (o != null)
            return o;

        o = request.getAttribute(name);
        if (o != null)
            return o;

        if (session != null) {
            o = session.getAttribute(name);
            if (o != null)
                return o;
        }

//這裡的context就是ServletContext的一個執行個體
        return context.getAttribute(name);
    }

==============================================
不用再解釋了,再清楚不過了,的確是一個一個的找了過來

至此,我們已經從logic:notPresent這個標籤開始,做了一次全程曆險,而且最後還到Tomcat家裡去小坐了一下
總結來說,logic:notPresent這個標籤是在指定scope中尋找name變數綁定的一個對象,可以用來定位一些資源是否被裝載
但這個標籤沒有強大到自動去找指定的檔案或其他資源的地步,需要我們在這個標籤被調用之前,將資源裝載並綁定到
page, request, session或Application中

Struts是一個優秀的開源項目,除了一整套Web運行機制外,她還帶了一個大標籤庫。這個標籤庫中有很多比較實用的標籤,
但如果理解不透,很容易發生錯用的現象。所以研讀Struts的源碼無其他目的,只是想好好搞清楚這些標籤的作用而已。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.