tomcat xml解析-digester

什么是digester?解决了什么问题?

在tomcat里,digester就是一个解析xml的工具,用来解析server.xml来创建应用服务器
digester本来仅仅是Jakarta Struts中的一个工具,用于处理struts-config.xml配置文件。由于将XML文件转换成相应的Java对象是一项很通用的功能,后来又被移到了Apache Common项目中。但在Tomcat中并不依赖Apache common,而是将Digester源码包含了进来,并改变了包路径。
digester底层以来SAX(Simple API for XML),采用事件驱动的方式将xml配置转换为java对象,是对SAX的高层次封装。

SAX是什么?

SAX是一个事件驱动的xml解析框架,与之类似的xml解析框架还有DOM、JDOM、DOM4J等。SAX解析器不像DOM那样建立一个完整的文档树,而是在读取文档时激活一系列事件,这些事件被推给事件处理器,然后由事件处理器提供对文档内容的访问。
常见的事件处理器有三种基本类型:

  • 用于访问XML DTD内容的DTDHandler;
  • 用于低级访问解析错误的ErrorHandler;
  • 用于访问文档内容的ContentHandler,这也是最普遍使用的事件处理器,一般也是最关注的

例如由如下xml,xml的一个element可以可以包含三种内容

  • attribute。department包含name和code属性,user也包含name和code属性
  • 子element。department包含user、extension子element,extension包含property-name、property-value两个子element
  • body。property-name和property-value具有body,分别是director、joke
<department name="deptname001" code="deptcode001">
  <user name="username001" code ="usercode001"/>
  <user name="username002" code ="usercode002"/>
  <extension>
    <property-name>director</property-name>
    <property-value>joke</property-value>
  </extension>
</department>

ContentHandler是一个接口,解析xml遇到各种事件时,会调用ContentHandler的相应回调接口,其中以下五个方法比较关注

  • startDocument 。开始解析xml的回调,在这里可以处理一些资源准备的工作
  • endDocument。xml解析结束的回调,在这里可以处理资源回收工作
  • startElement。读取到element开始时的回调,atts是该element内携带的属性
  • endElement。element读取结束的回调
  • characters。读取到body的回调

下面实现一个ContentHandler(删除了空实现的方法),来处理上面的xml

public class LoggerContentHandler implements ContentHandler {

    public static void main(String[] args) throws Exception {
        SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
        parser.getXMLReader().setContentHandler(new LoggerContentHandler());

        parser.getXMLReader().parse(new InputSource(Files.newInputStream(Paths.get("./test.xml"))));
    }

    @Override
    public void startDocument() throws SAXException {
        System.out.println("startDocument");
    }

    @Override
    public void endDocument() throws SAXException {
        System.out.println("endDocument");
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        Map<String, String> attrMap = new HashMap<>();
        for (int i = 0; i < atts.getLength(); i++) {
            String name = atts.getLocalName(i);
            if (name.isEmpty()) {
                name = atts.getQName(i);
            }
            String value = atts.getValue(i);
            attrMap.put(name, value);
        }

        System.out.println("startElement, uri: " + uri + ", localName: " + localName + ", qName: " + qName +
            ", atts: " + attrMap);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        System.out.println("endElement, uri: " + uri + ", localName: " + localName + ", qName: " + qName);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        System.out.println("characters: " + new String(ch, start, length));
    }
}

输出如下

startDocument
startElement, uri: , localName: , qName: department, atts: {code=deptcode001, name=deptname001}
characters: 

startElement, uri: , localName: , qName: user, atts: {code=usercode001, name=username001}
endElement, uri: , localName: , qName: user
characters: 

startElement, uri: , localName: , qName: user, atts: {code=usercode002, name=username002}
endElement, uri: , localName: , qName: user
characters: 

startElement, uri: , localName: , qName: extension, atts: {}
characters: 

startElement, uri: , localName: , qName: property-name, atts: {}
characters: director
endElement, uri: , localName: , qName: property-name
characters: 

startElement, uri: , localName: , qName: property-value, atts: {}
characters: joke
endElement, uri: , localName: , qName: property-value
characters: 

endElement, uri: , localName: , qName: extension
characters: 

endElement, uri: , localName: , qName: department
endDocument

digester是怎么实现的?

digester是SAX的高层次封装,类继承图如下
image.png
可以看到其本身就是一个ContentHandler,因此可以在xml解析的各时期收到回调事件,主要是接收startDocument、endDocument、startElement、endElement、characters这五个事件。这种级别上的抽象对于SAX来讲足够好了,但对于使用者来讲不够便利,主要有两点

  • SAX是面向事件的,解析每个标签都会发出startElement、endElement、characters,怎么对不同标签进行不同的处理?
  • 不同标签的处理往往需要创建不同的对象,然后设置不同属性,但创建对象、设置属性本身是通用的,怎么复用?
  • 一个标签会包含属性、子标签、body等,处理子标签时当前标签的上下文怎么处理?

怎么从面向事件转为面向标签?

image.png
digester将对标签的不同操作抽象为rule,通过rules维护不同标签的处理操作。当digester解析到某个标签时,就能够获取到该标签的对应的rule,从而调用rules的方法处理当前标签

Rule是怎么抽象的?

Rule表示针对某个标签的具体的操作,digester给了一些默认的Rule

  • ObjectCreateRule:当遇到标签开始时,会创建一个对象,push到对象栈里面;这个标签结束时会从栈里面弹出对象
  • SetPropertiesRule:遇到标签开始时,会从标签中获取属性,填充到对象里
  • SetNextRule:当遇到标签结束时,会将栈顶元素作为参数,调用栈顶第二个元素的指定方法

Rule有四个回调方法:

  • begin。开始解析一个标签时的回调,能够获取到这个标签的属性

  • body。标签结束时的回调,能够获取到这个标签里定义的body

  • end。标签解析结束的回调

  • finish。xml解析结束的回调
    SAX回调什么时候会回调这几个方法呢?

  • startElement事件:begin方法会被调用,传入标签的attribute

  • characters事件:digester只暂存body,不会调用rule方法

  • endElement事件:首先调用body方法,传入标签body;然后调用end方法,没有参数

  • endDocument事件:调用finish方法,rule可以做清理操作

上下文处理

digester的xml处理上下文是基于栈实现的。当处理标签/a时,在开始时会为/a标签创建上下文,当遇到子标签/a/b时,会将/a的上下文push到栈里面,再为/a/b创建上下文,当/a/b标签解析完之后,在从栈里面弹出上下文,继续处理/a标签。具体包括四种栈:

  • 对象栈。标签开始时可以创建对象push到栈里面,标签结束后要弹出
  • rule栈。标签开始时,会从rules里面找到当前标签对应的rule,push到栈里面暂存
  • body栈。暂存每个标签的body内容
  • 参数栈。暂存CallMethodRule的参数

具体就是Digester的几个属性

// 当前body
protected StringBuilder bodyText = new StringBuilder();
// body栈
protected ArrayStack<StringBuilder> bodyTexts = new ArrayStack<>();

// rule栈
protected ArrayStack<List<Rule>> matches = new ArrayStack<>(10);

// 参数栈
protected ArrayStack<Object> params = new ArrayStack<>();

// 暂存的根对象
protected Object root = null;
// 对象栈
protected ArrayStack<Object> stack = new ArrayStack<>();

代码实现

startElement方法(删除了无关代码)

    public void startElement(String namespaceURI, String localName, String qName, Attributes list)
            throws SAXException {

        // 对Attribute做一下处理,替换一下占位符
        list = updateAttributes(list);

        // 一个新的标签的开始,需要暂存上一个标签的bodyText,并创建新的bodyText,用于接收当前标签的body内容
        bodyTexts.push(bodyText);
        bodyText = new StringBuilder();

        // 获取当前标签名字
        String name = localName;
        if ((name == null) || (name.length() < 1)) {
            name = qName;
        }

        // match记录的是当前的标签的处理路径,例如/dept/a。当新标签开始,就需要追加当前标签名字。
        // 相应的,当标签解析结束时,就需要回退。这个逻辑在endElement方法里
        StringBuilder sb = new StringBuilder(match);
        if (match.length() > 0) {
            sb.append('/');
        }
        sb.append(name);
        match = sb.toString();

        // 通过Rules获取处理当前标签的Rule,暂存到matches里,并调用每个rule的begin方法
        List<Rule> rules = getRules().match(namespaceURI, match);
        matches.push(rules);
        if ((rules != null) && (rules.size() > 0)) {
            for (Rule value : rules) {
                try {
                    Rule rule = value;
                    rule.begin(namespaceURI, name, list);
                } catch (Exception e) {}
            }
        }

    }

characters方法(删除了无关代码)

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

        // 就是接收body内容,暂存起来
        bodyText.append(buffer, start, length);
    }

endElement方法(删除了无关代码)

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

        // 解析body里面的占位符
        bodyText = updateBodyText(bodyText);

        // 提取当前标签名字
        String name = localName;
        if ((name == null) || (name.length() < 1)) {
            name = qName;
        }

        // 从rule栈里拿出匹配上的rule。调用rule的body方法
        List<Rule> rules = matches.pop();
        if ((rules != null) && (rules.size() > 0)) {
            String bodyText = this.bodyText.toString().intern();
            for (Rule value : rules) {
                try {
                    Rule rule = value;
                    rule.body(namespaceURI, name, bodyText);
                } catch (Exception e) {}
            }
        }

        // 标签结束,要将外层标签的bodyText弹出,恢复上下文
        bodyText = bodyTexts.pop();

        // 调用rule的end方法
        if (rules != null) {
            for (int i = 0; i < rules.size(); i++) {
                int j = (rules.size() - i) - 1;
                try {
                    Rule rule = rules.get(j);
                    rule.end(namespaceURI, name);
                } catch (Exception e) {}
            }
        }

        // 恢复match内容
        int slash = match.lastIndexOf('/');
        if (slash >= 0) {
            match = match.substring(0, slash);
        } else {
            match = "";
        }

    }

endDocument方法(删除了无关代码)

   public void endDocument() throws SAXException {

       // 清空栈
        while (getCount() > 1) {
            pop();
        }

        // 调用所有rule的finish方法
        for (Rule rule : getRules().rules()) {
            try {
                rule.finish();
            } catch (Exception e) {}
        }

        clear();
    }

这里面的对象栈跟参数栈digester并没有修改,而是由Rule去操作这些栈。对象栈由ObjectCreateRule操作、参数栈由CallMethodRule操作

More Details

ObjectCreateRule的实现

public class ObjectCreateRule extends Rule {

    public ObjectCreateRule(String className) {
        this(className, (String) null);
    }
    public ObjectCreateRule(String className,
                            String attributeName) {
        this.className = className;
        this.attributeName = attributeName;
    }

    // className就是传进来创建对象的类。attributeName就是获取className的属性名,可以覆盖className
    protected String attributeName = null;
    protected String className = null;

    @Override
    public void begin(String namespace, String name, Attributes attributes)
            throws Exception {

        // 获取className,attributeName中获取的可以覆盖className
        String realClassName = className;
        if (attributeName != null) {
            String value = attributes.getValue(attributeName);
            if (value != null) {
                realClassName = value;
            }
        }

        // 实例化对象,push到对象栈里面
        Class<?> clazz = digester.getClassLoader().loadClass(realClassName);
        Object instance = clazz.getConstructor().newInstance();
        digester.push(instance);
    }

    @Override
    public void end(String namespace, String name) throws Exception {
        // end调用时需要弹出当前对象
        Object top = digester.pop();
    }
}

SetNextRule的实现。SetNextRule解决每个标签单独创建对象但是不能相互关联的问题,就是将栈顶对象作为参数,通过调用栈里面第二个对象个指定方法进行关联的

public class SetNextRule extends Rule {

    public SetNextRule(String methodName,
                       String paramType) {
        this.methodName = methodName;
        this.paramType = paramType;
    }

    // 指定的方法名跟参数类型
    protected String methodName = null;
    protected String paramType = null;
    protected boolean useExactMatch = false;// getter and setter

    @Override
    public void end(String namespace, String name) throws Exception {

        // 取出栈顶元素和第二个元素,调用相应方法
        Object child = digester.peek(0);
        Object parent = digester.peek(1);
        IntrospectionUtils.callMethod1(parent, methodName,
                child, paramType, digester.getClassLoader());
    }
}

SetPropertiesRule用来设置栈顶对象的属性

public class SetPropertiesRule extends Rule {

    @Override
    public void begin(String namespace, String theName, Attributes attributes)
            throws Exception {

        // 获取栈顶对象
        Object top = digester.peek();

        // 遍历attribute,通过IntrospectionUtils.setProperty设置属性
        for (int i = 0; i < attributes.getLength(); i++) {
            String name = attributes.getLocalName(i);
            if (name.isEmpty()) {
                name = attributes.getQName(i);
            }
            String value = attributes.getValue(i);
            if (!digester.isFakeAttribute(top, name)
                    && !IntrospectionUtils.setProperty(top, name, value)
                    && digester.getRulesValidation()) {
            }
        }

    }
}

怎么持有根节点?

ObjectCreateRule在标签开始时创建对象,在标签结束时弹出对象,那么当xml解析完毕,根对象也就弹出去了,怎么持有根对象呢?有两种方式
第一种方式,digester的push方法会判断栈是否为空,空的话会暂存到root属性上,解析结束后可以通过root属性获取根对象

  public void push(Object object) {

        if (stack.size() == 0) {
            root = object;
        }
        stack.push(object);
    }

第二种方式,digester使用方push一个对象到栈里面,当作跟对象的“容器”,解析根对象的时候SetNextRule将根对象关联到“容器”里

catalina中digester的使用

catalina里通过digester解析server.xml(有删除)

    protected Digester createStartDigester() {
        Digester digester = new Digester();

        // 1、创建Server,2、设置属性,3、将server关联到外部catalina实例(createStartDigester之后还会将Catalina实例push进去)
        digester.addObjectCreate("Server",
                                 "org.apache.catalina.core.StandardServer",
                                 "className");
        digester.addSetProperties("Server");
        digester.addSetNext("Server",
                            "setServer",
                            "org.apache.catalina.Server");

        // 创建listener、设置属性、关联到Server
        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");

        // 创建service、设置属性、关联到Server
        digester.addObjectCreate("Server/Service",
                                 "org.apache.catalina.core.StandardService",
                                 "className");
        digester.addSetProperties("Server/Service");
        digester.addSetNext("Server/Service",
                            "addService",
                            "org.apache.catalina.Service");

        // 为service创建listener,关联到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,关联到service
        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");

        // 创建Connector,关联到Service
        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");

        // 创建Listener,关联到Connector
        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");

        // 为Engine、Host、Context创建Rule
        digester.addRuleSet(new EngineRuleSet("Server/Service/"));
        digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
        digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));

        return digester;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值