Juconcurrent 学而不思则罔,思而不学则殆。

深入理解Tomcat(六)Digester组件


前言

Tomcat中的xml解析,是使用的apache开源组件digester。在tomcat源码中,Digester类所在位置为org.apache.tomcat.util.digester.Digester,它把开源组件digester的源代码拷贝了过来。

本文,我们主要是想了解一下digester的用法。以便在阅读tomcat源码的时候,看到xml解析相关代码的时候不会一脸茫然和懵逼。

digester有两种使用方式:

  1. 一种为tomat内嵌的org.apache.tomcat.util.digester.Digester
  2. 另一种为digester maven依赖

本文采用第二种–maven依赖的方式。

digester实现原理

digester最初是作为struct的一个工具模块,来完成xml解析的功能。但是很快地,有人发现并觉得digester不应该仅仅局限在struct,而应该变得更通用。于是经过apache的孵化,最终加入到了apache commons类库家族中,并形成了一个xml另类解析的工具类库。

digester底层是基于SAX+事件驱动+的方式来搭建实现的。那么在digester中,这三种元素分别起到什么作用呢?

  1. SAX,用于解析xml
  2. 事件驱动,在SAX解析的过程中加入事件来支持我们的对象映射
  3. 栈,当解析xml元素的开始和结束的时候,需要通过xml元素映射的类对象的入栈和出栈来完成事件的调用

通过一些实实在在的场景和例子,我们发现一个元素的作用无非是在其解析前后加入一些扩展逻辑!例如:

  1. 开始解析某个节点的时候,是否需要创建一个类
  2. 开始解析某个节点的时候,是否需要入栈操作
  3. 结束解析某个节点的时候,是否需要执行某个方法
  4. 结束解析某个节点的时候,是否需要出栈操作

如何引入依赖包

以maven为例,使用下面的dependency。

<dependency>
    <groupId>commons-digester</groupId>
    <artifactId>commons-digester</artifactId>
    <version>2.1</version>
</dependency>

如何使用

假如我们需要解析的xml为下面的格式。

<?xml version='1.0' encoding='utf-8'?>
<School name="Jen">
    <Grade name="1">
        <Class name="1" number="31"/>
        <Class name="2" number="32"/>
    </Grade>
    <Grade name="2">
        <Class name="1" number="41"/>
        <Class name="2" number="42"/>
        <Class name="3" number="37"/>
    </Grade>
</School>

同时,我们假设下面的约定成立:

  1. 一个学校有名字属性,下面有多个年级
  2. 每个年级有名字属性,下面有多个班
  3. 每个班有名字和学生人数两个属性

根据上面的规则,我们需要创建关联的3个类,SchoolGradeClass。 School有一个方法addGrade用于往学校对象中添加年级。

package com.juconcurrent.learn.apache.digester;

public class School {
    private String name;
    private Grade grades[] = new Grade[0];
    private final Object servicesLock = new Object();

    public void addGrade(Grade g) {
        synchronized (servicesLock) {
            Grade results[] = new Grade[grades.length + 1];
            System.arraycopy(grades, 0, results, 0, grades.length);
            results[grades.length] = g;
            grades = results;
        }
    }

    public Grade[] getGrades() {
        return grades;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

同样的,年级有一个addClass方法,用于往Grade对象添加Class班对象。

package com.juconcurrent.learn.apache.digester;

public class Grade {
    private String name;
    private Class classes[] = new Class[0];
    private final Object servicesLock = new Object();

    public void addClass(Class c) {
        synchronized (servicesLock) {
            Class results[] = new Class[classes.length + 1];
            System.arraycopy(classes, 0, results, 0, classes.length);
            results[classes.length] = c;
            classes = results;
        }
    }

    public Class[] getClasses() {
        return classes;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Class就比较简单了,只是一个简单的POJO对象。

package com.juconcurrent.learn.apache.digester;

public class Class {
    private String name;
    private int number;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }
}

好了,我们已经定义好了我们所需要创建对象的类。那么如何使用digester来创建我们所需的数据呢?我们先给出例子,然后再来详细分析其中的关键方法。

package com.juconcurrent.learn.apache.digester;

import org.apache.commons.digester.Digester;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class DigesterTest {
    // 属性和get/set方法,假设我们解析出来的School对象放在这儿
    private School school;
    public School getSchool() {
        return school;
    }
    public void setSchool(School s) {
        this.school = s;
    }

    private void digester() throws IOException, SAXException {
        // 读取根据文件的路径,创建InputSource对象,digester解析的时候需要用到
        File file = new File("/Users/pro/ws/learn/learn-javaagent/src/main/resources/School.xml");
        InputStream inputStream = new FileInputStream(file);
        InputSource inputSource = new InputSource(file.toURI().toURL().toString());
        inputSource.setByteStream(inputStream);

        // 创建Digester对象
        Digester digester = new Digester();
        // 是否需要用DTD验证XML文档的合法性
        digester.setValidating(true);
        // 将当前对象放到对象堆的最顶层,这也是这个类为什么要有school属性的原因!
        digester.push(this);

        /*
         * 下面开始为Digester创建匹配规则
         * Digester中的School、School/Grade、School/Grade/Class,分别对应School.xml的School、Grade、Class节点
         */

        // 为School创建规则

        /*
         * Digester.addObjectCreate(String pattern, String className, String attributeName)
         * pattern, 匹配的节点
         * className, 该节点对应的默认实体类
         * attributeName, 如果该节点有className属性, 用className的值替换默认实体类
         *
         * Digester匹配到School节点
         *
         * 1. 如果School节点没有className属性,将创建com.juconcurrent.learn.apache.digester.School对象;
         * 2. 如果School节点有className属性,将创建指定的(className属性的值)对象
         */
        digester.addObjectCreate("School", School.class.getName(), "className");
        // 将指定节点的属性映射到对象,即将School节点的name的属性映射到School.java
        digester.addSetProperties("School");

        /*
         * Digester.addSetNext(String pattern, String methodName, String paramType)
         * pattern, 匹配的节点
         * methodName, 调用父节点的方法
         * paramType, 父节点的方法接收的参数类型
         * Digester匹配到School节点,将调用DigesterTest(School的父节点)的setSchool方法,参数为School对象
         */
        digester.addSetNext("School", "setSchool", School.class.getName());

        // 为School/Grade创建规则
        digester.addObjectCreate("School/Grade", Grade.class.getName(), "className");
        digester.addSetProperties("School/Grade");

        // Grade的父节点为School
        digester.addSetNext("School/Grade", "addGrade", Grade.class.getName());

        // 为School/Grade/Class创建规则
        digester.addObjectCreate("School/Grade/Class", Class.class.getName(), "className");
        digester.addSetProperties("School/Grade/Class");
        digester.addSetNext("School/Grade/Class", "addClass", Class.class.getName());
        // 解析输入源
        digester.parse(inputSource);
    }

    // 只是将School对象进行控制台输出
    private void print(School s) {
        if (s != null) {
            System.out.println(s.getName() + "有" + s.getGrades().length + "个年级");
            for (int i = 0; i < s.getGrades().length; i++) {
                if (s.getGrades()[i] != null) {
                    Grade g = s.getGrades()[i];
                    System.out.println(g.getName() + "年级 有 " + g.getClasses().length + "个班:");
                    for (int j = 0; j < g.getClasses().length; j++) {
                        if (g.getClasses()[j] != null) {
                            Class c = g.getClasses()[j];
                            System.out.println(c.getName() + "班有" + c.getNumber() + "人");
                        }
                    }
                }
            }
        }
    }

    // 入口main()方法
    public static void main(String[] args) throws IOException, SAXException {
        DigesterTest digesterTest = new DigesterTest();
        digesterTest.digester();
        digesterTest.print(digesterTest.school);
    }
}

这儿我们需要着重说明一下digester里面的几个方法,大体上我们可以将其方法分为两类:操作类和规则类。

  1. 操作类
    • public void setValidating(boolean validating) // 是否根据DTD校验XML
    • public void push(Object object) // 将对象压入栈
    • public Object peek() // 获取栈顶对象
    • public Object pop() // 弹出栈顶对象
    • public Object parse(InputSource input) // 解析输入源
  2. 规则类
    • public void addObjectCreate(String pattern, String className, String attributeName) // 增加对象创建规则,当匹配到pattern模式时,如果指定了attributeName,则根据attributeName创建类对象;否则根据className创建类对象
    • public void addSetProperties(String pattern) // 增加属性设置规则,当匹配到pattern模式时,就填充其属性
    • public void addSetNext(String pattern, String methodName, String paramType) // 增加设置下一个规则,当匹配到pattern模式时,调用父节点的methodName方法,paramType为方法传入参数的类型
    • public void addRule(String pattern, Rule rule) // 当匹配到pattern模式时,增加一个自定义规则
    • public void addRuleSet(RuleSet ruleSet) // 增加规则集,一个规则集指的是对一个节点及下面的所有后续节点(子节点、子节点的子节点...)的解析

Tomcat中的规则解析例子

上面我们写了一个非常简单的例子,相信通过这样的例子我们可以很快地入门了。那么tomcat里面又是怎样写的呢?我们看看org.apache.catalina.startup.Catalina.createStartDigester这个方法,这个方法用于定义对server.xml的解析。该方法比较长,但是我并不打算对这个方法进行阉割和压缩,而是原封不动地拷贝到这儿,以便大家对此有一个比较完整的认识。

protected Digester createStartDigester() {
    long t1=System.currentTimeMillis();
    // Initialize the digester
    Digester digester = new Digester();
    digester.setValidating(false);
    digester.setRulesValidation(true);

    // 这儿设置无效的属性,fake是赝品的意思,也就是在检查到这些属性直接认为是无效的
    Map<Class<?>, List<String>> fakeAttributes = new HashMap<>();
    List<String> objectAttrs = new ArrayList<>();
    objectAttrs.add("className");
    fakeAttributes.put(Object.class, objectAttrs);
    // Ignore attribute added by Eclipse for its internal tracking
    List<String> contextAttrs = new ArrayList<>();
    contextAttrs.add("source");
    fakeAttributes.put(StandardContext.class, contextAttrs);
    digester.setFakeAttributes(fakeAttributes);

    // 设置是否使用线程上下文类加载器
    digester.setUseContextClassLoader(true);

    // Configure the actions we will be using
    digester.addObjectCreate("Server",
                             "org.apache.catalina.core.StandardServer",
                             "className");
    digester.addSetProperties("Server");
    digester.addSetNext("Server",
                        "setServer",
                        "org.apache.catalina.Server");

    digester.addObjectCreate("Server/GlobalNamingResources",
                             "org.apache.catalina.deploy.NamingResourcesImpl");
    digester.addSetProperties("Server/GlobalNamingResources");
    digester.addSetNext("Server/GlobalNamingResources",
                        "setGlobalNamingResources",
                        "org.apache.catalina.deploy.NamingResourcesImpl");

    digester.addObjectCreate("Server/Listener",
                             null, // MUST be specified in the element
                             "className");
    digester.addSetProperties("Server/Listener");
    digester.addSetNext("Server/Listener",
                        "addLifecycleListener",
                        "org.apache.catalina.LifecycleListener");

    digester.addObjectCreate("Server/Service",
                             "org.apache.catalina.core.StandardService",
                             "className");
    digester.addSetProperties("Server/Service");
    digester.addSetNext("Server/Service",
                        "addService",
                        "org.apache.catalina.Service");

    digester.addObjectCreate("Server/Service/Listener",
                             null, // MUST be specified in the element
                             "className");
    digester.addSetProperties("Server/Service/Listener");
    digester.addSetNext("Server/Service/Listener",
                        "addLifecycleListener",
                        "org.apache.catalina.LifecycleListener");

    //Executor
    digester.addObjectCreate("Server/Service/Executor",
                     "org.apache.catalina.core.StandardThreadExecutor",
                     "className");
    digester.addSetProperties("Server/Service/Executor");

    digester.addSetNext("Server/Service/Executor",
                        "addExecutor",
                        "org.apache.catalina.Executor");


    digester.addRule("Server/Service/Connector",
                     new ConnectorCreateRule());
    digester.addRule("Server/Service/Connector",
                     new SetAllPropertiesRule(new String[]{"executor", "sslImplementationName"}));
    digester.addSetNext("Server/Service/Connector",
                        "addConnector",
                        "org.apache.catalina.connector.Connector");

    digester.addObjectCreate("Server/Service/Connector/SSLHostConfig",
                             "org.apache.tomcat.util.net.SSLHostConfig");
    digester.addSetProperties("Server/Service/Connector/SSLHostConfig");
    digester.addSetNext("Server/Service/Connector/SSLHostConfig",
            "addSslHostConfig",
            "org.apache.tomcat.util.net.SSLHostConfig");

    digester.addRule("Server/Service/Connector/SSLHostConfig/Certificate",
                     new CertificateCreateRule());
    digester.addRule("Server/Service/Connector/SSLHostConfig/Certificate",
                     new SetAllPropertiesRule(new String[]{"type"}));
    digester.addSetNext("Server/Service/Connector/SSLHostConfig/Certificate",
                        "addCertificate",
                        "org.apache.tomcat.util.net.SSLHostConfigCertificate");

    digester.addObjectCreate("Server/Service/Connector/SSLHostConfig/OpenSSLConf",
                             "org.apache.tomcat.util.net.openssl.OpenSSLConf");
    digester.addSetProperties("Server/Service/Connector/SSLHostConfig/OpenSSLConf");
    digester.addSetNext("Server/Service/Connector/SSLHostConfig/OpenSSLConf",
                        "setOpenSslConf",
                        "org.apache.tomcat.util.net.openssl.OpenSSLConf");

    digester.addObjectCreate("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd",
                             "org.apache.tomcat.util.net.openssl.OpenSSLConfCmd");
    digester.addSetProperties("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd");
    digester.addSetNext("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd",
                        "addCmd",
                        "org.apache.tomcat.util.net.openssl.OpenSSLConfCmd");

    digester.addObjectCreate("Server/Service/Connector/Listener",
                             null, // MUST be specified in the element
                             "className");
    digester.addSetProperties("Server/Service/Connector/Listener");
    digester.addSetNext("Server/Service/Connector/Listener",
                        "addLifecycleListener",
                        "org.apache.catalina.LifecycleListener");

    digester.addObjectCreate("Server/Service/Connector/UpgradeProtocol",
                              null, // MUST be specified in the element
                              "className");
    digester.addSetProperties("Server/Service/Connector/UpgradeProtocol");
    digester.addSetNext("Server/Service/Connector/UpgradeProtocol",
                        "addUpgradeProtocol",
                        "org.apache.coyote.UpgradeProtocol");

    // Add RuleSets for nested elements
    digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));
    digester.addRuleSet(new EngineRuleSet("Server/Service/"));
    digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
    digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));
    addClusterRuleSet(digester, "Server/Service/Engine/Host/Cluster/");
    digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/"));

    // When the 'engine' is found, set the parentClassLoader.
    digester.addRule("Server/Service/Engine",
                     new SetParentClassLoaderRule(parentClassLoader));
    addClusterRuleSet(digester, "Server/Service/Engine/Cluster/");

    // 根据t1和t2,算出整个server.xml的Digester创建花费的时间
    long t2=System.currentTimeMillis();
    if (log.isDebugEnabled()) {
        log.debug("Digester for server.xml created " + ( t2-t1 ));
    }
    return (digester);
}

这儿我们看到,在tomcat中明显地用到了前面例子中说明的几个规则,我们再简单罗列一下:

  1. addObjectCreate,对象创建规则
  2. addSetProperties,属性设置规则
  3. addSetNext,设置下一个规则
  4. addRule,自定义规则
  5. digester.addRuleSet,自定义规则集

总结

本文我们对digester做了一个使用说明。

我们首先简单地说明了一下digester是什么,内部基于什么原理来实现的。然后通过一个School、Grade和Class这样的生活中的例子来说明digester的用法。最后通过查看tomcat中关于digester的例子代码,加深了我们对于digester的理解。

相信通过这篇文章,让我们在阅读tomcat源码的过程中不再对xml解析产生疑惑!

参考链接

  1. https://blog.csdn.net/qq_24451605/article/details/51289519
  2. https://blog.csdn.net/flyliuweisky547/article/details/23872231
  3. https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/tomcat/util/digester/package-summary.html

Content