新书推介:《语义网技术体系》
作者:瞿裕忠,胡伟,程龚
   XML论坛     W3CHINA.ORG讨论区     计算机科学论坛     SOAChina论坛     Blog     开放翻译计划     新浪微博  
 
  • 首页
  • 登录
  • 注册
  • 软件下载
  • 资料下载
  • 核心成员
  • 帮助
  •   Add to Google

    >> 本版讨论Java, J2SE, J2ME, J2EE, 以及Eclipse, NetBeans, JBuilder等Java开发环境,还有JSP, JavaServlet, JavaBean, EJB以及struts, hibernate, spring, webwork2, Java 3D, JOGL等相关技术。
    [返回] 中文XML论坛 - 专业的XML技术讨论区计算机技术与应用『 Java/Eclipse 』 → 从 XML 到 Java 的数据绑定-从文本到字节码(转贴) 查看新帖用户列表

      发表一个新主题  发表一个新投票  回复主题  (订阅本版) 您是本帖的第 6834 个阅读者浏览上一篇主题  刷新本主题   树形显示贴子 浏览下一篇主题
     * 贴子主题: 从 XML 到 Java 的数据绑定-从文本到字节码(转贴) 举报  打印  推荐  IE收藏夹 
       本主题类别:     
     mfc42d 帅哥哟,离线,有人找我吗?
      
      
      等级:大三暑假(ITELS考了6.5分!)(版主)
      文章:65
      积分:882
      门派:XML.ORG.CN
      注册:2004/6/13

    姓名:(无权查看)
    城市:(无权查看)
    院校:(无权查看)
    给mfc42d发送一个短消息 把mfc42d加入好友 查看mfc42d的个人资料 搜索mfc42d在『 Java/Eclipse 』的所有贴子 引用回复这个贴子 回复这个贴子 查看mfc42d的博客楼主
    发贴心情 从 XML 到 Java 的数据绑定-从文本到字节码(转贴)

    Brett McLaughlin
    Enhydra 策略顾问,Lutris Technologies
    2000 年 9 月

    内容:  
      
      
    XML 实例文档  
    打开前门  
    转换数据  
    递归对象树  
    生成类  
    使用 unmarshaller  
    启动 Web 服务  
    逐步发展的 API  
    结束语  
    参考资料  
    关于作者  
      


    本数据绑定系列的第三部分演示了如何使用“JSR-031:数据绑定,Sun 数据绑定规范申请”中指定的方法,将 XML 元素和属性转换成 Java 对象。这部分主要讲述从数据的 XML 表示移到应用程序代码易于使用的 Java 实例。第三部分论及通过将 XML 文档中的嵌套元素取消编组成 Java 对象、测试和用某些实际示例来使用新的工具。

    本系列的目标是演示如何将 XML 元素转换成 Java 对象,然后可以使用 Java 语言 accessor 和 mutator 方法直接处理 XML 数据。第一部分比较了数据绑定和 Java 应用程序中其它处理 XML 数据的方法,分析了设计决策,还定义了示例 Web 服务配置文档的 XML 模式。第二部分说明了如何从 XML 模式生成接口和实现,以便符合 XML 模式的 XML 文档可以转换成这些生成类的实例。

    在第三部分(共四部分)中,将完成基础知识的讲解,并且描述了如何精心设计代码以执行取消编组,取消编组将完成将 XML 转换成 Java 对象的过程。执行了取消编组后,可以使用测试类(已包括在内)来检查是否所有部分都已正确组合在一起。本系列的每一部分都建立在其它部分的基础之上,所以如果您还没有看过第一和第二部分,您也许会看不懂本文中的一些描述。如果要回顾专门的词汇表,请参阅术语解释侧栏。

    使用第一部分中为 WebServiceConfiguration 定义的 XML 模式(请参阅更新版本)和第二部分中的接口,即将创建为配置数据的特定实例提供数据的 XML 文档。任何符合模式的 XML 文档都可以编组成 Java 对象。这些对象应该是使用 SchemaMapper 类生成的类的实例。当然,最终结果就是数据绑定。

    制作 XML 实例文档
    创建符合模式的 XML 文档 -- 通常叫做 XML 实例 -- 很简单。文档必须只提供与模式中定义的约束相匹配的数据值,如清单 1 所示。

    清单 1. 符合示例 XML 模式的 XML 实例文档 <?xml version="1.0"?>

    <webServiceConfiguration xmlns="http://www.enhydra.org"
    牋牋牋牋牋牋牋牋牋牋牋牋 xmlns:xsi="http://www.w3.org/1999/XMLSchema/instance"
    牋牋牋牋牋牋牋牋牋牋牋牋 xsi:schemaLocation="http://www.enhydra.org
    牋牋牋牋牋牋牋牋牋牋牋牋牋牋牋牋牋牋牋牋牋牋 configuration.xsd"
    牋牋牋牋牋牋牋牋牋牋牋牋 version="1.1"
    牋牋牋牋牋牋牋牋牋牋牋牋 name="Unsecured Web Listener"
    >

    _<port number="80"
    牋牋牋_protocol="http"
    牋牋牋_protectedPort="false"
    _/>

    _<document root="/usr/local/enhydra/html"
    牋牋牋牋牋_index="*.html,*.xml"
    牋牋牋牋牋_error="error.html"
    _/>

    </webServiceConfiguration>


    清单 1 中的示例完整地显示了 WebServiceConfiguration 的实例。实例文档包括了两个名称空间声明。第一个是缺省名称空间声明,请参考 http://www.enhydra.org。这表示所有没有前缀的元素会分配到此名称空间。虽然,在本示例中不需要声明缺省名称空间,它还给予了文档一些身份。这个缺省名称空间有助于将该文档与其它有相似或等同元素名称的 XML 文档区分出来。

    定义的另一个名称空间分配给 xsi 前缀,所以带该前缀的所有元素都分配到此名称空间。它 (http://www.w3.org/1999/XMLSchema/instance) 引用“XML 模式实例规范”的 URI。该规范依次定义了 XML 文档如何引用文档符合的 XML 模式。最后,schemaLocation 属性引用 XML 模式。该属性的第一个变量是受到约束的名称空间(示例缺省名称空间,它包括文档中的每个元素)。第二个变量,用空格与第一个变量分开,引用 XML 模式的实际位置。本例中,模式 configuration.xsd 是一个本地文件,它与文档在同一个目录中。也可以通过使用 URL 来引用网络上任意位置的模式。

    在缺省名称空间中,附加属性(因为它们没有前缀)定义了版本 (1.1) 和名称 (Unsecured Web Listener)。

    接着,声明了模式中的 Port 对象,并定义了它的数据:端口号为 80,协议是 http。正确取消编组成 Java 代码后,该文档就变成了 WebServiceConfigurationImpl 类的实例。然后,Java 代码可以使用本系列第二部分中设计的接口 WebServiceConfiguration,以使用基本 XML 文档中的数据。(请注意,可能会在应用程序中执行验证,如模式验证侧栏中所概述的。)

    模式验证
    较新的 XML 语法分析器,如 Apache Xerces 语法分析器的当前发行版,允许对 XML 实例文档执行模式验证。验证允许在程序格式上确保 XML 文档符合它引用的 XML 模式。请与语法分析器供应商联系或参考文档,以确定语法分析器是否支持模式验证,其验证范围,以及如何打开验证。
    打开前门
    正式开始之前,需要提供入口点以取消编组 XML 文档,该文档作为返回 Java 对象的方法的输入。(由于您会忆起,本例中取消编组的结果只是 Java 对象。)然后,该对象可以转换成适当的接口,其实,您已经生成了该接口(在本系列第二部分中)。

    对于示例 SchemaMapper 类,允许传入 URL 是最有意义的。由于可以使用网络资源作为输入,而不是只允许文件名,这就提供了更多选择。知道了这一点后,下一步就从 URL 创建 JDOM 文档对象 (org.jdom.Document),然后处理文档。请查看清单 2 中执行该操作的代码。

    清单 2. 将字符串映射成 Java 指定的类型 /**
          *
    * This method is the public entry point for unmarshalling an object from * an XML instance document. *

    * * @param instanceURL URL for the instance document. * @return Object - the created Java Object, or * null if problems occur in a way that does not * generate an Exception. * @throws IOException when errors in binding occur. */ public static Object unmarshall(URL instanceURL) throws IOException { // Read in the document SAXBuilder builder = new SAXBuilder(); try { Document doc = builder.build(instanceURL); Element rootElement = doc.getRootElement(); Unmarshaller unmarshaller = new Unmarshaller(); return unmarshaller.getJavaRepresentation(rootElement); } catch (JDOMException e) { throw new IOException (e.getMessage()); } }


    清单 2 中的方法是静态的,允许直接调用它而无需实例化类的实例。由于对 unmarshall 方法的多个调用之间没有需要共享的数据,因此该方法可以是静态的。一旦处理了 XML,就将文档的根元素(以 JDOM 表示)就被传到执行从 XML 到 Java 对象转换的内部方法。

    转换数据
    我不打算逐行解释取消编组中使用的完整代码。可以查看类的完整源码清单,它基本上是不需加以说明的。但是,在入口点示例中,有一些值得强调的事情。如果创建了适当类的新实例,将使用 XML 文档提供的值调用 mutator 方法(全都名为 setXXX)。当然,这将使 XML 数据在实例的 Java 方法中随处都可用。清单 3 显示了处理这种查找方法以及随后调用的代码片段。

    清单 3. unmarshaller 类的入口点    // For each attribute, get its name and call mutator
                 List attributes = rootElement.getAttributes();
                 Method[] methods = objectClass.getMethods();

                 for (Iterator i = attributes.iterator(); i.hasNext(); ) {
                     Attribute att = (Attribute)i.next();

                     // Only want attributes for this namespace
                     if ((!att.getNamespace().equals(ns)) &&
                         (!att.getNamespace().equals(Namespace.NO_NAMESPACE))) {
                         continue;
                     }

                     // Determine method to call
                     String methodName = new StringBuffer()
                         .append("set")
                         .append(BindingUtils.initialCaps(att.getName()))
                         .toString();

                     // Find the method to call, and its parameter type
                     for (int j=0; j


    找到了根元素的属性,并确定了每个属性的适用方法。然后,就是处理实际的 java.lang.reflect.Method 对象。XML 属性的值已确定,并作为调用的参数传送到方法。但是,需要解决一个映射问题;XML 文档中的所有数据都作为 String 抽取,但传递时必须是适当的 Java 类型。清单 4 将一个方法添加到 DataMapping 辅助类中,以满足转换的需要。

    清单 4 将字符串映射成 Java 特定的类型     /**
          *
    * This will take the String value supplied and convert it * to an Object of the type specified in paramType. *

    * * @param value String value to convert. * @param paramType Class with type to convert to. * @return Object - value in correct type. */ public static Object getParameter(String value, Class paramType) { Object ob = null; String type = paramType.getName(); if (type.equals("java.lang.String")) { ob = value; } else if ((type.equals("int")) || (type.equals("java.lang.Integer"))) { ob = Integer.valueOf(value); } else if ((type.equals("long")) || (type.equals("java.lang.Long"))) { ob = Long.valueOf(value); } else if ((type.equals("float")) || (type.equals("java.lang.Float"))) { ob = Float.valueOf(value); } else if ((type.equals("double")) || (type.equals("java.lang.Double"))) { ob = Double.valueOf(value); } else if ((type.equals("boolean")) || (type.equals("java.lang.Boolean"))) { ob = Boolean.valueOf(value); } return ob; }


    在清单 4 中,值作为 String 传入,并且还传入了要转换的类和处理类型转换的方法。当然,这里包含的数据类型不多。可以添加更多类型(如 java.util.Date)来支持更复杂的数据映射。

    一旦数据转换成适当的类型,可以使用反射调用 accessor 方法,并可传入已转换的数据类型。这就使 XML 文档中的所有属性及其值可以在作为结果的 Java 实例中以方法变量和的值存储。

    递归对象树
    所剩下的将是生成嵌套对象(如 WebServiceConfiguration 对象中的 PortType 对象)。最后,将嵌套对象传递给 accessor 方法,然后将填充了成员变量值和对象引用的顶级对象返回给调用程序。这种方式生成了一棵对象树,其中主对象是该树的主干。每个嵌套对象都形成了本身必须填充的树的分枝。这些分枝可以有它们自己的分枝,带有本身必须填充的的嵌套对象。由此可知,这棵树可能变得非常复杂。

    在如果同一操作必须发生不知多少次的情况下,递归几乎总是完成操作的最佳选择。如果是 unmarshaller 类,则需要在将 XML 绑定到 Java 的完整过程上递归。一旦读取了所有属性并将它们分配给已创建的 Java 实例,就需要取出每个嵌套元素,然后再次执行取消编组。

    通过迭代所提供根的子元素,来完成 XML 文档的处理,如清单 5 所示。这些子元素中的每一个都将成为另一个对象,这就表示必须以该元素作为根元素重新开始取消编组过程。

    清单 5. 用递归处理嵌套元素 // Now do complex objects
                 List elements = rootElement.getChildren();

                 for (Iterator i = elements.iterator(); i.hasNext(); ) {
                     Element element = (Element)i.next();

                     // Only want elements for this namespace
                     if ((!element.getNamespace().equals(ns)) &&

                     (!element.getNamespace().equals(Namespace.NO_NAMESPACE))) {
                         continue;
                     }

                     // Determine method to call
                     String methodName = new
                     StringBuffer()
                         .append("set")
                         .append(BindingUtils.initialCaps(element.getName()))
                         .toString();

                     // Find the method to call, and its parameter type
                     for (int j=0; j<methods.length; j++) {
                         if (methods[j].getName().equals(methodName)) {
                             // Since all mutators have one param, get the first one
                             Class[] paramTypes =
                             methods[j].getParameterTypes();
                             Class paramType = paramTypes[0];


                             // Convert the type we have to the correct type
                             Object param = getJavaRepresentation(element);

                             // Invoke the method
                             methods[j].invoke(obj, new Object[] { param });
                         }
                     }
                 }


    注:您也许注意到我在清单 5 中的取消编组中做了一个假设,即成员变量总是由 XML 属性表示,嵌套对象由 XML 元素表示。那么,这些元素可能有自己的属性和嵌套元素。我的假设是唯一设置的限制,并且是合理的。这意味着取消编组过程不必查看引用的 XML 模式并确定特性是由元素表示,还是由属性表示。这也会使编组过程变得更简单,将在本系列的下一篇文章中出现。如果所有这一切对您没有难度,那么只使用属性作为变量,元素作为对象,不必考虑嵌套和递归。

    清单 5 中的代码看来很像上一段代码,其主要区别是用红色强调的几行。这段代码通过取消编组嵌套元素来获取参数,而不是使用 DataMapping 辅助类将文本值转换成 Java 数据类型。然后,返回的对象提供给适当的 mutator 方法(例如,setPort),迭代继续进行。一旦递归从底层到顶层解开,则创建的 Java 对象将返回到调用应用程序。很快吗!数据绑定完成了。

    通过使用运行的 unmarshaller 类,实际上,它最终使用了数据绑定工具。通过使用 XML 模式、XML 文档和一些简单的 Java 代码,访问 XML 就象访问 JavaBean 一样简单。

    生成类
    首先,确保已经从 XML 模式生成了 Java 类,如清单 6 所示。

    清单 6. 从示例模式生成 Java 类 /projects/dev/binding> export CLASSPATH=/projects/dev/jdom/lib/xerces.jar
    /projects/dev/binding> export CLASSPATH=$CLASSPATH:/projects/dev/jdom/build/jdom.jar
    /projects/dev/binding> export CLASSPATH=$CLASSPATH:/projects/dev/binding

    /projects/dev/binding> java org.enhydra.xml.binding.SchemaMapper xml/configuration.xsd

    /projects/dev/binding> javac -d . *.java


    使用 unmarshaller
    如果已经从 XML 模式生成了类,并经过编译,则可以继续。作为确保类是否工作的简单测试,可以使用清单 7 中的类测试数据绑定的功能性(或下载这个类)。

    清单 7. 数据绑定测试类  import java.io.File;
    import org.enhydra.xml.binding.unmarshaller;

    public class TestMapper {

         public static void main(String[] args) {
             System.out.println("Starting unmarshalling...");
       try {
                 File file = new File("xml/example.xml");
                 Object o = unmarshaller.unmarshall(file.toURL());
                 System.out.println("Object class: " + o.getClass().getName());   

                 System.out.println("Casting to WebServiceConfiguration...");
                 WebServiceConfiguration config = (WebServiceConfiguration)o;
                 System.out.println("Successful cast.");

                 System.out.println("Name: " + config.getName());
                 System.out.println("Version: " + config.getVersion());

                 System.out.println("Port Number: " + config.getPort().getNumber());
                 System.out.println("Port Protocol: " + config.getPort().getProtocol());
       } catch (Exception e) {
                 e.printStackTrace();
             }
         }
    }


    编译和运行该数据绑定测试类以查看结果,如清单 8 所示。

    清单 8. 测试 unmarshaller  /projects/dev/binding> javac -d . TestMapper.java

    /projects/dev/binding> java TestMapper
    Starting unmarshalling...
    Object class: WebServiceConfigurationImpl
    Casting to WebServiceConfiguration...
    Successful cast.
    Name: Unsecured Web Listener
    Version: 1.1
    Port Number: 80
    Port Protocol: http


    启动 Web 服务
    作为一个更实用的示例,让我们回顾已经在几篇文章中提到的 Web 服务示例。假设有一些可以编程启动的 Java 类(叫做 WebService),那么可简单地使用数据绑定来获取该类的配置信息。现在,从一个 XML 文档(或者甚至几个)中读取和启动新的 Web 侦听程序是非常容易的事 -- 这不需要任何 XML 特定 API 的知识,如清单 9 所示。将配置数据取消编组成 Java 对象,然后使用标准 Java accessor 方法(通常是 getXXX() 格式)来配置新的 Web 服务。

    清单 9. XML 到 Web 侦听程序 // Assume we have a List of URLs
         for (Iterator i = urls.iterator(); i.hasNext(); ) {
             WebServiceConfiguration config = unmarshaller.unmarshal((URL)i.next());

             WebService newService = new WebService();
             newService.setName(config.getName());

             // Set up port information
             newService.setPortNumber(config.getPort().getNumber());
             newService.setProtocol(config.getPort().getProtocol());

             // Set up document root
             newService.setDocRoot(config.getDocument().getRoot());
             newService.setErrorPage(config.getDocument().getError());

             newService.start();
         }


    就那么简单,即使是初级开发者也能写出使用这个简单 XML 文档及其数据的 Java 程序,而他甚至还不知道正在使用 XML!有关 XML 数据绑定代码的更多用法,请关注 Enhydra 应用服务器即将推出的新版本,在未来的发行版中将包含这里讨论的数据绑定类(并将在下一篇文章中继续讨论)。完成了 unmarshaller 的代码之后,就可以讨论最终细节了。

    跟上不断发展的 API
    就在一个月之前,我们看到 SchemaMapper 类,它从 XML 模式生成 Java 接口和实现。该代码很大程度地使用了 JDOM API(主要是因为它很方便,是我编写的!)。然而 30 天时间只够进行一届曲棍球季后赛,对于 API,如仍在开发中的 JDOM,却几乎是一生一世。自上一篇文章以来,有几个更改已经在 JDOM API 中生效了,大多数反映了一些更新的方法名。有关更改及其原因的详细信息,请访问 JDOM 网站(请参阅参考资料),可以在该网站上加入 JDOM-兴趣邮件列表。但是,为了帮助您使用最新和最好的版本,SchemaMapper 类再次出现在因特网上,并且已更新成使用最新版本的 JDOM(直接来自 CVS)。还可以下载源码。强烈建议从 CVS 获取最新的 JDOM,并使用更新版本的代码。(在第四部分到来之前,可能仍有更多更改。)

    JSR-031,数据绑定 API,在 Java 社区中仍是处在争论和测试过程的建议书。在这个过程中,它还可能做一些更改。尽管它还未成熟,至今为止许多使用 XML 的 Java 开发者还是会使用它,因为它是执行非常有用功能的方法。

    结束语
    通过使用本系列这部分中新的详细信息,可以使用数据绑定代码。使用 unmarshaller 类,就可以在 Java 代码中方便地使用 XML 文档,而不必直接借助于 XML API,如 DOM、SAX 或 JDOM。虽然示例建议使用数据绑定处理配置文件,您也许已经有了在应用程序中使用数据绑定的其它想法。也可以使用数据绑定代码来进行消息传递、数据存储和显示等等。

    本系列的第四篇,也就是最后一篇文章将主要讲述编组,即利用 Marshaller 类得到 Java 类,并将它转换成 XML 文档。该文章将讨论转换原来经过取消编组的 Java 对象,以及未经过取消编组的 Java 对象。到那时,希望您喜欢迄今为止出现的数据绑定代码,下次再见。

    术语解释
    数据绑定。一种使用 JSP-031 访问 Java 中 XML 数据的新方法。JSP-031 是一个仍在开发中的 API。
    显式类型。具有类型属性的 complexType 元素。模式中的显式类型成为生成的 Java 代码中的接口名称。
    隐式类型。不具有类型属性的 complexType 元素。这种情况下,接口名称由 SchemaMapper 生成。
    JSR-031。Sun 公司仍在开发中的一种新的 Java 规范申请。它用于将 XML 文档编译成一个或多个 Java 类,而在 Java 应用程序中可以方便地使用这些 Java 类。
    编组。 将 Java 对象转换为 XML 表示,拥有当前值。
    取消编组。 根据 XML 对象创建 Java 对象,通常是根据编组生成一个 Java 对象。
    参考资料

    钻研数据绑定并回顾数据绑定系列的第一部分和第二部分。
    下载 zip 文件,其中包含本文中示例的代码,包括 unMarshaller 类。
    从 David Megginson 的 SAX 页面中下载 XML 的简单 API (SAX) 的版本 2.0,它是用于读取 XML 的基于事件的 Java API。
    了解 DOM 的最新状态,在 W3C DOM 工作组的官方页面上,文档对象模型 (DOM) 是一种基于树的 API,用于在 Java 中读写 XML。
    下载 JDOM,并了解 JDOM 项目,JDOM 是一种 API,该 API 使从 Java 中使用 XML 变得更简单和直观。
    请阅读 JSR-031:数据绑定,它是 Sun 数据绑定规范申请。
    下载 Enhydra,它是一种开放源码 Java 应用服务器。
    对 XML 文档进行语法分析,然后用 Apache Xerces XML 语法分析器检查其模式有效性。
    了解 XML 名称空间的详细信息。
    想要了解 Brett 关于 Java 和 XML 的思路吗?请订购 Java 和 XML,由 O'Reilly 出版,是 Brett 关于该主题的著作。
    Java 语言开发人员,请加入 developerWorks XML 工具和 API 新闻组。
    需要关于 XML 的更多基本介绍吗?尝试 developerWorks 对 XML 的介绍教程和其它教材,其中涉及了最基本的主题。
    一些商业工具也从 XML 对象执行数据绑定。它们都基于专有技术;JSR-031 API (aka Adelard) 仍是一个非公开的工作草案,因此没有商业产品可以包含它。请查看 KaanBaan,它在“XML 事务服务器”中执行动态数据绑定,还请查看 Breeze XML Studio。

    关于作者
    Brett McLaughlin (brett@newInstance.com) 是 Lutris Technologies 的 Enhydra 策略顾问,他致力于研究分布式系统体系结构。他是 Java 和 XML (O'Reilly) 的作者。他还参与了如 Java servlet、Enterprise JavaBeans 技术、XML 和商家对商家应用程序等技术的研究。最近,他与 Jason Hunter 一起创立了 JDOM 项目,该项目为在 Java 应用程序中操纵 XML 提供了一个简单的 API。他还是 Apache Cocoon 项目、EJBoss EJB 服务器的开发人员,并且是 Apache Turbine 项目的共同创始人


       收藏   分享  
    顶(0)
      




    点击查看用户来源及管理<br>发贴IP:*.*.*.* 2004/7/9 16:08:00
     
     GoogleAdSense
      
      
      等级:大一新生
      文章:1
      积分:50
      门派:无门无派
      院校:未填写
      注册:2007-01-01
    给Google AdSense发送一个短消息 把Google AdSense加入好友 查看Google AdSense的个人资料 搜索Google AdSense在『 Java/Eclipse 』的所有贴子 访问Google AdSense的主页 引用回复这个贴子 回复这个贴子 查看Google AdSense的博客广告
    2024/5/11 18:27:06

    本主题贴数1,分页: [1]

    管理选项修改tag | 锁定 | 解锁 | 提升 | 删除 | 移动 | 固顶 | 总固顶 | 奖励 | 惩罚 | 发布公告
    W3C Contributing Supporter! W 3 C h i n a ( since 2003 ) 旗 下 站 点
    苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
    78.125ms