了解如何在 IBM® WebSphere® Enterprise Service Bus Version 6.0.1 中实现 Web 服务(SOAP/HTTP 和 SOAP/JMS)的运行时动态路由。
引言
使用 IBM WebSphere Enterprise Service Bus(以下称为 ESB)的集成开发人员可以创建中介以支持服务请求者和服务提供者之间的智能互连。在 Websphere ESB 中,中介模块提供了服务虚拟化机制,用来将请求者从接口、协议和提供者的标识中隔离出来。
标识虚拟化意味着,请求者把中介模块或中介作为提供者,而中介则可以将该请求路由到“真正的”提供者。对于通过 Web 服务进行交互的请求者和提供者,集成开发人员可以在开发期间使用 ESB 来标识真正的提供者的端点地址。然而,目前 ESB 没有为运行时更改端点地址提供一种简便的方式。
为什么这一点很重要呢?在许多情况下,中介必须在运行的时候,而不是在开发期间或部署期间,进行对真正的提供者的选择。例如,可以考虑使用从服务注册中心获取的信息来实现对真正的提供者的选择。在这种情况下,中介向注册中心进行查询,并希望注册中心为它提供包括满足查询要求的提供者端点地址的服务元数据。作为一种可选的方法,中介可能希望注册中心为它提供包括满足查询要求的端点地址的服务元数据列表,然后使用附加的信息,可能来自于请求本身,以选择合适的服务和端点地址。在这种情况下,开发期间所使用的端点地址仅仅只是一个占位符,而很少被使用到。
在本文中,您将了解如何避开目前 ESB 中的各种限制,并实现 Web 服务的运行时动态路由。首先,我们将描述构建仅支持静态或开发期间路由的 ESB 中介的常规技术,然后介绍用户实现运行时动态路由的技术。该方法使用替换了 ESB 中介框架中某些内置操作的定制中介元素。请注意,要使用本文中所介绍的技术,您必须使用 WebSphere ESB V6.0.1.1(或 WebSphere Process Server V6.0.1.1,其中包含了 ESB)。
SOAP/HTTP 的静态路由
本部分简要地介绍了如何构造仅支持静态路由的 ESB 中介模块。本部分假设您已经熟悉那些使用 WebSphere Integration Developer 开发 ESB 中介模块中所涉及的基本操作。请查看 “WebSphere Enterprise Service Bus 与 WebSphere Integration Developer 入门”,以获取关于使用 Integration Developer 进行中介模块开发的非常棒的介绍。
清单 1 显示了作为真正提供者的 Web 服务 (SOAP/HTTP) 的 WSDL,该提供者仅仅回显了一个复杂的数据结构。这个 WSDL 文件位于称为 Resources 的 Business Integration Library 项目中,该项目使得更容易从 Integration Developer 工作区的多个项目中引用该 WSDL 文件。
清单 1. BigEcho 服务定义
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions targetNamespace="http://big.com"
xmlns:impl="http://big.com"
xmlns:intf="http://big.com" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:wsi="http://ws-i.org/profiles/basic/1.1/xsd"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<wsdl:types>
<schema targetNamespace="http://big.com"
xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:impl="http://big.com" xmlns:intf="http://big.com"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<complexType name="BEData">
<sequence>
<element name="one" nillable="true" type="xsd:string"/>
<element name="two" nillable="true" type="xsd:string"/>
</sequence>
</complexType>
<element name="echoResponse">
<complexType>
<sequence>
<element name="echoReturn" nillable="true" type="impl:BEData"/>
</sequence>
</complexType>
</element>
<element name="echo">
<complexType>
<sequence>
<element name="d" nillable="true" type="impl:BEData"/>
</sequence>
</complexType>
</element>
</schema>
</wsdl:types>
<wsdl:message name="echoRequest">
<wsdl:part element="impl:echo" name="parameters"/>
</wsdl:message>
<wsdl:message name="echoResponse">
<wsdl:part element="impl:echoResponse" name="parameters"/>
</wsdl:message>
<wsdl:portType name="BigEcho">
<wsdl:operation name="echo">
<wsdl:input message="impl:echoRequest" name="echoRequest"/>
<wsdl:output message="impl:echoResponse" name="echoResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="BigEchoSoapBinding" type="impl:BigEcho">
<wsdlsoap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="echo">
<wsdlsoap:operation soapAction=""/>
<wsdl:input name="echoRequest">
<wsdlsoap:body use="literal"/>
</wsdl:input>
<wsdl:output name="echoResponse">
<wsdlsoap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="BigEchoService">
<wsdl:port binding="impl:BigEchoSoapBinding" name="BigEcho">
<wsdlsoap:address location="http://localhost:9081/BigEcho/services/BigEcho"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
|
图 1 显示了将该 WSDL 文件复制到 Resources Library 项目之后的 Business Integration Perspective 视图。请注意其中出现的端口(名为 BigEcho)、接口(WSDL 端口类型,也称为 BigEcho)和接口中使用的数据类型 (BEData)。
图 1. Library 中的 BigEcho
使用“WebSphere Enterprise Service Bus 与 WebSphere Integration Developer 入门”中所介绍的技术,创建在其依赖项中引用这个 Resources Library 的名为 StaticRoute 的中介模块。StaticRoute 具有一项导出、一项导入和相应的中介组件。图 2 显示了进行下列操作后得到的 Integration Developer 组装图:
- 创建包含一个中介组件的中介模块,该中介组件被自动地命名为 Mediation1。
- 向这个中介模块添加一项导入和导出,它们分别被自动命名为 Import1 和 Export1。
- 将 BigEcho 接口指定为 Import1 和 Export1。
- 为 Export1 生成 Web 服务绑定 (HTTP)。
- 为 Import1 生成 Web 服务绑定 (HTTP)。
- 为 Import1 的绑定选择 BigEcho.wsdl。
- 将 Export1 连接到 Mediation1,并将 Mediation1 连接到 Import1。
图 2. StaticRoute 中介模块组装
图 3 显示了 Import1 的绑定信息。您可以从清单 1 的 BigEcho WSDL 中看到相应的信息确认。
图 3. Import1 绑定
图 4 显示了在 Mediation Flow Editor 中的中介模块的操作映射,这在本示例中非常简单:导出上的 echo 操作映射到导入上的 echo 操作。
图 4. StaticRoute 的操作映射
图 5 显示了简单的请求流,其中 Input 节点(表示来自 Export1 的请求)连接到 Callout 节点(表示对 Import1 的请求)。这种安排方式将传入请求直接从导出传递到导入。如果需要对请求进行任何附加的处理,如进行转换,那么需要将用来执行这项处理的中介元素连接到 Input 和 Callout 之间。
图 5. StaticRoute 请求流
图 6 显示了简单的响应流,其中 CalloutResponse 节点(表示来自 Import1 的响应)连接到 InputResponse 节点(表示对 Import1 的响应)。这种安排方式将传入响应直接从导入传递到导出。如果需要对响应进行任何附加的处理,如进行转换,那么需要将用来执行这项处理的中介元素连接到 CalloutResponse 和 InputResponse 之间。
图 6. StaticRoute 响应流
在对中介进行了保存并将其部署到测试服务器(ESB 或 WebSphere Process Server)之后,便可以对该中介模块进行测试。我们使用一个简单的 Java™ 客户端来调用从为 Export1 创建的 WSDL 文件生成的代理。
要从 Business Integration Perspective 生成一个代理,请进行以下操作:
- 打开 Physical Resource 视图并展开 StaticRoute 项目,如图 7 所示。
- 右键单击接口 Export1_BigEchoHttp_Service.wsdl(它将该中介模块描述为 Web 服务,并在创建该中介模块时自动生成),然后选择 Web Services => Generate Client
- 将客户端代理放入合适的项目中。我们创建了一个调用该代理的简单 Java 客户端,而这个代理将调用中介模块。
图 7. StaticRoute 的 Physical Resources 视图
我们所使用的 BigEcho 服务实现与中介处于相同的测试环境,所以在测试环境控制台中显示了消息“+++ Got to BigEcho”,这表示中介模块对 BigEcho 服务进行了调用。
SOAP/HTTP 的动态路由
可以考虑一下,如果您需要更改 StaticRoute 中介所使用的提供者时,需要进行什么操作。最明显的解决方案是在运行期间更改 Import1 所使用的端点地址。遗憾的是,当前版本的 ESB 并不支持这种操作。您可以修改 StaticRoute 中介并添加第二项导入,以提供一个可选的提供者,而对导入所使用的提供者的选择将在运行时进行。对于某些情况,在开发期间添加新的可选的提供者是可行的,然而并不是对所有的情况都有效,比如对于实际提供者端点地址需要从服务注册中心获取的情况,如 UDDI 的实现。
我们将使用包含自定义中介元素的中介来实现动态路由。“Developing custom mediations for WebSphere Enterprise Service Bus”描述了使用自定义中介元素的基本技巧。我们将创建一个在其依赖项中引用 Resources library 的 DynamicRoute 中介模块。DynamicRoute 具有一项导出、一项导入和相应的中介组件。在完成下列操作后:
- 创建包含一个中介组件的中介模块,该中介组件被自动地命名为 Mediation1
- 向这个中介模块添加一项导入和导出,它们分别被自动命名为 Import1 和 Export1
- 将 BigEcho 接口指定为 Import1 和 Export1
- 为 Export1 生成 Web 服务绑定 (HTTP)
- 为 Import1 生成 Web 服务绑定 (HTTP)
- 为 Import1 的绑定选择 BigEcho.wsdl
- 将 Export1 连接到 Mediation1,并将 Mediation1 连接到 Import1
您会看到其组装图与图 2 中所示的 StaticRoute 中介的组装图非常相似。
DynamicRoute 中介模块的操作映射,同样非常简单,与图 4 中所示的 StaticRoute 中介的操作映射相同。简单的响应流,其中 CalloutResponse 连接到 InputResponse,这也与图 6 中 StaticRoute 中介的响应流相同。而显著的区别在于请求流,我们在其中引入了自定义中介元素,如图 8 所示。
图 8. DynamicRoute 请求流
我们插入了一个名为 Router 的自定义中介元素,并按预期将 Input 节点连接到其输入终端。然而请注意,将 Router 的输出终端连接到了 InputResponse 节点,而不是 Callout 节点。这意味着请求流向 Router,而不是经过它流向 Import1。相反,Router 必须调用 Import1 所表示的 Web 服务。而且,因为没有使用 Import1,响应无法通过 Import1 返回到响应流,所以并没有使用响应流。相反,Router 将它接收到的响应经过 InputResponse 节点发送到 Export1。
- 选择 Router 中介元素并显示其属性。
- 单击 Details 选项卡,并单击 Define。
- 在 Define Custom Mediation 向导的 Define Custom Mediation 对话框中,单击 Next。
- 在 Specify Message Types 对话框中,确保将 Message Root 设置为 /,然后单击 Next。使用 / 作为消息根节点,使得中介元素可以看到整个 Service Message Object,包括任何上下文和 Header。
- 在 Create a new interface 对话框中,单击 Next。
- 在 Generate Java Implementation 对话框中,单击 Finish,并保存该中介流。
- 在保存了中介流之后,返回到组装图并对它进行保存。
- 右键单击 Mediation1 并选择 Merge Implementation
- 在所得到的对话框中单击 OK 以合并实现。您将看到 Merge Implementation 向导,如图 9 所示。
图 9. Merge Implementation 向导
- 确保选中 Create Java Component 下面的复选框,然后单击 OK。
- 这时显示了 Mediation Flow Editor。返回组装图并保存该中介。
- 在主菜单中,单击 Project => Clean。
- 在弹出对话框中,单击 OK。现在,组装图中的中介应该与图 10 所示类似。
图 10. 表示自定义中介的中介
- CustomMediation1Partner 表示 Router 自定义中介元素。现在,需要将 CustomMediation1Partner 连接到 Import。在第一个 Add Wire 对话框中,单击 Yes 以创建一个匹配的接口。
- 在第二个 Add Wire 对话框中,如图 11 所示,单击 No。这样通过避免在 Java 对象和 Service Message Object 之间进行映射,从而简化了 Router 的任务。
图 11. Add Wire 对话框
- 其组装图应与图 12 所示类似。保存该中介模块。
通过自定义中介元素进行路由的基本要求已经满足。现在,让我们来看看如何实现类似前面所讨论的静态路由,然后我们将为创建动态路由完成所需的附加操作。
图 12. 自定义中介连接到导入的中介
- 返回 Mediation Flow Editor。
- 选择 Router 元素并查看其属性。
- 选择 Implementation 选项卡。
- 单击 Open Java Editor 并在所得到的对话框中单击 OK。
- 滚动到
execute() 方法。
- 现在您可以输入允许 Router 中介元素调用 Import1 所表示的 Web 服务的代码。它使用 Service Component Architecture 框架 (SCA),而 ESB 正是建立于该框架的基础上。清单 2 显示了所需的代码。您需要清楚,DataObject 输入参数 (input1) 是整个 SMO,这样该中介元素可以看到上下文、Header 和正文。与之类似,DataObject 返回的必须是整个 SMO。当然,这个输入参数包含了来自于原始请求者的输入消息或负载,而返回参数则包含了来自于提供者的响应。
清单 2. 在具有静态地址的自定义中介元素中进行服务调用
public DataObject execute(DataObject input1) {
System.out.println("... In Router mediation primitive");
// (1) Create endpoint reference
EndpointReference eRef = EndpointReferenceFactory.INSTANCE
.createEndpointReference();
eRef.setAddress("http://localhost:9081/BigEcho/services/BigEcho");
// (2) invoke service and extract response payload
Service echoService = (Service)
ServiceManager.INSTANCE.getService("BigEchoPartner", eRef);
DataObject request = input1.getDataObject("body/echo");
DataObject response = (DataObject) echoService.invoke("echo",
request);
DataObject responseBody = response.getDataObject("echoReturn");
// (3) create response message and response
BOFactory boFactory = (BOFactory) ServiceManager.INSTANCE
.locateService("com/ibm/websphere/bo/BOFactory");
DataObject message = boFactory.createByMessage("http://big.com",
"echoResponse");
DataObject myRes = boFactory.createByElement("http://big.com",
"echoResponse");
// (4) add response and return
myRes.setDataObject("echoReturn", responseBody);
message.setDataObject("parameters", myRes);
input1.setDataObject("body", theMes);
return input1;
}
|
清单 2 中的代码段 (1) 创建了一个端点引用。将端点引用中的地址硬编码为 BigEcho 服务,如清单 1 中的 WSDL 文件所示。
代码段 (2) 根据端点引用中的地址获取 BigEcho 服务代理。请注意在图 4 中,使用符号 BigEchoPartner 来表示导入。在查找的过程中需要使用导入的符号名称。CustomMediation1Partner 和 Import1 之间的连接允许 Router 在查找中顺利地使用符号名。接下来的代码段提取了请求消息的负载。因为传入的消息是整个 SMO,所以使用 XPath 表达式 body/echo 来访问请求负载。然后,代码段使用负载来调用相应的服务。最后,它提取了响应负载。
代码段 (3) 创建了一个响应消息,并从清单 1 中 WSDL 的适当的元素创建了一个响应。
最后,代码段 (4) 将原始响应的负载插入到响应消息结构中。请注意,然后代码段使用响应消息替换了传入 SMO 中的请求消息。这样允许任何 Header 或上下文流经 Router 中介元素。然后,代码段返回响应,如前所述,该响应流向 InputResponse 节点。
- 保存清单 2 中所示的代码。
- 保存中介流和中介模块,然后再次清除。
- 部署这个中介模块。
- 要测试 DynamicRoute 中介模块,您可以使用与测试 StaticRoute 相同的测试客户端,只需要将测试客户端中所使用的代理的端点地址从指向 StaticRoute 修改为指向 DynamicRoute。例如,如果 StaticRoute 的端点地址为 http://localhost:9080/StaticRouteWeb/sca/Export1,那么 DynamicRoute 的端点地址为 http://localhost:9080/DynamicRouteWeb/sca/Export1。运行该测试客户端。如果检查服务器控制台视图,您将看到调用了中介模块和 BigEcho 服务,如清单 3 所示。
清单 3. 调用中介和服务后的控制台输出
SystemOut O ... in Router mediation primitive
SystemOut O +++ Got to BigEcho.
|
SOAP/HTTP 的增强动态路由
前面描述的方法假设实际的端点地址以某种方式从使用该端点地址的相同的自定义中介元素中获得。然而,这种方法并不具有很好的可重用性。让我们来看一个更具可重用性的动态路由方法。这个方法利用了 SMO 瞬态上下文,而该瞬态上下文是一种用于在中介元素之间进行通信的机制。有关更详细的信息,请查看 WebSphere Enterprise Service Bus 信息中心。我们将在请求流中引入另一个中介元素,该中介元素在瞬态上下文中设置了所需的端点地址。我们还将对 Router 中介元素进行修改,以便从瞬态上下文中提取相应的地址,并根据这个地址对服务提供者发出调用。
- 首先在 Resources Library 中创建一个名为 EPR 的新的业务对象,如图 13 所示。这个业务对象将定义所使用的瞬态上下文。这个 EPR 业务对象仅包含一个名为 address 的字段,并且我们使用了缺省命名空间。在更现实的实现中,该结构的定义可能建立在 WS-Addressing 规范的基础上。
图 13. 瞬态业务对象
- 打开 DynamicRoute 中介模块的 Mediation Flow Editor,在请求流中启用瞬态上下文。
- 选择 Request flow 选项卡。
- 选择 Input 节点并打开其属性。
- 选择 Details 选项卡。您将看到与图 14 所示类似的内容。
- 单击 Transient context 右边的 Browse。
- 在 Data Type Selection 对话框中,选择 EPR,然后单击 OK。Input 节点的详细信息中应该显示了瞬态上下文集,如图 14 所示。
图 14. 瞬态上下文集
- 重新启动 DynamicRoute 项目以确保使用了瞬态上下文机制。
- 下一步,添加用于设置上下文的另一个自定义中介元素。在 DynamicRoute 的 Mediation Flow Editor 中,删除 Input 节点和 Router 之间的连接。
- 添加一个新的中介元素,并将其命名为 Setter。
- 将 Input 节点连接到 Setter 输入终端,并将 Setter 输出终端连接到 Router 输入终端。
- 打开 Setter 属性,选择 Detail 选项卡并单击 Define。
- 与定义 Router 时相同,请确保将根元素设置为 /,然后单击 Finish。中介请求流应该与图 15 所示类似。
图 15. 带 Setter 节点的 DynamicRoute 请求流
- 保存该中介流,返回到组装图,按照前面的描述合并实现。
- 清除这个中介。您应该看到与图 16 所示类似的内容。CustomMediation2Partner 表示 Setter 自定义中介元素。
图 16. 带 Setter 和 Router 的 DynamicRoute
- 现在,您需要为 Setter 自定义中介元素创建实现,这个自定义中介元素在传递到 Setter 的瞬态上下文中指定了所需的端点地址。清单 4 显示了所需的代码。代码段 (1) 只是将端点地址设置为所需的值。XPath 表达式
context/transient/address 反映了 SMO 具有名为 context 的子节点,context 具有名为 transient 的子节点,并且因为我们已经声明了 transient 具有由 EPR 所定义的结构,所以 transient 具有名为 address 的包含所需的端点地址的子节点。在更实际的情况下,可能按照从服务注册中心获取的信息对该地址进行设置。
清单 4. 对瞬态上下文进行设置以支持动态路由
public DataObject execute(DataObject input1) {
System.out.println("... In Setter mediation primitive");
// (1) set EPR address
input1.setString("context/transient/address",
"http://localhost:9081/BigEcho/services/BigEcho");
return input1;
}
|
- 修改 Router 中介元素以从瞬态上下文中提取所需的端点地址,并且使用它作为服务调用的端点地址。清单 5 显示了在静态路由代码的基础上需要为 Router 自定义中介元素中进行的修改:
清单 5. 带动态地址的自定义中介元素中的服务调用
public DataObject execute(DataObject input1) {
System.out.println("... In Router mediation primitive");
// (0) extract address
String address = input1.getString("context/transient/address");
// (1') Create endpoint reference
EndpointReference eRef = EndpointReferenceFactory.INSTANCE
.createEndpointReference();
eRef.setAddress(address);
// (2) invoke service and extract response body
...
// (3) create response message and add response
...
// (4) add response and return
...
}
|
在清单 2 所示的静态形式的基础上添加的代码段 (0),从瞬态上下文中提取了所需的端点地址,XPath 表达式与用于向 Setter 插入端点地址的表达式相同。代码段 (1) 使用从瞬态上下文中提取的地址创建了端点引用。代码段 (2) 到 (4) 与清单 2 中的相同。
- 如果再次运行测试客户端,您应该看到类似清单 6 输出所示的内容。您将看到 Setter 中介元素设置了所需的端点地址,并且 Router 中介元素使用该地址来调用 BigEcho 服务。
清单 6. 使用增强的中介调用中介和服务后的控制台输出
SystemOut O ... In Setter mediation primitive
SystemOut O ... In Router mediation primitive
SystemOut O +++ Got to BigEcho.
|
这个示例说明,端点地址的选择和该地址的实际使用可以分开,这样便为动态路由提供了一种更加灵活并更具可重用性的方法。请注意,更现实的场景可能需要对请求或响应、或者请求和响应进行附加的处理。在图 15 所示的中介流中,任何请求处理必须发生在 Router 自定义中介元素之前,并且任何响应处理必须发生在 InputResponse 节点之前。
附加的绑定
您已经了解了如何为 SOAP/HTTP Web 服务绑定启用动态路由。您也可以对 SOAP/JMS Web 服务绑定使用同样的技术。然而,有几点需要说明的地方。首先,考虑 SOAP/JMS 服务的 SCA 模块,由导入和业务组件构成。我们发现,导出所使用的队列的名称和 Integration Developer 生成的 WSDL 文件的名称,仅从接口和模块中导出的名称获得。这意味着,当需要创建两个具有相同接口的模块时,您必须为导出指定不同的名称,否则队列和 WSDL 文件的名称将发生冲突,并且会带来不必要的结果。
第二点,我们发现绑定于模块中的导出的 WSDL 文件(或 Web 服务端口)必须对该模块是可用的。我们简单地将该信息复制到模块所引用的 Resources Library 中。
最后来看一下清单 7,其中包含了来自于为 SOAP/JMS 模块自动生成的 WSDL 的 service 元素,而 SOAP/JMS 模块包含了使用清单 1 中的 BigEcho 端口类型的名为 JS1Export1 的导出。请注意,wsdl:service 元素中的 wsdl:port 元素中的 soap:address 元素的 location 属性包含了一个列举队列(目标)名称、连接工厂名称和端口名称的值的 URI 字符串。字符串 & 用作这些值之间的分隔符。EndpointReference.setAddress() 方法(请参见清单 2 和清单 5)中使用的 URI 字符串必须使用 & 作为分隔符。
清单 7. SOAP/JMS Web 服务的 WSDL 代码段
<wsdl:service name="JS1Export1_BigEchoJmsService">
<wsdl:port name="JS1Export1_BigEchoJmsPort"
binding="this:JS1Export1_BigEchoJmsBinding">
<soap:address
location=
"jms:/queue?destination=jms/JS1Export1
&connectionFactory=jms/JS1Export1QCF
&targetService=JS1Export1_BigEchoJmsPort"
/>
</wsdl:port>
</wsdl:service>
|
这个技术也可以用于 SCA 绑定。对于 SCA 绑定,不会生成 WSDL 文件。请注意,创建端点引用所需的地址格式是 sca://<项目名称>/<导出名称>。
结束语
本文向您介绍了如何在 WebSphere ESB V6 中实现 Web 服务动态路由。在许多实际的企业服务总线场景中,这是至关重要的。本文还向您介绍了如何使用 SCA 框架和 ESB 中介上下文。所有的这些技术都可以通过 WebSphere ESB 来提高实际的 ESB 解决方案。
致谢
作者要感谢 Rob Phippen,他对 WebSphere ESB 中的动态路由进行了数小时的讨论,而正是在此基础上才得出了本文中所描述的这些技术。 |