Apache Commons Digester 一 (基础内容、核心API)

简介:

前言

  在许多需要处理XML格式数据的应用环境中, 如果能够以“事件驱动”的方式来处理XML文档,比如,当识别出特定的XML元素时,触发“创建对象”操作事件(或者触发调用对象的方法事件),这对于应用程序开发来说,是非常有用的;
熟悉以SAX(Simple API for XML Parsing)方式来处理XML文档的开发人员会认识到,Digester为SAX事件提供了更高层次,对开发者更加友好的接口,它隐藏了大部分导航XML元素层次结构的细节,以便于开发者更加专注于要执行的处理操作;

使用Digester的基本步骤

  1. 创建一个 org.apache.commons.digester3.Digester 类的实例对象。这里补充说明下,只要我们已经完成XML解析操作,并且不在多个线程中使用同一个Digester对象,那么就可以安全的重复使用我们预先创建的这个Digester实例;不过重用Digester实例并不是非常推荐,最好每个XML解析对应一个单独的Digester实例;
  2. 为Digester实例配置属性值,通过配置属性值,我们可以改变Digester 的解析行为,具体有哪些属性值可以配置,待会会介绍;
  3. 可选的, 可以将我们的一些初始对象push到Digester栈里;.
  4. 在输入的XML文档中,给所有需要触发规则(rule)处理的元素匹配模式(pattern)注册规则;针对任何一个模式,你可以注册任意数量的规则;补充说明下,如果一个模式对应多个规则,则begin和body事件方法会按照它们注册的顺序依次执行,而end事件方法是倒序执行的;
  5. 最后,调用digester.parse()方法,该方法需要传入XML文件的引用作为参数,该参数支持多种格式的文件流;另外需要注意的是,该方法会抛出IOException or SAXException异常,以及各种可能的在规则解析处理时遇到的异常,如NoSuchMethodException、IllegalAccessException…

了解基本步骤后,来看一个简单的示例,如下所示,是我们即将要解析的xml文件:

<foo name="The Parent">
    <bar id="123" title="The First Child" />
    <bar id="456" title="The Second Child" />
    <bar id="789" title="The Second Child" />
</foo>

首先,创建两个java bean对应xml中的元素信息:

Foo类

  View Code

Bar类

  View Code

使用Digester解析xml:

复制代码
package apache.commons.digester3.example.simpletest;

import java.io.IOException;

import org.apache.commons.digester3.Digester;
import org.xml.sax.SAXException;

import apache.commons.digester3.example.pojo.Bar;
import apache.commons.digester3.example.pojo.Foo;

/**
 * 
 * @author http://www.cnblogs.com/chenpi/
 * @version 2017年6月3日
 */

public class Main
{

    public static void main(String[] args)
    {

        try
        {
            //1、创建Digester对象实例
            Digester digester = new Digester();

            //2、配置属性值
            digester.setValidating(false);

            //3、push对象到对象栈
            //digester.push(new Foo());
            
            //4、设置匹配模式、规则
            digester.addObjectCreate("foo", "apache.commons.digester3.example.pojo.Foo");
            digester.addSetProperties("foo");
            digester.addObjectCreate("foo/bar", "apache.commons.digester3.example.pojo.Bar");
            digester.addSetProperties("foo/bar");
            digester.addSetNext("foo/bar", "addBar", "apache.commons.digester3.example.pojo.Bar");

            //5、开始解析
            Foo foo = digester.parse(Main.class.getClassLoader().getResourceAsStream("example.xml"));

            //6、打印解析结果
            System.out.println(foo.getName());
            for (Bar bar : foo.getBarList())
            {
                System.out.println(bar.getId() + "," + bar.getTitle());
            }

        }
        catch (IOException e)
        {

            e.printStackTrace();
        }
        catch (SAXException e)
        {

            e.printStackTrace();
        }
    }
}
复制代码

结果打印:

The Parent
123,The First Child
456,The Second Child
789,The Second Child

注意以上代码涉及类型的自动转换,如id属性,由字符串类型转为整型,这里所有的类型转换都是由commons-beanutils包的ConvertUtils来完成的。

Digester属性配置

org.apache.commons.digester3.Digester实例对象包含若干成员属性,这些属性值是可以设置的,以便我们自定义解析操作;

为了让这些配置在XML解析前生效,这些属性值的更改一定要在parse方法调用之前设置;

如下是一些可以配置的属性

Property  Description
classLoader

通过配置这个属性值,我们可以指定ObjectCreateRule和FactoryCreateRule规则使用的类加载器;另外,在没指定该值的时候,如果useContextClassLoader属性值设为true,则会使用当前线程上下文类加载器,否则直接使用加载Digester类的同一个类加载器;

errorHandler 通过配置这个属性值,我们可以指定一个SAX ErrorHandler,当解析错误出现的时候,该Handler会收到通知;
namespaceAware 通过设置该boolean值,可以让Digester在解析的时候,识别出XML命名空间;
xincludeAware 通过设置该boolean值,可以让Digester在解析的时候识别出语法 ,注意该设置只有在namespaceAware设为true的前提下才有效.
ruleNamespaceURI 该值一般配合namespaceAware使用,在namespaceAware设为true的情况下,设置ruleNamespaceURI,那么接下来的规则只会匹配ruleNamespaceURI命名空间下的元素;
rules 通过该属性,我们可以额外添加一些自定义解析匹配规则;
useContextClassLoader

如果useContextClassLoader属性值设为true,则会使用当前线程上下文类加载器,否则直接使用加载Digester类的同一个类加载器

注意 - 如果设置了classLoader属性值,改属性配置将被忽略;

validating 通过设置该boolean值, 可以配置Digester是否做DTD检查

另外,我们可以通过Digesterregister方法,让Digester在遇到DOCTYPE声明时,使用本地dtd,而不是从网上获取,如下所示:

        URL url = new URL("/org/apache/struts/resources/struts-config_1_0.dtd");
        digester.register("-//Apache Software Foundation//DTD Struts Configuration 1.0//EN", url.toString());

Digester对象栈

Digester使用的一个核心技术就是动态构建一颗java对象树,在构建的过程中,一个重要的辅助数据结构即对象栈;

以如下xml为例:

<foo name="The Parent">
    <bar id="123" title="The First Child" />
    <bar id="456" title="The Second Child" />
    <bar id="789" title="The Second Child" />
</foo>

在解析的时候:

首先会创建一个foo对象,并压入对象栈,然后设置foo属性值name,紧接着,创建bar对象并压入栈,然后设置bar的属性值,然后将该bar对象添加的到foo对象的barlist属性集合中,然后bar对象弹出对象栈;

以此类推,遇到起始标记的元素创建对象入栈,遇到结尾标记的元素做出栈操作,出栈前,需要将出栈对象并关联到上一个栈顶对象;

最终,解析完xml后,留在栈顶的就关联了所有在xml解析中创建的动态对象了;

Digester暴露出的与对象栈操作API如下所示:

  • clear() - 清除对象栈.
  • peek() - 返回栈顶对象引用,但是不弹出.
  • pop() - 返回栈顶对象,并弹出.
  • push() - 入栈操作.

Digester元素匹配模式

Digester的一个关键特性是可以自动识别xml的层次结构,程序员只需要关心遇到匹配到某个元素后需要做哪些操作即可;

如下是一个示例,其中a, a/b, a/b/c为匹配模式,对应xml中特定位置的元素:

复制代码
  <a>         -- Matches pattern "a"
    <b>       -- Matches pattern "a/b"
      <c/>    -- Matches pattern "a/b/c"
      <c/>    -- Matches pattern "a/b/c"
    </b>
    <b>       -- Matches pattern "a/b"
      <c/>    -- Matches pattern "a/b/c"
      <c/>    -- Matches pattern "a/b/c"
      <c/>    -- Matches pattern "a/b/c"
    </b>
  </a>
复制代码

Digester规则处理

当匹配到模式时,会触发规则处理,具体的规则处理机制是由这个org.apache.commons.digester3.Rule接口封装的,该接口定义了以下几个方法:

  • begin() - 匹配到xml元素开始标记时,调用该方法;
  • body() - 匹配到xml元素body时,调用该方法;
  • end() - 匹配到xml元素结束标记时,调用该方法;
  • finish() - 当所有解析方法解析完毕后,调用该方法,用于清楚临时数据等;

默认情况下,Digester提供了以下Rule接口的实现类,我们在编码的时候可以直接使用,详见API文档:

如下是一个SetNextRule规则实现类的示例(两种写法):

            Rule rule = new SetNextRule("addBar",Bar.class);
            digester.addRule("foo/bar", rule );

            //digester.addSetNext("foo/bar", "addBar", Bar.class.getName());

Digester日志

日志是调试、排查错误非常关键的一个环节,Digester记录了非常详细的日志,我们可以按如下方式来开启日志打印功能;

这里的日志实现选择log4j,

首先,在pom.xml加上如下依赖:

        <!-- https://mvnrepository.com/artifact/log4j/log4j -->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

然后,编写一个配置文件log4j.properties放到resources路径下:

复制代码
### set log levels ###
log4j.rootLogger = debug, stdout

### \u8F93\u51FA\u5230\u63A7\u5236\u53F0 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n
复制代码

运行程序,发现已经可以看到DEBUG调试日志了日志:

2017-06-04 18:26:33  [ main:51 ] - [ DEBUG ]    Fire body() for SetPropertiesRule[aliases={}, ignoreMissingProperty=true]
2017-06-04 18:26:33  [ main:51 ] - [ DEBUG ]    Popping body text ''
2017-06-04 18:26:33  [ main:51 ] - [ DEBUG ]    Fire end() for SetPropertiesRule[aliases={}, ignoreMissingProperty=true]
2017-06-04 18:26:33  [ main:52 ] - [ DEBUG ]    Fire end() for ObjectCreateRule[className=apache.commons.digester3.example.pojo.Foo, attributeName=null]
2017-06-04 18:26:33  [ main:52 ] - [ DEBUG ]  [ObjectCreateRule]{foo} Pop 'apache.commons.digester3.example.pojo.Foo'
2017-06-04 18:26:33  [ main:52 ] - [ DEBUG ]  endDocument()
The Parent
123,The First Child
456,The Second Child
789,The Second Child

Digester例子

前面我们已经举了一个Digester的简单使用例子,这里将继续展示几个示例;

解析xml元素body值

如下XML文档就是我们要解析内容:

复制代码
<web-app>
    <servlet>
        <servlet-name>action</servlet-name>
        <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
            <init-param>
                <param-name>application</param-name>
                <param-value>org.apache.struts.example.ApplicationResources</param-value>
            </init-param>
            <init-param>
                <param-name>config</param-name>
                <param-value>/WEB-INF/struts-config.xml</param-value>
            </init-param>
    </servlet>
</web-app>
复制代码

首先,定义一个ServletBean,存储以上xml信息,如下所示:

复制代码
/*
 * File Name: ServletBean.java
 * Description: 
 * Author: http://www.cnblogs.com/chenpi/
 * Create Date: 2017年6月4日
 */
package apache.commons.digester3.example.pojo;

import java.util.HashMap;
import java.util.Map;

/**
 * 
 * @author    http://www.cnblogs.com/chenpi/
 * @version   2017年6月4日
 */

public class ServletBean
{

    private String servletName;
    private String servletClass;
    private Map<String, String> initParams = new HashMap<String, String>();
    
    
    public void addInitParam(String paramName, String paramValue){
        initParams.put(paramName, paramValue);
    }
    /**
     * @return the servletName
     */
    public String getServletName()
    {
        return servletName;
    }
    /**
     * @param servletName the servletName to set
     */
    public void setServletName(String servletName)
    {
        this.servletName = servletName;
    }
    /**
     * @return the servletClass
     */
    public String getServletClass()
    {
        return servletClass;
    }
    /**
     * @param servletClass the servletClass to set
     */
    public void setServletClass(String servletClass)
    {
        this.servletClass = servletClass;
    }
    /**
     * @return the initParams
     */
    public Map<String, String> getInitParams()
    {
        return initParams;
    }
    /**
     * @param initParams the initParams to set
     */
    public void setInitParams(Map<String, String> initParams)
    {
        this.initParams = initParams;
    }
}
复制代码

编写规则解析xml,如下所示:

复制代码
/*
 * File Name: Main2.java
 * Description: 
 * Author: http://www.cnblogs.com/chenpi/
 * Create Date: 2017年6月4日
 */
package apache.commons.digester3.example.simpletest;

import java.io.IOException;

import org.apache.commons.digester3.Digester;
import org.apache.commons.digester3.Rule;
import org.apache.commons.digester3.SetNextRule;
import org.xml.sax.SAXException;

import apache.commons.digester3.example.pojo.Bar;
import apache.commons.digester3.example.pojo.Foo;
import apache.commons.digester3.example.pojo.ServletBean;

/**
 * 
 * @author http://www.cnblogs.com/chenpi/
 * @version 2017年6月4日
 */

public class WebMain
{

    public static void main(String[] args)
    {
        try
        {
            // 1、创建Digester对象实例
            Digester digester = new Digester();

            // 2、配置属性值
            digester.setValidating(false);

            // 3、push对象到对象栈

            // 4、设置匹配模式、规则
            digester.addObjectCreate("web-app/servlet", "apache.commons.digester3.example.pojo.ServletBean");
            digester.addCallMethod("web-app/servlet/servlet-name", "setServletName", 0);
            digester.addCallMethod("web-app/servlet/servlet-class", "setServletClass", 0);
            digester.addCallMethod("web-app/servlet/init-param", "addInitParam", 2);
            digester.addCallParam("web-app/servlet/init-param/param-name", 0);
            digester.addCallParam("web-app/servlet/init-param/param-value", 1);

            // 5、开始解析
            ServletBean servletBean = digester
                .parse(ExampleMain.class.getClassLoader().getResourceAsStream("web.xml"));

            // 6、打印解析结果
            System.out.println(servletBean.getServletName());
            System.out.println(servletBean.getServletClass());
            for(String key : servletBean.getInitParams().keySet()){
                System.out.println(key + ": " + servletBean.getInitParams().get(key));
            }

        }
        catch (IOException e)
        {

            e.printStackTrace();
        }
        catch (SAXException e)
        {

            e.printStackTrace();
        }

    }
}
复制代码

结果打印:

action
org.apache.struts.action.ActionServlet
application: org.apache.struts.example.ApplicationResources
config: /WEB-INF/struts-config.xml

解析XML命名空间

对于没有使用命名空间的xml来说,Digester默认的处理机制已经足够满足我们的需求了。但是当XML文档使用命名空间的时候,对于不同命名空间的元素来说,有时候我们希望使用不同的规则去解析它。 
Digester 没有提供对命名空间的完全支持, 但已经足够完成大多数任务了. 开启Digester的命名空间支持只需要以下几个步骤即可:
1、通过配置以下属性值,告诉 Digester,需要开启命名空间解析:

 digester.setNamespaceAware( true );

2、声明接下来的规则关联的命名空间,注意我们这里没有指明任何前缀,XML文档的作者是可以使用任何他们喜欢的前缀的

digester.setRuleNamespaceURI( "http://www.mycompany.com/MyNamespace" );

3、添加该命名空间下的规则, 通常会调用 addObjectCreate() 或者 addSetProperties()这类方法. 注意,这里匹配的模式是不需要加上前缀的:

    digester.addObjectCreate( "foo/bar", "com.mycompany.MyFoo" );
    digester.addSetProperties( "foo/bar");    

4.    重复2、3步骤,解析其它命名空间的元素.
如下,是一个示例,使用以上步骤即可完成解析:

<m:foo
   xmlns:m="http://www.mycompany.com/MyNamespace"
   xmlns:y="http://www.yourcompany.com/YourNamespace">
  <m:bar name="My Name" value="My Value"/>
  <y:bar id="123" product="Product Description"/>
</m:foo>

由于我们给Digester指定的命名空间为http://www.mycompany.com/MyNamespace,所以以上xml只有第一个bar会被解析出来。

如下是一个完整示例(XML及对应解析代码):

<m:foo xmlns:m="http://www.mycompany.com/MyNamespace" 
       xmlns:y="http://www.yourcompany.com/YourNamespace" name="The Parent">
    <m:bar id="123" title="The First Child" />
    <y:bar id="456" title="The Second Child" />
    <m:bar id="789" title="The Second Child" />
</m:foo>
复制代码
/*
 * File Name: Main.java
 * Description: 
 * Author: http://www.cnblogs.com/chenpi/
 * Create Date: 2017年6月3日
 */
package apache.commons.digester3.example.simpletest;

import java.io.IOException;

import org.apache.commons.digester3.Digester;
import org.xml.sax.SAXException;

import apache.commons.digester3.example.pojo.Bar;
import apache.commons.digester3.example.pojo.Foo;

/**
 * 
 * @author http://www.cnblogs.com/chenpi/
 * @version 2017年6月3日
 */

public class ExampleNSMain
{

    public static void main(String[] args)
    {

        try
        {
            Digester digester = new Digester();

            digester.setValidating(false);
            digester.setNamespaceAware(true);
            digester.setRuleNamespaceURI("http://www.mycompany.com/MyNamespace");

            digester.addObjectCreate("foo", Foo.class);
            digester.addSetProperties("foo");
            digester.addObjectCreate("foo/bar", Bar.class);
            digester.addSetProperties("foo/bar");

            digester.addSetNext("foo/bar", "addBar", Bar.class.getName());

            Foo foo = digester
                .parse(ExampleNSMain.class.getClassLoader().getResourceAsStream("example_ns.xml"));

            System.out.println(foo.getName());
            for (Bar bar : foo.getBarList())
            {
                System.out.println(bar.getId() + "," + bar.getTitle());
            }

        }
        catch (IOException e)
        {

            e.printStackTrace();
        }
        catch (SAXException e)
        {

            e.printStackTrace();
        }

    }
}
复制代码

使用命名空间前缀用于模式匹配

当一个命名空间下的规则集合与另一个命名空间的规则集合相互独立的话,使用带命名空间的规则非常有用,但是当我们的规则逻辑需要使用到不同命名空间下的元素时,那么使用带命名空间前缀的模式将会是一个更好的策略;
很简单,我们只需要,设置NamespaceAware 属性为false,然后在模式前面带上命名空间前缀即可。
比如, (将 NamespaceAware 设为false), 那么模式 m:bar' 只会匹配命名空间前缀为m的bar元素.

如下是一个完整demo:

复制代码
/*
 * File Name: Main.java
 * Description: 
 * Author: http://www.cnblogs.com/chenpi/
 * Create Date: 2017年6月3日
 */
package apache.commons.digester3.example.simpletest;

import java.io.IOException;

import org.apache.commons.digester3.Digester;
import org.xml.sax.SAXException;

import apache.commons.digester3.example.pojo.Bar;
import apache.commons.digester3.example.pojo.Foo;

/**
 * 
 * @author http://www.cnblogs.com/chenpi/
 * @version 2017年6月3日
 */

public class ExampleNS2Main
{

    public static void main(String[] args)
    {

        try
        {
            Digester digester = new Digester();

            digester.setValidating(false);
            digester.setNamespaceAware(false);
            //digester.setRuleNamespaceURI("http://www.mycompany.com/MyNamespace");

            digester.addObjectCreate("m:foo", Foo.class);
            digester.addSetProperties("m:foo");
            digester.addObjectCreate("m:foo/m:bar", Bar.class);
            digester.addSetProperties("m:foo/m:bar");
            digester.addSetNext("m:foo/m:bar", "addBar", Bar.class.getName());

            digester.addObjectCreate("m:foo/y:bar", Bar.class);
            digester.addSetProperties("m:foo/y:bar");
            digester.addSetNext("m:foo/y:bar", "addBar", Bar.class.getName());

            Foo foo = digester
                .parse(ExampleNS2Main.class.getClassLoader().getResourceAsStream("example_ns.xml"));

            System.out.println(foo.getName());
            for (Bar bar : foo.getBarList())
            {
                System.out.println(bar.getId() + "," + bar.getTitle());
            }

        }
        catch (IOException e)
        {

            e.printStackTrace();
        }
        catch (SAXException e)
        {

            e.printStackTrace();
        }

    }
}
复制代码

错误排查

Digester 是基于 SAX 开发的. Digestion 会抛出两种类型的 Exception:

  • java.io.IOException
  • org.xml.sax.SAXException

第一个异常很少会抛出,且该异常众所周知。 通常我们遇到最多的是第二个异常,当SAX解析无法完成的时候会抛出该异常,所以熟悉SAX的错误处理方式对诊断SAXException很有帮助。
当一个SAX 解析器 遇到xml问题时 (哈哈,有时候,会在遇到xml问题之后),会抛出SAXParseException异常. 该异常是SAXException 的子类,并且包含了一些额外信息(哪里出错,出了什么错误),如果我们捕获了这类异常,那么就可以明确知道问题是XML引起的,而不是Digester或者我们的解析规则。通常来说,捕获该异常并记录详细信息到日志对诊断错误非常有帮助。
一般情况下 SAXException 异常内部会组合另一个异常,换句话说,就是当Digester遇到异常的时候,会首先将该异常封装成一个SAXException异常,然后将该SAXException重新抛出 。所以,捕获SAXException异常,并仔细检查被封装的内部异常,有助于排查错误;
错误示例:

org.xml.sax.SAXParseException; lineNumber: 4; columnNumber: 44; 元素类型 "y:bar" 必须后跟属性规范 ">" 或 "/>"。
    at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(AbstractSAXParser.java:1239)
    at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(SAXParserImpl.java:648)
    at org.apache.commons.digester3.Digester.parse(Digester.java:1642)
    at org.apache.commons.digester3.Digester.parse(Digester.java:1701)
    at apache.commons.digester3.example.simpletest.ExampleNS2Main.main(ExampleNS2Main.java:48)

参考资料

http://commons.apache.org/proper/commons-digester/guide/core.html

示例代码

https://github.com/peterchenhdu/apache-commons-digester-example

本文转自风一样的码农博客园博客,原文链接:http://www.cnblogs.com/chenpi/p/6930730.html,如需转载请自行联系原作者
相关文章
|
5月前
|
Apache
java.lang.NoClassDefFoundError: org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream
java.lang.NoClassDefFoundError: org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream
149 0
|
2月前
|
安全 前端开发 程序员
Springboot-EolinkApikit一键生成注释与一键上传API接口(idea社区版2023.1.4+apache-maven-3.9.3-bin)
Springboot-EolinkApikit一键生成注释与一键上传API接口(idea社区版2023.1.4+apache-maven-3.9.3-bin)
18 0
|
2月前
|
Java API Maven
Springboot快速搭建跨域API接口(idea社区版2023.1.4+apache-maven-3.9.3-bin)
Springboot快速搭建跨域API接口(idea社区版2023.1.4+apache-maven-3.9.3-bin)
33 0
|
2月前
|
算法 Java Apache
Apache Commons
Apache Commons是一个开源项目,提供了一系列的工具和库,用于简化Java开发中的常见任务。
24 1
|
4月前
|
Java API Apache
ZooKeeper【基础 03】Java 客户端 Apache Curator 基础 API 使用举例(含源代码)
ZooKeeper【基础 03】Java 客户端 Apache Curator 基础 API 使用举例(含源代码)
22 0
|
11月前
|
API Apache
Apache Kafka-通过API获取主题所有分区的积压消息数量
Apache Kafka-通过API获取主题所有分区的积压消息数量
100 0
|
11月前
|
API Apache 数据安全/隐私保护
Apache ZooKeeper - 使用原生的API操作ZK_ACL权限
Apache ZooKeeper - 使用原生的API操作ZK_ACL权限
83 0
|
11月前
|
Java 测试技术 API
Apache ZooKeeper - 使用原生的API操作ZK
Apache ZooKeeper - 使用原生的API操作ZK
75 0
|
11月前
|
移动开发 前端开发 Java
Spring MVC-09循序渐进之文件上传(基于Apache Commons FileUpload)
Spring MVC-09循序渐进之文件上传(基于Apache Commons FileUpload)
74 0
|
11月前
|
Dubbo 应用服务中间件 测试技术
带你读《Apache Dubbo微服务开发从入门到精通》——二、 API配置(上)
带你读《Apache Dubbo微服务开发从入门到精通》——二、 API配置(上)
92 0

热门文章

最新文章

推荐镜像

更多