Digester源码解析(3讲)
2013-02-19 21:21 阅读(169)

在进行下面的源码解析之前,我们先来讨论个概念----节点工作环境。

看了org.apache.commons.digester.Digester的源码会知道Digester类继承了DefaultHandler类,而DefaultHandler实现了包括ContentHandler接口在内各种处理器接口,因此Digester也是一个ContentHandler处理器,那么当xml文档

被解析时,当解析到某一元素时就会依次调用Digester的startElement()、characters()、endElement()三个方法。由于这三个方法在不同时点运行,所以有些数据就需要存储在一个公共的环境中,以便三个方法都能访问到。这些数据包括:当前

元素的所有规则(rule)、元素的值、当前元素的匹配模式等等,这些就构成了当前节点(元素)的工作环境。

具体总结起来节点工作环境包括三个数据:

(1)当前元素的规则集(rules)  -----------由Digester的matches属性承担

(2)当前元素的文本值  -----------由Digester的bodyText属性承担

(3)当前元素的匹配模式(matching pattern) ---------由Digester的match属性承担


同时考虑到当进入到某一子元素时,其父元素的工作环境并没有退出,这样就需要将父元素的工作环境暂时保存起来,以便当该元素退出时在将父元素的工作环境恢复出来,这样最好的解决办法就是采用堆栈。

当进入到某一元素时(即调用了startElement()方法),将元素的工作环境压入堆栈,访问工作环境时只需要访问堆栈的栈顶元素即可,而当元素退出后(即执行endElement()),在将栈顶的元素出栈,这样既建立了当前元素的工作环境同时也保存了父元素 的工作环境,很好的解决了上面的问题,而Digester就是这样做的。

有了上面的分析后,我们在来看下代码就比较容易理解了:

/**
     * Process notification of the beginning of the document being reached.
     *
     * @exception SAXException if a parsing error is to be reported
     */
    public void startDocument() throws SAXException {

        if (saxLog.isDebugEnabled()) {
            saxLog.debug("startDocument()");
        }

        // ensure that the digester is properly configured, as 
        // the digester could be used as a SAX ContentHandler
        // rather than via the parse() methods.
        configure();
    }
当开始解析xml文档时,会调用startDocument()方法,F5进入下会发现Digester在这个方法中什么也没做,configure()也是空方法。应该是留着做扩展用。


    public void startElement(String namespaceURI, String localName,
                             String qName, Attributes list)
            throws SAXException {
        boolean debug = log.isDebugEnabled();
        
        if (customContentHandler != null) {
            // forward calls instead of handling them here
            customContentHandler.startElement(namespaceURI, localName, qName, list);
            return;
        }

        if (saxLog.isDebugEnabled()) {
            saxLog.debug("startElement(" + namespaceURI + "," + localName + "," +
                    qName + ")");
        }
        
        // Save the body text accumulated for our surrounding element
        bodyTexts.push(bodyText);
        if (debug) {
            log.debug("  Pushing body text '" + bodyText.toString() + "'");
        }
        bodyText = new StringBuffer();

        // the actual element name is either in localName or qName, depending 
        // on whether the parser is namespace aware
        String name = localName;
        if ((name == null) || (name.length() < 1)) {
            name = qName;
        }

        // Compute the current matching rule
        StringBuffer sb = new StringBuffer(match);
        if (match.length() > 0) {
            sb.append('/');
        }
        sb.append(name);
        match = sb.toString();
        if (debug) {
            log.debug("  New match='" + match + "'");
        }

        // Fire "begin" events for all relevant rules
        List rules = getRules().match(namespaceURI, match);
        matches.push(rules);
        if ((rules != null) && (rules.size() > 0)) {
            Substitutor substitutor = getSubstitutor();
            if (substitutor!= null) {
                list = substitutor.substitute(list);
            }
            for (int i = 0; i < rules.size(); i++) {
                try {
                    Rule rule = (Rule) rules.get(i);
                    if (debug) {
                        log.debug("  Fire begin() for " + rule);
                    }
                    rule.begin(namespaceURI, name, list);
                } catch (Exception e) {
                    log.error("Begin event threw exception", e);
                    throw createSAXException(e);
                } catch (Error e) {
                    log.error("Begin event threw error", e);
                    throw e;
                }
            }
        } else {
            if (debug) {
                log.debug("  No rules found matching '" + match + "'.");
            }
        }
    }

当xml解析器解析到某一元素时,第一个被调用的就是startElement()  方法,这个方法将完成俩个工作。一个是建立当前元素的工作环境同时保存父元素的工作环境,另一个工作就是执行规则的begin()方法。

下面我将上面方法的各部分摘出来一点一点分析:

 // Compute the current matching rule
        StringBuffer sb = new StringBuffer(match);
        if (match.length() > 0) {
            sb.append('/');
        }
        sb.append(name);
        match = sb.toString();
        if (debug) {
            log.debug("  New match='" + match + "'");
        }
这段代码是建立工作环境中的匹配模式。其中的局部参数"name"是元素的标签名称。第一句先将匹配模式放入StringBuffer中便于字符连接操作,if (match.length() > 0) { sb.append('/'); }如果不是根元素则拼接"/", sb.append(name);将元素标签

名也拼接上,match = sb.toString();拼接完后再将match从StringBuffer中弄出来,这样当前元素的匹配模式就生成了。注意到这里并没有采用我上面说的堆栈,而是用的字符串,但原理是一样的,上面的匹配模式拼接过程相当于入栈,当退出元素时

在将标签名name及字符"/"从match中截掉,这相当于出栈。

List rules = getRules().match(namespaceURI, match);
matches.push(rules);

这段代码是建立工作环境中的规则集(rules)。根据上面生成的匹配模式match到Digester的规则库中检索出当前元素的所有规则,然后将其压入到堆栈中,这个的具体操作在上一篇中已经介绍过了,这里不过多说了。


bodyTexts.push(bodyText);
if (debug) {
      log.debug("  Pushing body text '" + bodyText.toString() + "'");
}
bodyText = new StringBuffer();

这部分是建立工作环境中的元素文本值。注意这里也没有采用简单的一个堆栈,而是采用了俩个部分来完成。"bodyTexts"是个堆栈,其作用是保存父元素的文本值。bodyText是当前元素的文本值,而此处只是生成了存放文本值的StringBuffer对象,具体的值需要执行到characters()方法时才会生成。


以上的三段代码讲解了当前元素的工作环境是如何生成的,虽然三者没有统一采用堆栈才存放,但具体的实现和堆栈的效果是一样的,请不要拘泥于此。



if ((rules != null) && (rules.size() > 0)) {
            Substitutor substitutor = getSubstitutor();
            if (substitutor!= null) {
                list = substitutor.substitute(list);
            }
            for (int i = 0; i < rules.size(); i++) {
                try {
                    Rule rule = (Rule) rules.get(i);
                    if (debug) {
                        log.debug("  Fire begin() for " + rule);
                    }
                    rule.begin(namespaceURI, name, list);
                } catch (Exception e) {
                    log.error("Begin event threw exception", e);
                    throw createSAXException(e);
                } catch (Error e) {
                    log.error("Begin event threw error", e);
                    throw e;
                }
            }
        } else {
            if (debug) {
                log.debug("  No rules found matching '" + match + "'.");
            }
        }

这段代码是遍历当前元素的规则集,并执行规则的begin()方法,具体的begin方法都做了啥我们在下文在讲解。其中的Substitutor 对象是用于转换属性的,目前的Digester中其什么也没做,可以先不用去理他。这样startElement()方法就说完了。


public void characters(char buffer[], int start, int length)
            throws SAXException {

        if (customContentHandler != null) {
            // forward calls instead of handling them here
            customContentHandler.characters(buffer, start, length);
            return;
        }

        if (saxLog.isDebugEnabled()) {
            saxLog.debug("characters(" + new String(buffer, start, length) + ")");
        }

        bodyText.append(buffer, start, length);

    }
characters() 方法是生成文本值的,同时将文本值放到工作环境bodyText中。这里我要简单的说下这个文本值到底是什么。如果当前的元素是个叶子元素(即没有子元素的元素),那么这个文本值就是元素的值,如<student>aaa</student>,这里的文本


值就是"aaa",如果当前元素不是叶子元素,那么这个文本值就是父元素的开始标签与子元素的开始标签内的任意字符(如果有的话),最常见的就是空白字符(换行符、空格等)。


public void endElement(String namespaceURI, String localName,
                           String qName) throws SAXException {

        if (customContentHandler != null) {
            // forward calls instead of handling them here
            customContentHandler.endElement(namespaceURI, localName, qName);
            return;
        }

        boolean debug = log.isDebugEnabled();

        if (debug) {
            if (saxLog.isDebugEnabled()) {
                saxLog.debug("endElement(" + namespaceURI + "," + localName +
                        "," + qName + ")");
            }
            log.debug("  match='" + match + "'");
            log.debug("  bodyText='" + bodyText + "'");
        }

        // the actual element name is either in localName or qName, depending 
        // on whether the parser is namespace aware
        String name = localName;
        if ((name == null) || (name.length() < 1)) {
            name = qName;
        }

        // Fire "body" events for all relevant rules
        List rules = (List) matches.pop();
        if ((rules != null) && (rules.size() > 0)) {
            String bodyText = this.bodyText.toString();
            Substitutor substitutor = getSubstitutor();
            if (substitutor!= null) {
                bodyText = substitutor.substitute(bodyText);
            }
            for (int i = 0; i < rules.size(); i++) {
                try {
                    Rule rule = (Rule) rules.get(i);
                    if (debug) {
                        log.debug("  Fire body() for " + rule);
                    }
                    rule.body(namespaceURI, name, bodyText);
                } catch (Exception e) {
                    log.error("Body event threw exception", e);
                    throw createSAXException(e);
                } catch (Error e) {
                    log.error("Body event threw error", e);
                    throw e;
                }
            }
        } else {
            if (debug) {
                log.debug("  No rules found matching '" + match + "'.");
            }
        }

        // Recover the body text from the surrounding element
        bodyText = (StringBuffer) bodyTexts.pop();
        if (debug) {
            log.debug("  Popping body text '" + bodyText.toString() + "'");
        }

        // Fire "end" events for all relevant rules in reverse order
        if (rules != null) {
            for (int i = 0; i < rules.size(); i++) {
                int j = (rules.size() - i) - 1;
                try {
                    Rule rule = (Rule) rules.get(j);
                    if (debug) {
                        log.debug("  Fire end() for " + rule);
                    }
                    rule.end(namespaceURI, name);
                } catch (Exception e) {
                    log.error("End event threw exception", e);
                    throw createSAXException(e);
                } catch (Error e) {
                    log.error("End event threw error", e);
                    throw e;
                }
            }
        }

        // Recover the previous match expression
        int slash = match.lastIndexOf('/');
        if (slash >= 0) {
            match = match.substring(0, slash);
        } else {
            match = "";
        }

    }
endElement()方法的主要功能有俩个,一个是依次执行规则的body()、end()俩个方法,另一个是退出当前元素的工作环境。由代码中可以看到,matchesbodyTexts都执行了pop(出栈方法),同时在方法最后将match的标签名和字符"/"截取掉,恢复到


父元素的匹配模式, 这样就完成了退出工作环境的操作。同时遍历rules,调用body()、end()俩个方法。代码虽多,但是按照我上面的分析思路,我觉得还是比较好理解的。

public void endDocument() throws SAXException {

        if (saxLog.isDebugEnabled()) {
            if (getCount() > 1) {
                saxLog.debug("endDocument():  " + getCount() +
                             " elements left");
            } else {
                saxLog.debug("endDocument()");
            }
        }

        // Fire "finish" events for all defined rules
        Iterator rules = getRules().rules().iterator();
        while (rules.hasNext()) {
            Rule rule = (Rule) rules.next();
            try {
                rule.finish();
            } catch (Exception e) {
                log.error("Finish event threw exception", e);
                throw createSAXException(e);
            } catch (Error e) {
                log.error("Finish event threw error", e);
                throw e;
            }
        }

        // Perform final cleanup
        clear();

    }

到了endDocument()方法,整个xml文档的解析已经基本完成。归结起来此方法也主要完成俩个功能,一个是调用所有规则的finish()方法,一个是清除本次解析中各属性和变量中的值,为下次解析做准备。

 Iterator rules = getRules().rules().iterator(); 这个就是上一篇说的,会去取RulesBase 中的存放在属性"rules"中的所有规则,由于其是ArrayList类型,就保证了执行的有序性。


至此,Digester的主流程就说完了,剩下的要解析的就是Digester内置的规则是如何工作的,是怎样相互配合最后完成java bean的生成的,待续。