Struts源码阅读心得之logic:notPresent篇

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的源码无其他目的,只是想好好搞清楚这些标签的作用而已。
 

原文地址:https://www.cnblogs.com/super119/p/1988616.html