(window.webpackJsonp=window.webpackJsonp||[]).push([[710],{1144:function(e,a,t){"use strict";t.r(a);var r=t(56),s=Object(r.a)({},(function(){var e=this,a=e.$createElement,t=e._self._c||a;return t("ContentSlotsDistributor",{attrs:{"slot-key":e.$parent.slotKey}},[t("h1",{attrs:{id:"spring-web-服务参考文档"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#spring-web-服务参考文档"}},[e._v("#")]),e._v(" Spring Web 服务参考文档")]),e._v(" "),t("h2",{attrs:{id:"序言"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#序言"}},[e._v("#")]),e._v(" 序言")]),e._v(" "),t("p",[e._v("在当前面向服务的体系结构时代,越来越多的人使用 Web 服务来连接以前未连接的系统。最初,Web 服务被认为是执行远程过程调用的另一种方式。然而,随着时间的推移,人们发现 RPC 和 Web 服务之间存在很大的差异。特别是当与其他平台的互操作性很重要时,通常更好的做法是发送封装的 XML 文档,其中包含处理请求所需的所有数据。从概念上讲,与消息队列相比,基于 XML 的 Web 服务比远程解决方案更好。总的来说,XML 应该被认为是数据的平台中立表示,即 SOA 的"),t("em",[e._v("通用语言")]),e._v("。在开发或使用 Web 服务时,重点应该放在这个 XML 上,而不是 Java 上。")]),e._v(" "),t("p",[e._v("Spring Web 服务专注于创建这些文档驱动的 Web 服务。 Spring Web 服务促进了契约优先的 SOAP 服务开发,允许通过使用操纵 XML 有效负载的多种方法之一来创建灵活的 Web 服务。 Spring-WS 提供了一个功能强大的"),t("a",{attrs:{href:"#server"}},[e._v("消息调度框架")]),e._v("、一个与你现有的应用程序安全解决方案集成的"),t("a",{attrs:{href:"#security"}},[e._v("WS-Security")]),e._v("解决方案,以及一个遵循熟悉的 Spring 模板模式的"),t("a",{attrs:{href:"#client"}},[e._v("客户端 API")]),e._v("。")]),e._v(" "),t("h1",{attrs:{id:"一-导言"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#一-导言"}},[e._v("#")]),e._v(" 一.导言")]),e._v(" "),t("p",[e._v("这是参考文献"),t("a",{attrs:{href:"#what-is-spring-ws"}},[e._v("是一个概述")]),e._v("的第一部分 Spring Web 服务和底层概念。 Spring-WS 随后被介绍,并且"),t("a",{attrs:{href:"#why-contract-first"}},[e._v("概念")]),e._v("契约优先 Web 服务开发的背后的"),t("a",{attrs:{href:"#why-contract-first"}},[e._v("概念")]),e._v("被解释。")]),e._v(" "),t("h2",{attrs:{id:"_1-什么是-spring-web-服务"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-什么是-spring-web-服务"}},[e._v("#")]),e._v(" 1.什么是 Spring Web 服务?")]),e._v(" "),t("h3",{attrs:{id:"_1-1-导言"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-导言"}},[e._v("#")]),e._v(" 1.1.导言")]),e._v(" "),t("p",[e._v("Spring Web 服务( Spring-WS)是 Spring 社区的产品,并且专注于创建文档驱动的 Web 服务。 Spring Web 服务旨在促进契约优先的 SOAP 服务开发,允许通过使用操纵 XML 有效负载的多种方法之一来创建灵活的 Web 服务。该产品基于 Spring 本身,这意味着你可以使用 Spring 概念(例如依赖注入)作为 Web 服务的一个组成部分。")]),e._v(" "),t("p",[e._v("人们使用 Spring-WS 的原因有很多,但在遵循 Web 服务最佳实践时,大多数人在发现其他 SOAP 堆栈缺乏之后都会被它所吸引。 Spring-WS 使最佳实践成为一种简单的实践。这包括一些实践,如 WS-I 基本概要文件、契约优先的开发,以及在契约和实现之间的松散耦合。 Spring Web 服务的其他关键特性是:")]),e._v(" "),t("ul",[t("li",[t("p",[t("a",{attrs:{href:"#features-powerful-mappings"}},[e._v("强大的映射")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#features-xml-api-support"}},[e._v("XML API 支持")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#features-flexible-xml-marshalling"}},[e._v("灵活的 XML 编组")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#features-reusing-your-spring-expertise"}},[e._v("Reusing Your Spring expertise")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#features-support-for-ws-security"}},[e._v("对 WS-Security 的支持")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#features-integration-with-spring-security"}},[e._v("Integration with Spring Security")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#features-apache-license"}},[e._v("Apache license")])])])]),e._v(" "),t("h4",{attrs:{id:"_1-1-1-强大的映射"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-1-强大的映射"}},[e._v("#")]),e._v(" 1.1.1.强大的映射")]),e._v(" "),t("p",[e._v("你可以将传入的 XML 请求分发到任何对象,这取决于消息有效负载、SOAP 动作报头或 XPath 表达式。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-2-xml-api-支持"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-2-xml-api-支持"}},[e._v("#")]),e._v(" 1.1.2.XML API 支持")]),e._v(" "),t("p",[e._v("传入的 XML 消息不仅可以通过标准的 JAXP API(如 DOM、SAX 和 STAX)来处理,还可以通过 JDOM、DOM4J、XOM 甚至编组技术来处理。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-3-灵活的-xml-编组"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-3-灵活的-xml-编组"}},[e._v("#")]),e._v(" 1.1.3.灵活的 XML 编组")]),e._v(" "),t("p",[e._v("Spring Web 服务构建在 Spring 框架中的对象/XML 映射模块上,该模块支持 JAXB1 和 2、Castor、XMLBeans、JiBX 和 XStream。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-4-重用你的-spring-专长"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-4-重用你的-spring-专长"}},[e._v("#")]),e._v(" 1.1.4.重用你的 Spring 专长")]),e._v(" "),t("p",[e._v("Spring-WS 对所有配置都使用 Spring 应用程序上下文,这应该有助于 Spring 开发人员快速获得最新信息。另外, Spring-WS 的体系结构类似于 Spring-MVC 的体系结构。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-5-对-ws-security-的支持"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-5-对-ws-security-的支持"}},[e._v("#")]),e._v(" 1.1.5.对 WS-Security 的支持")]),e._v(" "),t("p",[e._v("WS-Security 允许你对 SOAP 消息进行签名、加密和解密,或者针对它们进行身份验证。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-6-与-spring-安全性的集成"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-6-与-spring-安全性的集成"}},[e._v("#")]),e._v(" 1.1.6.与 Spring 安全性的集成")]),e._v(" "),t("p",[e._v("Spring Web 服务的 WS-Security 实现提供了与 Spring 安全性的集成。这意味着你也可以为你的 SOAP 服务使用你现有的 Spring 安全配置。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-7-apache-许可证"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-7-apache-许可证"}},[e._v("#")]),e._v(" 1.1.7. Apache 许可证")]),e._v(" "),t("p",[e._v("你可以放心地在项目中使用 Spring-WS。")]),e._v(" "),t("h3",{attrs:{id:"_1-2-运行时环境"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-2-运行时环境"}},[e._v("#")]),e._v(" 1.2.运行时环境")]),e._v(" "),t("p",[e._v("Spring Web 服务需要一个标准的 Java8 运行时环境。 Spring-WS 是基于 Spring Framework4.0.9 构建的,但支持更高的版本。")]),e._v(" "),t("p",[e._v("Spring-WS 由许多模块组成,这些模块在本节的其余部分中进行了描述。")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("XML 模块("),t("code",[e._v("spring-xml.jar")]),e._v(")包含用于 Spring Web 服务的各种 XML 支持类。这个模块主要用于 Spring-WS 框架本身,而不是 Web 服务开发人员。")])]),e._v(" "),t("li",[t("p",[e._v("核心模块("),t("code",[e._v("spring-ws-core.jar")]),e._v(")是 Spring 的 Web 服务功能的核心部分。它提供了中心["),t("code",[e._v("WebServiceMessage")]),e._v("](#web-service-messages)和["),t("code",[e._v("SoapMessage")]),e._v("](#SOAP-message)接口、"),t("a",{attrs:{href:"#server"}},[e._v("服务器端")]),e._v("框架(具有强大的消息调度功能)、用于实现 Web 服务端点的各种支持类,以及"),t("a",{attrs:{href:"#client"}},[e._v("客户端")]),t("code",[e._v("WebServiceTemplate")]),e._v("。")])]),e._v(" "),t("li",[t("p",[e._v("支持模块("),t("code",[e._v("spring-ws-support.jar")]),e._v(")包含额外的传输(JMS、电子邮件和其他)。")])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#security"}},[e._v("Security")]),e._v("包("),t("code",[e._v("spring-ws-security.jar")]),e._v(")提供了与核心 Web 服务包集成的 WS-Security 实现。它允许你对 SOAP 消息进行签名、解密和加密,并向 SOAP 消息添加主体令牌。此外,它允许你使用现有的 Spring 安全安全实现来进行身份验证和授权。")])])]),e._v(" "),t("p",[e._v("下图显示了 Spring-WS 模块之间的依赖关系。箭头表示依赖关系(即, Spring-WS Core 依赖于 Spring-XML 和 Spring 3 及更高版本中的 OXM 模块)。")]),e._v(" "),t("p",[t("img",{attrs:{src:"spring-deps.png",alt:"spring deps"}})]),e._v(" "),t("h3",{attrs:{id:"_1-3-支持的标准"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-支持的标准"}},[e._v("#")]),e._v(" 1.3.支持的标准")]),e._v(" "),t("p",[e._v("Spring Web 服务支持以下标准:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("SOAP1.1 和 1.2")])]),e._v(" "),t("li",[t("p",[e._v("WSDL1.1 和 2.0(仅 WSDL1.1 支持基于 XSD 的生成)")])]),e._v(" "),t("li",[t("p",[e._v("WS-I 基本配置文件 1.0、1.1、1.2 和 2.0")])]),e._v(" "),t("li",[t("p",[e._v("WS-Addressing1.0 和 2004 年 8 月的草案")])]),e._v(" "),t("li",[t("p",[e._v("SOAP 消息安全 1.1,用户名令牌配置文件 1.1,X.509 证书令牌配置文件 1.1,SAML 令牌配置文件 1.1,Kerberos 令牌配置文件 1.1,基本安全配置文件 1.1")])])]),e._v(" "),t("h2",{attrs:{id:"_2-为什么要先签约"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-为什么要先签约"}},[e._v("#")]),e._v(" 2.为什么要先签约?")]),e._v(" "),t("p",[e._v("在创建 Web 服务时,有两种开发风格:Contract-last 和 Contract-first。当你使用契约-最后一种方法时,首先从 Java 代码开始,然后让 Web 服务契约(在 WSDL 中——参见侧边栏)从中生成。当使用 Contract-First 时,你可以从 WSDL Contract 开始,并使用 Java 来实现该 Contract。")]),e._v(" "),t("p",[t("strong",[e._v("什么是 WSDL?")])]),e._v(" "),t("p",[e._v("WSDL 代表 Web 服务描述语言。WSDL 文件是描述 Web 服务的 XML 文档。它指定了服务的位置和服务公开的操作(或方法)。有关 WSDL 的更多信息,请参见"),t("a",{attrs:{href:"https://www.w3.org/TR/wsdl",target:"_blank",rel:"noopener noreferrer"}},[e._v("WSDL 规范"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("p",[e._v("Spring-WS 只支持契约优先的开发风格,本节解释了原因。")]),e._v(" "),t("h3",{attrs:{id:"_2-1-对象-xml-阻抗不匹配"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-对象-xml-阻抗不匹配"}},[e._v("#")]),e._v(" 2.1.对象/XML 阻抗不匹配")]),e._v(" "),t("p",[e._v("与 ORM 领域类似,在 ORM 领域中,我们有"),t("a",{attrs:{href:"https://en.wikipedia.org/wiki/Object-Relational_impedance_mismatch",target:"_blank",rel:"noopener noreferrer"}},[e._v("对象/关系阻抗不匹配"),t("OutboundLink")],1),e._v(",将 Java 对象转换为 XML 也有类似的问题。乍一看,O/X 映射问题似乎很简单:为每个 Java 对象创建一个 XML 元素,将所有 Java 属性和字段转换成子元素或属性。然而,事情并不像表面上看起来那么简单,因为层次化语言(例如 XML(尤其是 XSD))与 Java 的图形模型之间存在根本的区别。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("这一部分的大部分内容都受到了[[alpine]和[[effective-Enterprise-java]](#effective-Enterprise-java)的启发。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_2-1-1-xsd-扩展"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-1-xsd-扩展"}},[e._v("#")]),e._v(" 2.1.1.XSD 扩展")]),e._v(" "),t("p",[e._v("在 Java 中,改变类的行为的唯一方法是对其进行子类分类,以便将新的行为添加到该子类中。在 XSD 中,你可以通过限制数据类型来扩展数据类型——也就是说,限制元素和属性的有效值。例如,考虑以下示例:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n')])])]),t("p",[e._v("这种类型通过正则表达式来限制 XSD 字符串,只允许三个大写字母。如果将此类型转换为 Java,则最终得到一个普通的"),t("code",[e._v("java.lang.String")]),e._v("。正则表达式在转换过程中丢失,因为 Java 不允许这些类型的扩展。")]),e._v(" "),t("h4",{attrs:{id:"_2-1-2-不可移植类型"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-2-不可移植类型"}},[e._v("#")]),e._v(" 2.1.2.不可移植类型")]),e._v(" "),t("p",[e._v("Web 服务最重要的目标之一是可互操作:支持多个平台,如 Java、.NET、Python 和其他平台。因为所有这些语言都有不同的类库,所以你必须使用一些常见的跨语言格式来在它们之间进行通信。这种格式是 XML,所有这些语言都支持这种格式。")]),e._v(" "),t("p",[e._v("由于这种转换,你必须确保在服务实现中使用可移植类型。例如,考虑一个返回"),t("code",[e._v("java.util.TreeMap")]),e._v("的服务:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('public Map getFlights() {\n // use a tree map, to make sure it\'s sorted\n TreeMap map = new TreeMap();\n map.put("KL1117", "Stockholm");\n ...\n return map;\n}\n')])])]),t("p",[e._v("毫无疑问,这个映射的内容可以转换为某种 XML,但是由于没有用 XML 描述映射的标准方法,所以它将是专有的。此外,即使可以将其转换为 XML,许多平台也不具有类似于"),t("code",[e._v("TreeMap")]),e._v("的数据结构。因此,当一个.NET 客户机访问你的 Web 服务时,它可能会以"),t("code",[e._v("System.Collections.Hashtable")]),e._v("结束,这具有不同的语义。")]),e._v(" "),t("p",[e._v("在客户端工作时也存在此问题。考虑以下 XSD 片段,它描述了一个服务契约:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n\n')])])]),t("p",[e._v("此契约定义了一个请求,该请求接受"),t("code",[e._v("date")]),e._v(",这是一个表示年、月和日的 XSD 数据类型。如果我们从 Java 调用这个服务,我们可能使用"),t("code",[e._v("java.util.Date")]),e._v("或"),t("code",[e._v("java.util.Calendar")]),e._v("。然而,这两个类实际上都描述了时间,而不是日期。因此,我们最终实际上发送了代表 2007 年 4 月 4 日午夜的数据("),t("code",[e._v("2007-04-04T00:00:00")]),e._v("),这与"),t("code",[e._v("2007-04-04")]),e._v("不同。")]),e._v(" "),t("h4",{attrs:{id:"_2-1-3-循环图"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-3-循环图"}},[e._v("#")]),e._v(" 2.1.3.循环图")]),e._v(" "),t("p",[e._v("假设我们有如下的类结构:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public class Flight {\n private String number;\n private List passengers;\n\n // getters and setters omitted\n}\n\npublic class Passenger {\n private String name;\n private Flight flight;\n\n // getters and setters omitted\n}\n")])])]),t("p",[e._v("这是一个循环图:"),t("code",[e._v("Flight")]),e._v("是指"),t("code",[e._v("Passenger")]),e._v(",再次是指"),t("code",[e._v("Flight")]),e._v("。类似的循环图在 Java 中非常常见。如果我们采用一种简单的方法将其转换为 XML,那么我们最终会得到这样的结果:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n Arjen Poutsma\n \n \n \n Arjen Poutsma\n \n \n \n Arjen Poutsma\n ...\n')])])]),t("p",[e._v("处理这样的结构可能需要很长时间才能完成,因为此循环没有停止条件。")]),e._v(" "),t("p",[e._v("解决此问题的一种方法是使用对已经编组的对象的引用:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n Arjen Poutsma\n \n \n ...\n \n\n')])])]),t("p",[e._v("这解决了递归问题,但引入了新的问题。首先,你无法使用 XML 验证器来验证此结构。另一个问题是,在 SOAP(RPC/Encoded)中使用这些引用的标准方式已被弃用,而倾向于使用文档/文字(参见 WS-I)。")]),e._v(" "),t("p",[e._v("这些只是处理 O/X 映射时的一些问题。在编写 Web 服务时,尊重这些问题是很重要的。尊重它们的最好方法是完全关注 XML,同时使用 Java 作为一种实现语言。这就是契约优先的意义所在。")]),e._v(" "),t("h3",{attrs:{id:"_2-2-合同优先与合同优先"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-2-合同优先与合同优先"}},[e._v("#")]),e._v(" 2.2.合同优先与合同优先")]),e._v(" "),t("p",[e._v("除了上一节中提到的对象/XML 映射问题外,还有其他原因使我们更喜欢契约优先的开发风格。")]),e._v(" "),t("ul",[t("li",[t("p",[t("a",{attrs:{href:"#contract-first-fragility"}},[e._v("Fragility")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#contract-first-performance"}},[e._v("表现")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#contract-first-reusability"}},[e._v("可重用性")])])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"#contract-first-versioning"}},[e._v("Versioning")])])])]),e._v(" "),t("h4",{attrs:{id:"_2-2-1-脆弱性"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-2-1-脆弱性"}},[e._v("#")]),e._v(" 2.2.1.脆弱性")]),e._v(" "),t("p",[e._v("如前所述,Contract-Last 开发风格导致你的 Web 服务契约(WSDL 和你的 XSD)是从你的 Java 契约(通常是一个接口)生成的。如果你使用这种方法,你不能保证合同随着时间的推移而保持不变。每次更改 Java 契约并重新部署它时,可能都会对 Web 服务契约进行后续更改。")]),e._v(" "),t("p",[e._v("此外,并非所有的 SOAP 堆栈都从 Java 契约生成相同的 Web 服务契约。这意味着,将当前的 SOAP 堆栈更改为不同的堆栈(无论出于什么原因)也可能会更改 Web 服务契约。")]),e._v(" "),t("p",[e._v("当 Web 服务契约发生更改时,必须指示契约的用户获得新的契约,并可能更改其代码以适应契约中的任何更改。")]),e._v(" "),t("p",[e._v("要使合同有用,它必须尽可能长时间地保持不变。如果合同变更,你必须与服务的所有用户联系,并指示他们获得新版本的合同。")]),e._v(" "),t("h4",{attrs:{id:"_2-2-2-表现"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-2-2-表现"}},[e._v("#")]),e._v(" 2.2.2.表现")]),e._v(" "),t("p",[e._v("当一个 Java 对象被自动转换为 XML 时,就无法确定哪些内容是通过连接发送的。一个对象可能引用另一个对象,后者引用另一个对象,依此类推。最后,虚拟机中堆上的一半对象可能会转换为 XML,这会导致响应时间变慢。")]),e._v(" "),t("p",[e._v("在使用 Contract-First 时,你将显式地描述将哪些 XML 发送到哪里,从而确保它正是你想要的。")]),e._v(" "),t("h4",{attrs:{id:"_2-2-3-可重用性"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-2-3-可重用性"}},[e._v("#")]),e._v(" 2.2.3.可重用性")]),e._v(" "),t("p",[e._v("在一个单独的文件中定义模式,可以让你在不同的场景中重用该文件。考虑一个名为"),t("code",[e._v("airline.xsd")]),e._v("的文件中"),t("code",[e._v("AirportCode")]),e._v("的定义:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n')])])]),t("p",[e._v("通过使用"),t("code",[e._v("import")]),e._v("语句,可以在其他模式甚至 WSDL 文件中重用此定义。")]),e._v(" "),t("h4",{attrs:{id:"_2-2-4-版本化"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-2-4-版本化"}},[e._v("#")]),e._v(" 2.2.4.版本化")]),e._v(" "),t("p",[e._v("尽管合同必须尽可能长时间保持不变,但有时确实需要更改。在 Java 中,这通常会产生一个新的 Java 接口,例如"),t("code",[e._v("AirlineService2")]),e._v(",以及该接口的(新的)实现。当然,必须保留旧的服务,因为可能有一些客户尚未迁移。")]),e._v(" "),t("p",[e._v("如果使用 Contract-First,我们可以在 Contract 和实现之间有一个更松散的耦合。这样的松散耦合使我们能够在一个类中实现契约的两个版本。例如,我们可以使用 XSLT 样式表将任何“旧式”消息转换为“新式”消息。")]),e._v(" "),t("h2",{attrs:{id:"_3-书面合同优先的-web-服务"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-书面合同优先的-web-服务"}},[e._v("#")]),e._v(" 3.书面合同优先的 Web 服务")]),e._v(" "),t("p",[e._v("本教程向你展示了如何编写"),t("a",{attrs:{href:"#why-contract-first"}},[e._v("契约优先的 Web 服务")]),e._v("——也就是说,如何开发首先从 XML 模式或 WSDL 契约开始,然后是 Java 代码的 Web 服务。 Spring-WS 专注于这种开发风格,本教程应该帮助你开始学习。请注意,本教程的第一部分几乎不包含 Spring-WS 特定的信息。它主要是关于 XML、XSD 和 WSDL 的。"),t("a",{attrs:{href:"#tutorial-creating-project"}},[e._v("第二部分")]),e._v("着重于用 Spring-WS 实现这个契约。")]),e._v(" "),t("p",[e._v("在进行契约优先的 Web 服务开发时,最重要的事情是考虑 XML。这意味着 Java 语言的概念不那么重要。跨线发送的是 XML,你应该关注这一点。使用 Java 来实现 Web 服务是一个实现细节。")]),e._v(" "),t("p",[e._v("在本教程中,我们定义了一个由人力资源部门创建的 Web 服务。客户可以向这项服务发送假期申请表格来预订假期。")]),e._v(" "),t("h3",{attrs:{id:"_3-1-信息"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-1-信息"}},[e._v("#")]),e._v(" 3.1.信息")]),e._v(" "),t("p",[e._v("在这一节中,我们将重点关注发送到 Web 服务和从 Web 服务发送的实际 XML 消息。我们首先要确定这些信息是什么样子的。")]),e._v(" "),t("h4",{attrs:{id:"_3-1-1-假日"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-1-1-假日"}},[e._v("#")]),e._v(" 3.1.1.假日")]),e._v(" "),t("p",[e._v("在该场景中,我们必须处理假期请求,因此确定 XML 中的假期是什么样子是有意义的:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n 2006-07-03\n 2006-07-07\n\n')])])]),t("p",[e._v("假日由开始日期和结束日期组成。我们还决定对日期使用标准的"),t("a",{attrs:{href:"https://www.cl.cam.ac.uk/~mgk25/iso-time.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("ISO 8601"),t("OutboundLink")],1),e._v("日期格式,因为这节省了大量的解析麻烦。我们还向元素添加了一个命名空间,以确保我们的元素可以在其他 XML 文档中使用。")]),e._v(" "),t("h4",{attrs:{id:"_3-1-2-雇员"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-1-2-雇员"}},[e._v("#")]),e._v(" 3.1.2.雇员")]),e._v(" "),t("p",[e._v("在这个场景中,还有一个员工的概念。以下是它在 XML 中的样子:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n 42\n Arjen\n Poutsma\n\n')])])]),t("p",[e._v("我们使用了与以前相同的名称空间。如果"),t("code",[e._v("")]),e._v("元素可以在其他场景中使用,那么使用不同的名称空间可能是有意义的,例如"),t("code",[e._v("[http://example.com/employees/schemas](http://example.com/employees/schemas)")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_3-1-3-度假请求"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-1-3-度假请求"}},[e._v("#")]),e._v(" 3.1.3.度假请求")]),e._v(" "),t("p",[t("code",[e._v("holiday")]),e._v("元素和"),t("code",[e._v("employee")]),e._v("元素都可以放在"),t("code",[e._v("")]),e._v("中:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n 2006-07-03\n 2006-07-07\n \n \n 42\n Arjen\n Poutsma\n \n\n')])])]),t("p",[e._v("这两个元素的顺序并不重要:"),t("code",[e._v("")]),e._v("可能是第一个元素。重要的是,所有的数据都在那里。事实上,数据是唯一重要的东西:我们采用数据驱动的方法。")]),e._v(" "),t("h3",{attrs:{id:"_3-2-数据契约"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-2-数据契约"}},[e._v("#")]),e._v(" 3.2.数据契约")]),e._v(" "),t("p",[e._v("既然我们已经看到了一些可以使用的 XML 数据的示例,那么将其形式化为一个模式是有意义的。这个数据契约定义了我们接受的消息格式。为 XML 定义这样的契约有四种不同的方式:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("DTD")])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"https://www.w3.org/XML/Schema",target:"_blank",rel:"noopener noreferrer"}},[e._v("XMLSchema(XSD)"),t("OutboundLink")],1)])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"http://www.relaxng.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("RELAX NG"),t("OutboundLink")],1)])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"http://www.schematron.com/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Schematron"),t("OutboundLink")],1)])])]),e._v(" "),t("p",[e._v("DTD 的名称空间支持有限,因此它们不适合于 Web 服务。RELAXNG 和 Schematron 比 XML Schema 更简单。不幸的是,它们并没有得到跨平台的广泛支持。因此,我们使用了 XML 模式。")]),e._v(" "),t("p",[e._v("到目前为止,创建 XSD 的最简单方法是从示例文档中进行推断。任何好的 XML 编辑器或 Java IDE 都提供这种功能。基本上,这些工具使用一些示例 XML 文档来生成一个模式来验证它们。最终结果当然需要润色,但这是一个很好的起点。")]),e._v(" "),t("p",[e._v("使用前面描述的示例,我们最终生成了以下生成的模式:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n')])])]),t("p",[e._v("这个生成的模式可以改进。首先要注意的是,每个类型都有一个根级元素声明。这意味着 Web 服务应该能够接受所有这些元素作为数据。这是不可取的:我们只希望接受"),t("code",[e._v("")]),e._v("。通过删除包装元素标签(从而保留类型)并内联结果,我们可以实现这一点,如下所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n')])])]),t("p",[e._v("该模式仍然存在一个问题:对于这样的模式,你可以期望下面的消息进行验证:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n this is not a date\n neither is this\n \n PlainText Section qName:lineannotation level:4, chunks:[<, !-- ... --, >] attrs:[:]\n\n')])])]),t("p",[e._v("显然,我们必须确保开始日期和结束日期是真正的日期。XMLSchema 有一个很好的内置"),t("code",[e._v("date")]),e._v("类型,我们可以使用它。我们还将"),t("code",[e._v("NCName")]),e._v("s 更改为"),t("code",[e._v("string")]),e._v("实例。最后,我们将"),t("code",[e._v("sequence")]),e._v("中的"),t("code",[e._v("")]),e._v("修改为"),t("code",[e._v("all")]),e._v("。这告诉 XML 解析器"),t("code",[e._v("")]),e._v("和"),t("code",[e._v("")]),e._v("的顺序不重要。我们的最终 XSD 如下所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n (1)\n (1)\n \n \n \n \n \n (2)\n (2)\n \n \n \n \n \n (3)\n (3)\n \n \n\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[t("code",[e._v("all")]),e._v("告诉 XML 解析器,"),t("code",[e._v("")]),e._v("和"),t("code",[e._v("")]),e._v("的顺序不重要。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[e._v("我们使用"),t("code",[e._v("xs:date")]),e._v("数据类型(包括一年、一个月和一天)表示"),t("code",[e._v("")]),e._v("和"),t("code",[e._v("")]),e._v("。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("3")])]),e._v(" "),t("td",[t("code",[e._v("xs:string")]),e._v("用于名字和姓氏。")])])])]),e._v(" "),t("p",[e._v("我们将此文件存储为"),t("code",[e._v("hr.xsd")]),e._v("。")]),e._v(" "),t("h3",{attrs:{id:"_3-3-服务合同"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-3-服务合同"}},[e._v("#")]),e._v(" 3.3.服务合同")]),e._v(" "),t("p",[e._v("服务契约通常表示为"),t("a",{attrs:{href:"https://www.w3.org/TR/wsdl",target:"_blank",rel:"noopener noreferrer"}},[e._v("WSDL"),t("OutboundLink")],1),e._v("文件。请注意,在 Spring-WS 中,不需要手工编写 WSDL。基于 XSD 和一些约定, Spring-WS 可以为你创建 WSDL,如在标题为"),t("a",{attrs:{href:"#tutorial-implementing-endpoint"}},[e._v("实现端点")]),e._v("的部分中所解释的那样。本节的其余部分展示了如何手工编写 WSDL。你可能希望跳到"),t("a",{attrs:{href:"#tutorial-creating-project"}},[e._v("下一节")]),e._v("。")]),e._v(" "),t("p",[e._v("我们从标准的前导码开始我们的 WSDL,并通过导入我们现有的 XSD。为了将模式从定义中分离出来,我们为 WSDL 定义使用了一个单独的名称空间:"),t("code",[e._v("[http://mycompany.com/hr/definitions](http://mycompany.com/hr/definitions)")]),e._v("。以下清单显示了序言部分:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n')])])]),t("p",[e._v("接下来,我们根据所写的模式类型添加消息。我们只有一条消息,即我们在模式中放入的"),t("code",[e._v("")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v(' \n \n \n')])])]),t("p",[e._v("我们将消息作为一种操作添加到端口类型中:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v(' \n \n \n \n \n')])])]),t("p",[e._v("该消息完成了 WSDL 的抽象部分(接口),并保留了具体部分。具体部分由"),t("code",[e._v("binding")]),e._v("(它告诉客户机如何调用你刚刚定义的操作)和"),t("code",[e._v("service")]),e._v("(它告诉客户机在哪里调用它)组成。")]),e._v(" "),t("p",[e._v("添加一个具体的部分是相当标准的。要做到这一点,请参考你先前定义的抽象部分,确保对"),t("code",[e._v("soap:binding")]),e._v("元素使用"),t("code",[e._v("document/literal")]),e._v("("),t("code",[e._v("rpc/encoded")]),e._v("已废弃),为操作选择一个"),t("code",[e._v("soapAction")]),e._v("(在本例中,"),t("code",[e._v("[http://mycompany.com/RequestHoliday](http://mycompany.com/RequestHoliday)")]),e._v(",但任何 URI 都有效),并确定你希望请求到达的"),t("code",[e._v("location")]),e._v("URL(在本例中,"),t("code",[e._v("[http://mycompany.com/humanresources](http://mycompany.com/humanresources)")]),e._v("):")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n (2)\n (3)\n \n (4)\n \n (2)\n \n \n (4)(5)\n (7)\n \n (8)\n \n (6)\n \n \n \n \n (5)\n (9)\n \n \n\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[e._v("我们导入在"),t("a",{attrs:{href:"#tutorial.xsd"}},[e._v("数据契约")]),e._v("中定义的模式。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[e._v("我们定义"),t("code",[e._v("HolidayRequest")]),e._v("消息,它在"),t("code",[e._v("portType")]),e._v("中使用。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("3")])]),e._v(" "),t("td",[t("code",[e._v("HolidayRequest")]),e._v("类型在模式中定义。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("4")])]),e._v(" "),t("td",[e._v("我们定义"),t("code",[e._v("HumanResource")]),e._v("端口类型,它在"),t("code",[e._v("binding")]),e._v("中使用。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("5")])]),e._v(" "),t("td",[e._v("我们定义"),t("code",[e._v("HumanResourceBinding")]),e._v("绑定,它在"),t("code",[e._v("port")]),e._v("中使用。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("6")])]),e._v(" "),t("td",[e._v("我们使用文档/文字样式。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("7")])]),e._v(" "),t("td",[e._v("字面"),t("code",[e._v("[http://schemas.xmlsoap.org/soap/http](http://schemas.xmlsoap.org/soap/http)")]),e._v("表示 HTTP 传输。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("8")])]),e._v(" "),t("td",[t("code",[e._v("soapAction")]),e._v("属性表示每个请求都将发送的"),t("code",[e._v("SOAPAction")]),e._v("HTTP 头。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("9")])]),e._v(" "),t("td",[t("code",[e._v("[http://localhost:8080/holidayService/](http://localhost:8080/holidayService/)")]),e._v("地址是可以调用 Web 服务的 URL。")])])])]),e._v(" "),t("p",[e._v("前面的清单显示了最终的 WSDL。在下一节中,我们将描述如何实现生成的模式和 WSDL。")]),e._v(" "),t("h3",{attrs:{id:"_3-4-创建项目"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-4-创建项目"}},[e._v("#")]),e._v(" 3.4.创建项目")]),e._v(" "),t("p",[e._v("在本节中,我们使用"),t("a",{attrs:{href:"https://maven.apache.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Maven"),t("OutboundLink")],1),e._v("为我们创建初始项目结构。这样做并不是必需的,但大大减少了我们为设置 HolidayService 而必须编写的代码量。")]),e._v(" "),t("p",[e._v("下面的命令通过使用 Spring-WS 原型(即项目模板)为我们创建一个 Maven Web 应用程序项目:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("mvn archetype:create -DarchetypeGroupId=org.springframework.ws \\\n -DarchetypeArtifactId=spring-ws-archetype \\\n -DarchetypeVersion= \\\n -DgroupId=com.mycompany.hr \\\n -DartifactId=holidayService\n")])])]),t("p",[e._v("前面的命令创建一个名为"),t("code",[e._v("holidayService")]),e._v("的新目录。在这个目录中有一个"),t("code",[e._v("src/main/webapp")]),e._v("目录,其中包含 WAR 文件的根目录。你可以在这里找到标准的 Web 应用程序部署描述符("),t("code",[e._v("'WEB-INF/web.xml'")]),e._v("),它定义了一个 Spring-WS"),t("code",[e._v("MessageDispatcherServlet")]),e._v(",并将所有传入的请求映射到这个 Servlet:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n MyCompany HR Holiday Service\n\n \x3c!-- take special notice of the name of this servlet --\x3e\n \n spring-ws\n org.springframework.ws.transport.http.MessageDispatcherServlet\n \n\n \n spring-ws\n /*\n \n\n\n')])])]),t("p",[e._v("除了前面的"),t("code",[e._v("WEB-INF/web.xml")]),e._v("文件外,还需要另一个 Spring-WS 特定的配置文件,名为"),t("code",[e._v("WEB-INF/spring-ws-servlet.xml")]),e._v("。该文件包含所有 Spring-WS 特定的 bean,例如"),t("code",[e._v("EndPoints")]),e._v("和"),t("code",[e._v("WebServiceMessageReceivers")]),e._v(",并用于创建新的 Spring 容器。该文件的名称来自附加在其上的助理 Servlet(在本例中为"),t("code",[e._v("'spring-ws'")]),e._v(")的名称,该文件的名称为"),t("code",[e._v("-servlet.xml")]),e._v("。因此,如果你定义一个名为"),t("code",[e._v("MessageDispatcherServlet")]),e._v("的"),t("code",[e._v("'dynamite'")]),e._v(",则 Spring-WS 特定配置文件的名称将变为"),t("code",[e._v("WEB-INF/dynamite-servlet.xml")]),e._v("。")]),e._v(" "),t("p",[e._v("(你可以在[[tutorial.example.sws-conf-file]](#tutorial.example.sws-conf-file)中看到这个示例的"),t("code",[e._v("WEB-INF/spring-ws-servlet.xml")]),e._v("文件的内容。)")]),e._v(" "),t("p",[e._v("创建了项目结构之后,就可以将上一节中的模式和 WSDL 放入"),t("code",[e._v("'WEB-INF/'")]),e._v("文件夹中。")]),e._v(" "),t("h3",{attrs:{id:"_3-5-实现端点"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-5-实现端点"}},[e._v("#")]),e._v(" 3.5.实现端点")]),e._v(" "),t("p",[e._v("在 Spring-WS 中,实现端点来处理传入的 XML 消息。端点通常是通过使用"),t("code",[e._v("@Endpoint")]),e._v("注释对类进行注释来创建的。在这个端点类中,你可以创建一个或多个处理传入请求的方法。方法签名可以非常灵活。你可以包括与传入的 XML 消息相关的几乎任何类型的参数类型,正如我们在本章后面所解释的那样。")]),e._v(" "),t("h4",{attrs:{id:"_3-5-1-处理-xml-消息"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-5-1-处理-xml-消息"}},[e._v("#")]),e._v(" 3.5.1.处理 XML 消息")]),e._v(" "),t("p",[e._v("在这个示例应用程序中,我们使用"),t("a",{attrs:{href:"http://www.jdom.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("JDom 2"),t("OutboundLink")],1),e._v("来处理 XML 消息。我们还使用"),t("a",{attrs:{href:"https://www.w3.org/TR/xpath20/",target:"_blank",rel:"noopener noreferrer"}},[e._v("XPath"),t("OutboundLink")],1),e._v(",因为它允许我们选择 XML JDOM 树的特定部分,而不需要严格的模式一致性。")]),e._v(" "),t("p",[e._v("下面的清单显示了定义我们的假日端点的类:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('package com.mycompany.hr.ws;\n\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Arrays;\nimport java.util.Date;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.ws.server.endpoint.annotation.Endpoint;\nimport org.springframework.ws.server.endpoint.annotation.PayloadRoot;\nimport org.springframework.ws.server.endpoint.annotation.RequestPayload;\n\nimport com.mycompany.hr.service.HumanResourceService;\nimport org.jdom2.Element;\nimport org.jdom2.JDOMException;\nimport org.jdom2.Namespace;\nimport org.jdom2.filter.Filters;\nimport org.jdom2.xpath.XPathExpression;\nimport org.jdom2.xpath.XPathFactory;\n\n@Endpoint (1)\npublic class HolidayEndpoint {\n\n private static final String NAMESPACE_URI = "http://mycompany.com/hr/schemas";\n\n private XPathExpression startDateExpression;\n\n private XPathExpression endDateExpression;\n\n private XPathExpression firstNameExpression;\n\n private XPathExpression lastNameExpression;\n\n private HumanResourceService humanResourceService;\n\n @Autowired (2)\n public HolidayEndpoint(HumanResourceService humanResourceService) throws JDOMException {\n this.humanResourceService = humanResourceService;\n\n Namespace namespace = Namespace.getNamespace("hr", NAMESPACE_URI);\n XPathFactory xPathFactory = XPathFactory.instance();\n startDateExpression = xPathFactory.compile("//hr:StartDate", Filters.element(), null, namespace);\n endDateExpression = xPathFactory.compile("//hr:EndDate", Filters.element(), null, namespace);\n firstNameExpression = xPathFactory.compile("//hr:FirstName", Filters.element(), null, namespace);\n lastNameExpression = xPathFactory.compile("//hr:LastName", Filters.element(), null, namespace);\n }\n\n @PayloadRoot(namespace = NAMESPACE_URI, localPart = "HolidayRequest") (3)\n public void handleHolidayRequest(@RequestPayload Element holidayRequest) throws Exception {(4)\n Date startDate = parseDate(startDateExpression, holidayRequest);\n Date endDate = parseDate(endDateExpression, holidayRequest);\n String name = firstNameExpression.evaluateFirst(holidayRequest).getText() + " " + lastNameExpression.evaluateFirst(holidayRequest).getText();\n\n humanResourceService.bookHoliday(startDate, endDate, name);\n }\n\n private Date parseDate(XPathExpression expression, Element element) throws ParseException {\n Element result = expression.evaluateFirst(element);\n if (result != null) {\n SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");\n return dateFormat.parse(result.getText());\n } else {\n throw new IllegalArgumentException("Could not evaluate [" + expression + "] on [" + element + "]");\n }\n }\n\n}\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[t("code",[e._v("HolidayEndpoint")]),e._v("注释为"),t("code",[e._v("@Endpoint")]),e._v("。这将类标记为"),t("code",[e._v("@Component")]),e._v("的一种特殊类型,适用于在 Spring-WS 中处理 XML 消息,也使它适合于组件扫描。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[t("code",[e._v("HolidayEndpoint")]),e._v("需要"),t("code",[e._v("HumanResourceService")]),e._v("业务服务来操作,因此我们在构造函数中注入依赖项并用"),t("code",[e._v("@Autowired")]),e._v("对其进行注释。"),t("br"),e._v("接下来,我们使用 JDOM2API 设置 XPath 表达式。有四个表达式:"),t("code",[e._v("//hr:StartDate")]),e._v("用于提取"),t("code",[e._v("")]),e._v("文本值,"),t("code",[e._v("//hr:EndDate")]),e._v("用于提取结束日期,以及两个用于提取员工的姓名。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("3")])]),e._v(" "),t("td",[t("code",[e._v("@PayloadRoot")]),e._v("注释告诉 Spring-WS"),t("code",[e._v("handleHolidayRequest")]),e._v("方法适合处理 XML 消息。该方法可以处理的消息类型由注释值指示。在这种情况下,它可以"),t("br"),e._v("处理具有"),t("code",[e._v("HolidayRequest")]),e._v("本地部分和"),t("code",[e._v("[http://mycompany.com/hr/schemas](http://mycompany.com/hr/schemas)")]),e._v("名称空间的 XML 元素。"),t("br"),e._v("关于将消息映射到端点的更多信息将在下一节中提供。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("4")])]),e._v(" "),t("td",[e._v("主要的处理方法是"),t("code",[e._v("handleHolidayRequest(..)")]),e._v("方法,它从传入的 XML 消息中传递"),t("code",[e._v("")]),e._v("元素。"),t("code",[e._v("@RequestPayload")]),e._v("注释表示"),t("code",[e._v("holidayRequest")]),e._v("参数应该映射到"),t("br"),e._v("请求消息的有效负载。我们使用 XPath 表达式从 XML 消息中提取字符串值,并通过使用"),t("code",[e._v("SimpleDateFormat")]),e._v("("),t("code",[e._v("parseData")]),e._v("方法)将这些值转换为"),t("code",[e._v("Date")]),e._v("对象。使用这些值,我们调用业务服务上的一个方法,"),t("br"),e._v("通常,这会导致启动一个数据库事务,并更改数据库中的一些记录,"),t("br"),e._v("最后,我们定义一个"),t("code",[e._v("void")]),e._v("返回类型,这表明我们不想向 Spring-WS 发送响应消息。"),t("br"),e._v("如果我们想要响应消息,我们可以返回一个 JDOM 元素来表示响应消息的有效负载。")])])])]),e._v(" "),t("p",[e._v("使用 JDOM 只是处理 XML 的选项之一。其他选项包括 DOM、DOM4J、XOM、SAX 和 STAX,但也包括 JAXB、Castor、XMLBeans、JiBX 和 XStream 等编组技术,如"),t("a",{attrs:{href:"#common"}},[e._v("下一章")]),e._v("中所述。我们之所以选择 JDOM,是因为它让我们能够访问原始 XML,也因为它基于类(而不是 W3C DOM 和 DOM4J 那样的接口和工厂方法),这使得代码不那么冗长。我们使用 XPath 是因为它不像编组技术那么脆弱。只要能够找到日期和名称,就不需要严格的模式一致性。")]),e._v(" "),t("p",[e._v("因为我们使用 JDOM,所以我们必须向 Maven "),t("code",[e._v("pom.xml")]),e._v("添加一些依赖关系,它位于项目目录的根目录中。以下是 POM 的相关部分:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("\n \n org.springframework.ws\n spring-ws-core\n \n \n \n jdom\n jdom\n 2.0.1\n \n \n jaxen\n jaxen\n 1.1\n \n\n")])])]),t("p",[e._v("下面是如何使用组件扫描在"),t("code",[e._v("spring-ws-servlet.xml")]),e._v(" Spring XML 配置文件中配置这些类。我们还指示 Spring-WS 使用注释驱动的端点,使用"),t("code",[e._v("")]),e._v("元素。")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n\n\n')])])]),t("h4",{attrs:{id:"_3-5-2-将消息路由到端点"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-5-2-将消息路由到端点"}},[e._v("#")]),e._v(" 3.5.2.将消息路由到端点")]),e._v(" "),t("p",[e._v("作为编写端点的一部分,我们还使用"),t("code",[e._v("@PayloadRoot")]),e._v("注释来指示哪种消息可以由"),t("code",[e._v("handleHolidayRequest")]),e._v("方法处理。在 Spring-WS 中,这个过程是"),t("code",[e._v("EndpointMapping")]),e._v("的职责。在这里,我们使用"),t("code",[e._v("PayloadRootAnnotationMethodEndpointMapping")]),e._v("基于内容路由消息。下面的清单显示了我们之前使用的注释:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('@PayloadRoot(namespace = "http://mycompany.com/hr/schemas", localPart = "HolidayRequest")\n')])])]),t("p",[e._v("前面示例中显示的注释基本上意味着,每当收到带有名称空间"),t("code",[e._v("[http://mycompany.com/hr/schemas](http://mycompany.com/hr/schemas)")]),e._v("和"),t("code",[e._v("HolidayRequest")]),e._v("本地名称的 XML 消息时,它都会路由到"),t("code",[e._v("handleHolidayRequest")]),e._v("方法。通过在我们的配置中使用"),t("code",[e._v("")]),e._v("元素,我们启用了"),t("code",[e._v("@PayloadRoot")]),e._v("注释的检测。在一个端点中有多个相关的处理方法是可能的(也是非常常见的),每个方法处理不同的 XML 消息。")]),e._v(" "),t("p",[e._v("还有其他将端点映射到 XML 消息的方法,这在"),t("a",{attrs:{href:"#common"}},[e._v("下一章")]),e._v("中进行了描述。")]),e._v(" "),t("h4",{attrs:{id:"_3-5-3-提供服务和存根实现"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-5-3-提供服务和存根实现"}},[e._v("#")]),e._v(" 3.5.3.提供服务和存根实现")]),e._v(" "),t("p",[e._v("现在我们有了端点,我们需要"),t("code",[e._v("HumanResourceService")]),e._v("及其实现,供"),t("code",[e._v("HolidayEndpoint")]),e._v("使用。下面的清单显示了"),t("code",[e._v("HumanResourceService")]),e._v("接口:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("package com.mycompany.hr.service;\n\nimport java.util.Date;\n\npublic interface HumanResourceService {\n void bookHoliday(Date startDate, Date endDate, String name);\n}\n")])])]),t("p",[e._v("出于教程的目的,我们使用"),t("code",[e._v("HumanResourceService")]),e._v("的一个简单存根实现:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('package com.mycompany.hr.service;\n\nimport java.util.Date;\n\nimport org.springframework.stereotype.Service;\n\n@Service (1)\npublic class StubHumanResourceService implements HumanResourceService {\n public void bookHoliday(Date startDate, Date endDate, String name) {\n System.out.println("Booking holiday for [" + startDate + "-" + endDate + "] for [" + name + "] ");\n }\n}\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[t("code",[e._v("StubHumanResourceService")]),e._v("注释为"),t("code",[e._v("@Service")]),e._v("。这将类标记为一个业务外观,这使得它成为"),t("code",[e._v("@Autowired")]),e._v("中注入的候选项。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h3",{attrs:{id:"_3-6-发布-wsdl"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-6-发布-wsdl"}},[e._v("#")]),e._v(" 3.6.发布 WSDL")]),e._v(" "),t("p",[e._v("最后,我们需要发布 WSDL。正如"),t("a",{attrs:{href:"#tutorial-service-contract"}},[e._v("服务合同")]),e._v("中所述,我们不需要自己编写 WSDL。 Spring-WS 可以基于一些约定生成一个。以下是我们对这一代的定义:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v(' (5)\n (2)\n\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[t("code",[e._v("id")]),e._v("决定了可以检索 WSDL 的 URL。在这种情况下,"),t("code",[e._v("id")]),e._v("是"),t("code",[e._v("holiday")]),e._v(",这意味着 WSDL 可以在 Servlet 上下文中检索"),t("br"),e._v("作为"),t("code",[e._v("holiday.wsdl")]),e._v("。完整的 URL 是"),t("code",[e._v("[http://localhost:8080/holidayService/holiday.wsdl](http://localhost:8080/holidayService/holiday.wsdl)")]),e._v("。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[e._v("接下来,我们将 WSDL 端口类型设置为"),t("code",[e._v("HumanResource")]),e._v("。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("3")])]),e._v(" "),t("td",[e._v("我们设置了服务可以到达的位置:"),t("code",[e._v("/holidayService/")]),e._v("。我们使用一个相对的 URI,并指示框架动态地将它"),t("br"),e._v("转换为一个绝对的 URI。因此,如果将服务部署到不同的上下文中,我们不需要手动更改 URI。"),t("br"),e._v("有关更多信息,请参见"),t("a",{attrs:{href:"#server-automatic-wsdl-exposure"}},[e._v("该部分名为“自动 WSDL 曝光”")]),e._v("。为了使位置转换工作,我们需要在"),t("code",[e._v("web.xml")]),e._v("中向"),t("code",[e._v("spring-ws")]),e._v(" Servlet 添加一个 init 参数(如下一个清单所示)。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("4")])]),e._v(" "),t("td",[e._v("我们为 WSDL 定义本身定义了目标名称空间。不需要设置此属性。如果未设置,则 WSDL 具有与 XSD 模式相同的名称空间。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("5")])]),e._v(" "),t("td",[t("code",[e._v("xsd")]),e._v("元素引用了我们在"),t("a",{attrs:{href:"#tutorial.xsd"}},[e._v("数据契约")]),e._v("中定义的人力资源模式。我们将模式放置在应用程序的"),t("code",[e._v("WEB-INF")]),e._v("目录中。")])])])]),e._v(" "),t("p",[e._v("下面的清单展示了如何添加 init 参数:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("\n transformWsdlLocations\n true\n\n")])])]),t("p",[e._v("你可以使用"),t("code",[e._v("mvn install")]),e._v("创建一个 WAR 文件。如果部署应用程序(到 Tomcat、 Jetty 等)并将浏览器指向"),t("a",{attrs:{href:"http://localhost:8080/holidayService/holiday.wsdl",target:"_blank",rel:"noopener noreferrer"}},[e._v("这个位置"),t("OutboundLink")],1),e._v(",则会看到生成的 WSDL。此 WSDL 已准备好供客户机使用,例如"),t("a",{attrs:{href:"http://www.soapui.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("soapUI"),t("OutboundLink")],1),e._v("或其他 SOAP 框架。")]),e._v(" "),t("p",[e._v("本教程到此结束。教程代码可以在 Spring-WS 的完整发行版中找到。如果你希望继续,请查看作为发行版一部分的 echo 示例应用程序。在此之后,看一下航空公司示例,它有点复杂,因为它使用了 JAXB、WS-Security、 Hibernate 和事务性服务层。最后,你可以阅读参考文档的其余部分。")]),e._v(" "),t("h1",{attrs:{id:"ii。参考文献"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#ii。参考文献"}},[e._v("#")]),e._v(" ii。参考文献")]),e._v(" "),t("p",[e._v("参考文档的这一部分详细介绍了构成 Spring Web 服务的各个组件。其中包括"),t("a",{attrs:{href:"#common"}},[e._v("a chapter")]),e._v(",讨论了客户端和服务器端 WS 共有的部分,专门讨论"),t("a",{attrs:{href:"#server"}},[e._v("编写服务器端 Web 服务")]),e._v("的细节的一章,关于在"),t("a",{attrs:{href:"#client"}},[e._v("客户端")]),e._v("上使用 Web 服务的一章,以及关于使用"),t("a",{attrs:{href:"#security"}},[e._v("WS-Security")]),e._v("的一章。")]),e._v(" "),t("h2",{attrs:{id:"_4-共享组件"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-共享组件"}},[e._v("#")]),e._v(" 4.共享组件")]),e._v(" "),t("p",[e._v("本章将探讨在客户端和服务器端之间共享的组件 Spring-WS 开发。这些接口和类代表了 Spring-WS 的构建块,因此你需要理解它们的功能,即使你不直接使用它们。")]),e._v(" "),t("h3",{attrs:{id:"_4-1-web-服务消息"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-web-服务消息"}},[e._v("#")]),e._v(" 4.1.Web 服务消息")]),e._v(" "),t("p",[e._v("本节描述了 Spring-WS 使用的消息和消息工厂。")]),e._v(" "),t("h4",{attrs:{id:"_4-1-1-webservicemessage"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-1-webservicemessage"}},[e._v("#")]),e._v(" 4.1.1."),t("code",[e._v("WebServiceMessage")])]),e._v(" "),t("p",[e._v("Spring Web 服务的核心接口之一是"),t("code",[e._v("WebServiceMessage")]),e._v("。该接口表示与协议无关的 XML 消息。接口包含以"),t("code",[e._v("javax.xml.transform.Source")]),e._v("或"),t("code",[e._v("javax.xml.transform.Result")]),e._v("的形式提供对消息有效负载的访问的方法。"),t("code",[e._v("Source")]),e._v("和"),t("code",[e._v("Result")]),e._v("是标记接口,表示对 XML 输入和输出的抽象。具体的实现封装了各种 XML 表示,如下表所示:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[e._v("Source or Result implementation")]),e._v(" "),t("th",[e._v("打包的 XML 表示")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("code",[e._v("javax.xml.transform.dom.DOMSource")])]),e._v(" "),t("td",[t("code",[e._v("org.w3c.dom.Node")])])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("javax.xml.transform.dom.DOMResult")])]),e._v(" "),t("td",[t("code",[e._v("org.w3c.dom.Node")])])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("javax.xml.transform.sax.SAXSource")])]),e._v(" "),t("td",[t("code",[e._v("org.xml.sax.InputSource")]),e._v("和"),t("code",[e._v("org.xml.sax.XMLReader")])])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("javax.xml.transform.sax.SAXResult")])]),e._v(" "),t("td",[t("code",[e._v("org.xml.sax.ContentHandler")])])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("javax.xml.transform.stream.StreamSource")])]),e._v(" "),t("td",[t("code",[e._v("java.io.File")]),e._v(","),t("code",[e._v("java.io.InputStream")]),e._v(",或"),t("code",[e._v("java.io.Reader")])])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("javax.xml.transform.stream.StreamResult")])]),e._v(" "),t("td",[t("code",[e._v("java.io.File")]),e._v(","),t("code",[e._v("java.io.OutputStream")]),e._v(",或"),t("code",[e._v("java.io.Writer")])])])])]),e._v(" "),t("p",[e._v("除了读取和写入有效负载外,Web 服务消息还可以将自身写入输出流。")]),e._v(" "),t("h4",{attrs:{id:"_4-1-2-soapmessage"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-2-soapmessage"}},[e._v("#")]),e._v(" 4.1.2."),t("code",[e._v("SoapMessage")])]),e._v(" "),t("p",[t("code",[e._v("SoapMessage")]),e._v("是"),t("code",[e._v("WebServiceMessage")]),e._v("的一个子类。它包含特定于 SOAP 的方法,例如获取 SOAP 头、SOAP 错误等等。通常,你的代码不应该依赖于"),t("code",[e._v("SoapMessage")]),e._v(",因为可以通过在"),t("code",[e._v("WebServiceMessage")]),e._v("中使用"),t("code",[e._v("getPayloadSource()")]),e._v("和"),t("code",[e._v("getPayloadResult()")]),e._v("来获得 SOAP 主体的内容(消息的有效负载)。只有在需要执行特定于 SOAP 的操作(例如添加标头、获取附件等)时,才需要将"),t("code",[e._v("WebServiceMessage")]),e._v("强制转换为"),t("code",[e._v("SoapMessage")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_4-1-3-消息工厂"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-3-消息工厂"}},[e._v("#")]),e._v(" 4.1.3.消息工厂")]),e._v(" "),t("p",[e._v("具体的消息实现是由"),t("code",[e._v("WebServiceMessageFactory")]),e._v("创建的。这个工厂可以创建一个空消息或从输入流中读取消息。"),t("code",[e._v("WebServiceMessageFactory")]),e._v("有两个具体的实现方式。一种是基于 SAAJ 的,SAAJ 是一种带有 Java 附件 API 的 SOAP。另一种是基于 Axis2 的公理(Axis Object Model)。")]),e._v(" "),t("h5",{attrs:{id:"saajsoapmessagefactory"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#saajsoapmessagefactory"}},[e._v("#")]),e._v(" "),t("code",[e._v("SaajSoapMessageFactory")])]),e._v(" "),t("p",[t("code",[e._v("SaajSoapMessageFactory")]),e._v("使用 SOAP with Attachments API for Java 来创建"),t("code",[e._v("SoapMessage")]),e._v("实现。SAAJ 是 J2EE1.4 的一部分,因此大多数现代应用程序服务器都应该支持它。以下是常见应用程序服务器提供的 SAAJ 版本的概述:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[e._v("应用程序服务器")]),e._v(" "),t("th",[e._v("SAAJ Version")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[e._v("BEA WebLogic8")]),e._v(" "),t("td",[e._v("1.1")])]),e._v(" "),t("tr",[t("td",[e._v("BEA WebLogic9")]),e._v(" "),t("td",[e._v("1.1/1.2"),t("sup",[e._v("1")])])]),e._v(" "),t("tr",[t("td",[e._v("IBMWebSphere6")]),e._v(" "),t("td",[e._v("1.2")])]),e._v(" "),t("tr",[t("td",[e._v("Sun Glassfish 鱼 1")]),e._v(" "),t("td",[e._v("1.3")])]),e._v(" "),t("tr",[t("td",[t("sup",[e._v("1")]),e._v("WebLogic9 在 SAAJ1.2 实现中有一个已知的错误:它实现了所有的 1.2 接口,但在调用时抛出一个"),t("code",[e._v("UnsupportedOperationException")]),e._v("。 Spring Web 服务有一个解决方法:当在 WebLogic9 上操作时,它使用 SAAJ1.1.")]),e._v(" "),t("td")])])]),e._v(" "),t("p",[e._v("此外,Java SE6 还包括 SAAJ1.3.你可以按以下方式连接"),t("code",[e._v("SaajSoapMessageFactory")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("SAAJ 是基于 DOM 的文档对象模型。这意味着所有 SOAP 消息都存储在内存中。对于较大的 SOAP 消息,这可能不是性能。在这种情况下,"),t("code",[e._v("AxiomSoapMessageFactory")]),e._v("可能更适用。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h5",{attrs:{id:"axiomsoapmessagefactory"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#axiomsoapmessagefactory"}},[e._v("#")]),e._v(" "),t("code",[e._v("AxiomSoapMessageFactory")])]),e._v(" "),t("p",[t("code",[e._v("AxiomSoapMessageFactory")]),e._v("使用 Axis2 对象模型来创建"),t("code",[e._v("SoapMessage")]),e._v("实现。AXIOM 是基于 STAX 的,STAX 是 XML 的流媒体 API。STAX 为读取 XML 消息提供了一种基于拉的机制,对于较大的消息,这种机制可能更有效。")]),e._v(" "),t("p",[e._v("要提高"),t("code",[e._v("AxiomSoapMessageFactory")]),e._v("上的读取性能,可以将"),t("code",[e._v("payloadCaching")]),e._v("属性设置为 false(默认为 true)。这样做会使 SOAP 主体的内容直接从套接字流中读取。启用此设置后,有效负载只能读取一次。这意味着你必须确保消息的任何预处理(日志记录或其他工作)都不会占用它。")]),e._v(" "),t("p",[e._v("你可以使用"),t("code",[e._v("AxiomSoapMessageFactory")]),e._v("如下:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n\n')])])]),t("p",[e._v("除了有效负载缓存,AXIOM 还支持完整的流消息,如"),t("code",[e._v("StreamingWebServiceMessage")]),e._v("中所定义的。这意味着你可以直接设置响应消息的有效负载,而不是将其写入 DOM 树或缓冲区。")]),e._v(" "),t("p",[e._v("当处理程序方法返回支持 JAXB2 的对象时,将使用 AXIOM 的全流。它会自动将这个编组对象设置到响应消息中,并在响应结束时将其写出到传出套接字码流中。")]),e._v(" "),t("p",[e._v("有关完整流的更多信息,请参见"),t("code",[e._v("StreamingWebServiceMessage")]),e._v("和"),t("code",[e._v("StreamingPayload")]),e._v("的类级 Javadoc。")]),e._v(" "),t("h5",{attrs:{id:"soap1-1-或-1-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#soap1-1-或-1-2"}},[e._v("#")]),e._v(" SOAP1.1 或 1.2")]),e._v(" "),t("p",[t("code",[e._v("SaajSoapMessageFactory")]),e._v("和"),t("code",[e._v("AxiomSoapMessageFactory")]),e._v("都具有"),t("code",[e._v("soapVersion")]),e._v("属性,可以在其中注入一个"),t("code",[e._v("SoapVersion")]),e._v("常数。默认情况下,该版本是 1.1,但你可以将其设置为 1.2:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n \n \n \n \n\n\n')])])]),t("p",[e._v("在前面的示例中,我们定义了只接受 SOAP1.2 消息的"),t("code",[e._v("SaajSoapMessageFactory")]),e._v("。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("尽管两个版本的 SOAP 在格式上非常相似,但 1.2 版本并不向后兼容 1.1,因为它使用了不同的 XML 命名空间。SOAP1.1 和 1.2 之间的其他主要区别包括错误的不同结构,以及"),t("code",[e._v("SOAPAction")]),e._v("HTTP 头实际上已被弃用,尽管它们仍然有效。"),t("br"),t("br"),e._v("使用 SOAP 版本号(或一般的 WS-* 规范版本号)需要注意的一件重要事情是,规范的最新版本通常不是最流行的版本。对于 SOAP,这意味着(目前)使用的最佳版本是 1.1.版本 1.2 可能会在未来变得更受欢迎,但 1.1 是目前最安全的选择。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_4-1-4-messagecontext"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-4-messagecontext"}},[e._v("#")]),e._v(" 4.1.4."),t("code",[e._v("MessageContext")])]),e._v(" "),t("p",[e._v("通常,消息是成对出现的:请求和响应。在客户端创建一个请求,该请求通过一些传输发送到服务器端,在服务器端生成响应。这个响应被发送回客户机,在那里进行读取。")]),e._v(" "),t("p",[e._v("在 Spring Web 服务中,这样的会话包含在"),t("code",[e._v("MessageContext")]),e._v("中,其具有获取请求和响应消息的属性。在客户端,消息上下文由["),t("code",[e._v("WebServiceTemplate")]),e._v("](#client-web-service-template)创建。在服务器端,消息上下文是从特定于传输的输入流中读取的。例如,在 HTTP 中,从"),t("code",[e._v("HttpServletRequest")]),e._v("读取,然后将响应写回"),t("code",[e._v("HttpServletResponse")]),e._v("。")]),e._v(" "),t("h3",{attrs:{id:"_4-2-transportcontext"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-2-transportcontext"}},[e._v("#")]),e._v(" 4.2."),t("code",[e._v("TransportContext")])]),e._v(" "),t("p",[e._v("SOAP 协议的一个关键属性是它试图与传输无关。这就是为什么 Spring-WS 不支持通过 HTTP 请求 URL 而是通过消息内容将消息映射到端点的原因。")]),e._v(" "),t("p",[e._v("然而,有时需要获得对底层传输的访问,无论是在客户端还是服务器端。为此, Spring Web 服务具有"),t("code",[e._v("TransportContext")]),e._v("。传输上下文允许访问底层"),t("code",[e._v("WebServiceConnection")]),e._v(",这通常是服务器端的"),t("code",[e._v("HttpServletConnection")]),e._v("或客户端的"),t("code",[e._v("HttpUrlConnection")]),e._v("或"),t("code",[e._v("CommonsHttpConnection")]),e._v("。例如,你可以在服务器端端点或拦截器中获得当前请求的 IP 地址:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("TransportContext context = TransportContextHolder.getTransportContext();\nHttpServletConnection connection = (HttpServletConnection )context.getConnection();\nHttpServletRequest request = connection.getHttpServletRequest();\nString ipAddress = request.getRemoteAddr();\n")])])]),t("h3",{attrs:{id:"_4-3-使用-xpath-处理-xml"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-使用-xpath-处理-xml"}},[e._v("#")]),e._v(" 4.3.使用 XPath 处理 XML")]),e._v(" "),t("p",[e._v("处理 XML 的最佳方法之一是使用 XPath。引用[[effective-xml]],项目 35:")]),e._v(" "),t("blockquote",[t("p",[e._v("XPath 是第四代声明性语言,它允许你指定要处理的节点,而无需指定处理器应该如何导航到这些节点。XPath 的数据模型设计得非常好,能够完全支持几乎所有开发人员想要的 XML。例如,它合并了所有相邻的文本,包括 CDATA 部分中的文本,允许计算跳过注释和处理指令的值,包括来自子元素和子元素的文本,并要求解析所有外部实体引用。在实践中,XPath 表达式对于输入文档中意外但可能微不足道的更改往往更加健壮。")])]),e._v(" "),t("p",[e._v("——Elliotte Rusty Harold")]),e._v(" "),t("p",[e._v("Spring Web 服务有两种在应用程序中使用 XPath 的方式:更快的"),t("code",[e._v("XPathExpression")]),e._v("或更灵活的"),t("code",[e._v("XPathTemplate")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_4-3-1-xpathexpression"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-1-xpathexpression"}},[e._v("#")]),e._v(" 4.3.1."),t("code",[e._v("XPathExpression")])]),e._v(" "),t("p",[t("code",[e._v("XPathExpression")]),e._v("是对已编译的 XPath 表达式的抽象,例如 Java5"),t("code",[e._v("javax.xml.xpath.XPathExpression")]),e._v("接口或 Jaxen"),t("code",[e._v("XPath")]),e._v("类。要在应用程序上下文中构造表达式,可以使用"),t("code",[e._v("XPathExpressionFactoryBean")]),e._v("。下面的示例使用了这个工厂 Bean:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n \n \n\n \n \n \n\n\n')])])]),t("p",[e._v("前面的表达式不使用名称空间,但是我们可以通过使用工厂 Bean 的"),t("code",[e._v("namespaces")]),e._v("属性来设置这些名称空间。该表达式可在代码中使用如下:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('package sample;\n\npublic class MyXPathClass {\n\n private final XPathExpression nameExpression;\n\n public MyXPathClass(XPathExpression nameExpression) {\n this.nameExpression = nameExpression;\n }\n\n public void doXPath(Document document) {\n String name = nameExpression.evaluateAsString(document.getDocumentElement());\n System.out.println("Name: " + name);\n }\n\n}\n')])])]),t("p",[e._v("对于更灵活的方法,可以使用"),t("code",[e._v("NodeMapper")]),e._v(",这类似于 Spring 的 JDBC 支持中的"),t("code",[e._v("RowMapper")]),e._v("。下面的示例展示了如何使用它:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('package sample;\n\npublic class MyXPathClass {\n\n private final XPathExpression contactExpression;\n\n public MyXPathClass(XPathExpression contactExpression) {\n this.contactExpression = contactExpression;\n }\n\n public void doXPath(Document document) {\n List contacts = contactExpression.evaluate(document,\n new NodeMapper() {\n public Object mapNode(Node node, int nodeNum) throws DOMException {\n Element contactElement = (Element) node;\n Element nameElement = (Element) contactElement.getElementsByTagName("Name").item(0);\n Element phoneElement = (Element) contactElement.getElementsByTagName("Phone").item(0);\n return new Contact(nameElement.getTextContent(), phoneElement.getTextContent());\n }\n });\n PlainText Section qName; // do something with the list of Contact objects\n }\n}\n')])])]),t("p",[e._v("与 Spring JDBC 的"),t("code",[e._v("RowMapper")]),e._v("中的映射行类似,每个结果节点都通过使用匿名内部类进行映射。在这种情况下,我们创建一个"),t("code",[e._v("Contact")]),e._v("对象,稍后将使用它。")]),e._v(" "),t("h4",{attrs:{id:"_4-3-2-xpathtemplate"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-2-xpathtemplate"}},[e._v("#")]),e._v(" 4.3.2."),t("code",[e._v("XPathTemplate")])]),e._v(" "),t("p",[t("code",[e._v("XPathExpression")]),e._v("只允许对单个预编译表达式求值。一个更灵活但更慢的替代方案是"),t("code",[e._v("XpathTemplate")]),e._v("。该类遵循通篇 Spring 中使用的公共模板模式("),t("code",[e._v("JdbcTemplate")]),e._v(","),t("code",[e._v("JmsTemplate")]),e._v(",以及其他)。下面的清单展示了一个示例:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('package sample;\n\npublic class MyXPathClass {\n\n private XPathOperations template = new Jaxp13XPathTemplate();\n\n public void doXPath(Source source) {\n String name = template.evaluateAsString("/Contacts/Contact/Name", request);\n // do something with name\n }\n\n}\n')])])]),t("h3",{attrs:{id:"_4-4-消息日志记录和跟踪"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-消息日志记录和跟踪"}},[e._v("#")]),e._v(" 4.4.消息日志记录和跟踪")]),e._v(" "),t("p",[e._v("在开发或调试 Web 服务时,在消息到达或发送之前查看消息的内容可能非常有用。 Spring Web 服务通过标准的 Commons 日志记录接口提供了这种功能。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("确保使用 Commons 日志记录版本 1.1 或更高版本。较早的版本存在类加载问题,并且不与 log4j 跟踪级别集成。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("要记录所有服务器端消息,请将"),t("code",[e._v("org.springframework.ws.server.MessageTracing")]),e._v("记录器级别设置为"),t("code",[e._v("DEBUG")]),e._v("或"),t("code",[e._v("TRACE")]),e._v("。在"),t("code",[e._v("DEBUG")]),e._v("级别上,只记录有效负载根元素。在"),t("code",[e._v("TRACE")]),e._v("级别上,将记录整个消息内容。如果只想记录已发送的消息,请使用"),t("code",[e._v("org.springframework.ws.server.MessageTracing.sent")]),e._v("记录器。类似地,你可以使用"),t("code",[e._v("org.springframework.ws.server.MessageTracing.received")]),e._v("来记录仅接收到的消息。")]),e._v(" "),t("p",[e._v("在客户端,存在类似的记录器:"),t("code",[e._v("org.springframework.ws.client.MessageTracing.sent")]),e._v("和"),t("code",[e._v("org.springframework.ws.client.MessageTracing.received")]),e._v("。")]),e._v(" "),t("p",[e._v("下面的"),t("code",[e._v("log4j.properties")]),e._v("配置文件示例记录了客户端上已发送消息的全部内容,并且只记录了客户端接收消息的有效负载根元素。在服务器端,对发送和接收的消息都记录了有效负载根:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("log4j.rootCategory=INFO, stdout\nlog4j.logger.org.springframework.ws.client.MessageTracing.sent=TRACE\nlog4j.logger.org.springframework.ws.client.MessageTracing.received=DEBUG\n\nlog4j.logger.org.springframework.ws.server.MessageTracing=DEBUG\n\nlog4j.appender.stdout=org.apache.log4j.ConsoleAppender\nlog4j.appender.stdout.layout=org.apache.log4j.PatternLayout\nlog4j.appender.stdout.layout.ConversionPattern=%p [%c{3}] %m%n\n")])])]),t("p",[e._v("在这种配置下,一个典型的输出是:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('TRACE [client.MessageTracing.sent] Sent request [\n\n \n spring-ws\n org.springframework.ws.transport.http.MessageDispatcherServlet\n 1\n \n\n \n spring-ws\n /*\n \n\n\n")])])]),t("p",[e._v("在前面的示例中,所有请求都由"),t("code",[e._v("spring-ws``MessageDispatcherServlet")]),e._v("处理。这仅仅是设置 Spring Web 服务的第一步,因为 Spring-WS 框架使用的各种组件 bean 也需要进行配置。该配置由标准 Spring XML"),t("code",[e._v("")]),e._v("定义组成。因为"),t("code",[e._v("MessageDispatcherServlet")]),e._v("是标准的 Spring "),t("code",[e._v("DispatcherServlet")]),e._v(",所以它在你的 Web 应用程序的"),t("code",[e._v("WEB-INF")]),e._v("目录中查找名为[ Servlet-name]- Servlet.xml 的文件,并在 Spring 容器中创建在其中定义的 bean。在前面的示例中,它查找“/WEB-INF/ Spring-WS- Servlet.xml”。这个文件包含所有的 Spring Web 服务 bean,比如端点、编组器等等。")]),e._v(" "),t("p",[e._v("作为"),t("code",[e._v("web.xml")]),e._v("的替代方案,如果你在 Servlet 3+ 环境上运行,则可以通过编程方式配置 Spring-WS。为此, Spring-WS 提供了许多抽象基类,它们扩展了 Spring 框架中的"),t("code",[e._v("WebApplicationInitializer")]),e._v("接口。如果在 Bean 定义中也使用"),t("code",[e._v("@Configuration")]),e._v("类,则应扩展"),t("code",[e._v("AbstractAnnotationConfigMessageDispatcherServletInitializer")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public class MyServletInitializer\n extends AbstractAnnotationConfigMessageDispatcherServletInitializer {\n\n @Override\n protected Class[] getRootConfigClasses() {\n return new Class[]{MyRootConfig.class};\n }\n\n @Override\n protected Class[] getServletConfigClasses() {\n return new Class[]{MyEndpointConfig.class};\n }\n\n}\n")])])]),t("p",[e._v("在前面的示例中,我们告诉 Spring 端点 Bean 定义可以在"),t("code",[e._v("MyEndpointConfig")]),e._v("类中找到(这是"),t("code",[e._v("@Configuration")]),e._v("类)。 Bean 其他定义(通常是服务、存储库等)可以在"),t("code",[e._v("MyRootConfig")]),e._v("类中找到。默认情况下,"),t("code",[e._v("AbstractAnnotationConfigMessageDispatcherServletInitializer")]),e._v("将 Servlet 映射到两个模式:"),t("code",[e._v("/services")]),e._v("和"),t("code",[e._v("*.wsdl")]),e._v(",尽管你可以通过重写"),t("code",[e._v("getServletMappings()")]),e._v("方法对此进行更改。有关"),t("code",[e._v("MessageDispatcherServlet")]),e._v("的编程配置的更多详细信息,请参阅["),t("code",[e._v("AbstractMessageDispatcherServletInitializer")]),e._v('](https://DOCS. Spring.io/ Spring-ws/DOCS/current/org/springframework/ws/transport/http/support/AbstractMessageDispatcherServletInitializer.html)和[<](https://DOCS. Spring..io//// Spring-Messessframews/currumblessframews/curractork/sprintionframews/support/initializer.html)')]),e._v(" "),t("h5",{attrs:{id:"自动-wsdl-曝光"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#自动-wsdl-曝光"}},[e._v("#")]),e._v(" 自动 WSDL 曝光")]),e._v(" "),t("p",[t("code",[e._v("MessageDispatcherServlet")]),e._v("自动检测在其 Spring 容器中定义的任何"),t("code",[e._v("WsdlDefinition")]),e._v("bean。所有检测到的"),t("code",[e._v("WsdlDefinition")]),e._v("bean 也都通过"),t("code",[e._v("WsdlDefinitionHandlerAdapter")]),e._v("公开。这是一种方便的方式,可以通过定义一些 bean 向客户机公开你的 WSDL。")]),e._v(" "),t("p",[e._v("通过示例的方式,考虑下面的"),t("code",[e._v("")]),e._v("定义,在 Spring-WS 配置文件("),t("code",[e._v("/WEB-INF/[servlet-name]-servlet.xml")]),e._v(")中定义。请注意"),t("code",[e._v("id")]),e._v("属性的值,因为它是在公开 WSDL 时使用的。")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n')])])]),t("p",[e._v("或者,它可以是"),t("code",[e._v("@Bean")]),e._v("类中的"),t("code",[e._v("@Configuration")]),e._v("方法:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('@Bean\npublic SimpleWsdl11Definition orders() {\n\treturn new SimpleWsdl11Definition(new ClassPathResource("orders.wsdl"));\n}\n')])])]),t("p",[e._v("你可以通过"),t("code",[e._v("GET")]),e._v("请求访问在 Classpath 上的"),t("code",[e._v("orders.wsdl")]),e._v("文件中定义的 WSDL,该请求具有以下形式的 URL(酌情替换主机、端口和 Servlet 上下文路径):")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("http://localhost:8080/spring-ws/orders.wsdl\n")])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("所有"),t("code",[e._v("WsdlDefinition")]),e._v(" Bean 定义都由"),t("code",[e._v("MessageDispatcherServlet")]),e._v("在其 Bean 名称下以"),t("code",[e._v(".wsdl")]),e._v("为后缀的"),t("code",[e._v("MessageDispatcherServlet")]),e._v("公开。因此,如果 Bean 名称是"),t("code",[e._v("echo")]),e._v(",主机名称是"),t("code",[e._v("server")]),e._v(",而 Servlet 上下文(WAR 名称)是"),t("code",[e._v("spring-ws")]),e._v(",则可以在"),t("code",[e._v("[http://server/spring-ws/echo.wsdl](http://server/spring-ws/echo.wsdl)")]),e._v("处找到 WSDL。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[t("code",[e._v("MessageDispatcherServlet")]),e._v("(更准确地说是"),t("code",[e._v("WsdlDefinitionHandlerAdapter")]),e._v(")的另一个不错的特性是,它可以转换它公开的所有 WSDL 的"),t("code",[e._v("location")]),e._v("的值,以反映传入请求的 URL。")]),e._v(" "),t("p",[e._v("请注意,这个"),t("code",[e._v("location")]),e._v("转换特性在默认情况下是关闭的。要打开此功能,你需要为"),t("code",[e._v("MessageDispatcherServlet")]),e._v("指定一个初始化参数:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("\n\n \n spring-ws\n org.springframework.ws.transport.http.MessageDispatcherServlet\n \n transformWsdlLocations\n true\n \n \n\n \n spring-ws\n /*\n \n\n\n")])])]),t("p",[e._v("如果使用"),t("code",[e._v("AbstractAnnotationConfigMessageDispatcherServletInitializer")]),e._v(",启用转换就像覆盖"),t("code",[e._v("isTransformWsdlLocations()")]),e._v("方法以返回"),t("code",[e._v("true")]),e._v("一样简单。")]),e._v(" "),t("p",[e._v("请在["),t("code",[e._v("WsdlDefinitionHandlerAdapter")]),e._v("](https://DOCS. Spring.io/ Spring-ws/DOCS/current/org/springframework/ws/transport/http/wsdldefinitionhandleradapter.html)类上查看类级 Javadoc,以了解整个转换过程的更多信息。")]),e._v(" "),t("p",[e._v("Spring 作为手工编写 WSDL 并用"),t("code",[e._v("")]),e._v("公开它的一种替代方法,Web 服务还可以从 XSD 模式生成 WSDL。这是"),t("a",{attrs:{href:"#tutorial-publishing-wsdl"}},[e._v("发布 WSDL")]),e._v("中显示的方法。下一个应用程序上下文片段展示了如何创建这样的动态 WSDL 文件:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n\n')])])]),t("p",[e._v("或者,你可以使用 Java"),t("code",[e._v("@Bean")]),e._v("方法:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('@Bean\npublic DefaultWsdl11Definition orders() {\n DefaultWsdl11Definition definition = new DefaultWsdl11Definition();\n definition.setPortTypeName("Orders");\n definition.setLocationUri("http://localhost:8080/ordersService/");\n definition.setSchema(new SimpleXsdSchema(new ClassPathResource("echo.xsd")));\n\n return definition;\n}\n')])])]),t("p",[t("code",[e._v("")]),e._v("元素依赖于"),t("code",[e._v("DefaultWsdl11Definition")]),e._v("类。这个定义类在["),t("code",[e._v("org.springframework.ws.wsdl.wsdl11.provider")]),e._v("](https://DOCS. Spring.io/ Spring-ws/sites/1.5/apDOCS/org/springframework/ws/wsdl/wsdl11/provider/package-summary.html)包和["),t("code",[e._v("ProviderBasedWsdl4jDefinition")]),e._v("](https://DOCS. Spring.io/ Spring-ws/docs/current/org/springframews/wsdlframews/wsdl/wsdl/wsdl11/providwsdl4jsdl)类中使用 WSDL 提供程序,以生成首次如果有必要,请查看这些类的类级 Javadoc,以了解如何扩展此机制。")]),e._v(" "),t("p",[t("code",[e._v("DefaultWsdl11Definition")]),e._v("(因此,"),t("code",[e._v("")]),e._v("标记)通过使用约定从 XSD 模式构建 WSDL。它对模式中的所有"),t("code",[e._v("element")]),e._v("元素进行迭代,并为所有元素创建一个"),t("code",[e._v("message")]),e._v("。接下来,它为所有以定义的请求或响应后缀结尾的消息创建一个 WSDL。默认的请求后缀是"),t("code",[e._v("Request")]),e._v("。默认的响应后缀是"),t("code",[e._v("Response")]),e._v(",但是可以通过分别在"),t("code",[e._v("")]),e._v("上设置"),t("code",[e._v("requestSuffix")]),e._v("和"),t("code",[e._v("responseSuffix")]),e._v("属性来更改这些后缀。它还基于操作构建了"),t("code",[e._v("portType")]),e._v("、"),t("code",[e._v("binding")]),e._v("和"),t("code",[e._v("service")]),e._v("。")]),e._v(" "),t("p",[e._v("例如,如果我们的"),t("code",[e._v("Orders.xsd")]),e._v("模式定义了"),t("code",[e._v("GetOrdersRequest")]),e._v("和"),t("code",[e._v("GetOrdersResponse")]),e._v("元素,"),t("code",[e._v("")]),e._v("将创建一个"),t("code",[e._v("GetOrdersRequest")]),e._v("和"),t("code",[e._v("GetOrdersResponse")]),e._v("消息和一个"),t("code",[e._v("GetOrders")]),e._v("操作,该操作被放在"),t("code",[e._v("Orders")]),e._v("端口类型中。")]),e._v(" "),t("p",[e._v("要使用多个模式(通过包含或导入),你可以将 CommonsXMLSchema 放在类路径上。如果 Commons XMLSchema 位于类路径上,则"),t("code",[e._v("")]),e._v("元素遵循所有 XSD 导入,并将它们作为单个 XSD 在 WSDL 中包含和内联。这极大地简化了模式的部署,同时也使单独编辑模式成为可能。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("尽管在运行时从 XSD 创建 WSDL 很方便,但这种方法也有一些缺点。首先,尽管我们试图在不同版本之间保持 WSDL 生成过程的一致性,但它仍然有可能发生变化(略有变化)。其次,生成速度有点慢,但是,一旦生成,WSDL 就会被缓存,以备以后参考。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("因此,你应该仅在项目的开发阶段使用"),t("code",[e._v("")]),e._v("。我们建议使用你的浏览器下载生成的 WSDL,将其存储在项目中,并用"),t("code",[e._v("")]),e._v("公开它。这是确保 WSDL 不会随时间变化的唯一方法。")]),e._v(" "),t("h4",{attrs:{id:"_5-2-2-在dispatcherservlet中连接-spring-ws"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-2-在dispatcherservlet中连接-spring-ws"}},[e._v("#")]),e._v(" 5.2.2.在"),t("code",[e._v("DispatcherServlet")]),e._v("中连接 Spring-WS")]),e._v(" "),t("p",[e._v("作为"),t("code",[e._v("MessageDispatcherServlet")]),e._v("的替代方案,你可以在标准 Spring-Web MVC"),t("code",[e._v("DispatcherServlet")]),e._v("中连接"),t("code",[e._v("MessageDispatcher")]),e._v("。默认情况下,"),t("code",[e._v("DispatcherServlet")]),e._v("只能委托给"),t("code",[e._v("Controllers")]),e._v(",但是我们可以通过在 Servlet 的 Web 应用程序上下文中添加"),t("code",[e._v("WebServiceMessageReceiverHandlerAdapter")]),e._v("来指示它委托给"),t("code",[e._v("MessageDispatcher")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n \n \n\n ...\n\n \n\n\n')])])]),t("p",[e._v("请注意,通过显式地添加"),t("code",[e._v("WebServiceMessageReceiverHandlerAdapter")]),e._v(",Dispatcher Servlet 不会加载默认的适配器,并且无法处理标准的 Spring-MVC"),t("code",[e._v("@Controllers")]),e._v("。因此,我们在末尾加上"),t("code",[e._v("RequestMappingHandlerAdapter")]),e._v("。")]),e._v(" "),t("p",[e._v("以类似的方式,你可以连接"),t("code",[e._v("WsdlDefinitionHandlerAdapter")]),e._v("以确保"),t("code",[e._v("DispatcherServlet")]),e._v("可以处理"),t("code",[e._v("WsdlDefinition")]),e._v("接口的实现:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n\n \n \n \n myServiceDefinition\n \n \n \n \n\n \n\n \n \n \n\n ...\n\n\n')])])]),t("h4",{attrs:{id:"_5-2-3-jms-运输"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-3-jms-运输"}},[e._v("#")]),e._v(" 5.2.3.JMS 运输")]),e._v(" "),t("p",[e._v("Spring Web 服务通过在 Spring 框架中提供的 JMS 功能支持服务器端 JMS 处理。 Spring Web 服务提供"),t("code",[e._v("WebServiceMessageListener")]),e._v("以插入到"),t("code",[e._v("MessageListenerContainer")]),e._v("中。此消息侦听器需要"),t("code",[e._v("WebServiceMessageFactory")]),e._v("和"),t("code",[e._v("MessageDispatcher")]),e._v("才能操作。下面的配置示例展示了这一点:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n \n \n\n \n\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n\n')])])]),t("h4",{attrs:{id:"_5-2-4-电子邮件传输"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-4-电子邮件传输"}},[e._v("#")]),e._v(" 5.2.4.电子邮件传输")]),e._v(" "),t("p",[e._v("Spring 除了 HTTP 和 JMS,Web 服务还提供服务器端电子邮件处理。这个功能是通过"),t("code",[e._v("MailMessageReceiver")]),e._v("类提供的。该类监视 POP3 或 IMAP 文件夹,将电子邮件转换为"),t("code",[e._v("WebServiceMessage")]),e._v(",并使用 SMTP 发送任何响应。你可以通过"),t("code",[e._v("storeUri")]),e._v("配置主机名,该文件夹指示要监视请求的邮件文件夹(通常是 POP3 或 IMAP 文件夹),以及"),t("code",[e._v("transportUri")]),e._v(",该文件夹指示用于发送响应的服务器(通常是 SMTP 服务器)。")]),e._v(" "),t("p",[e._v("你可以配置"),t("code",[e._v("MailMessageReceiver")]),e._v("如何使用可插入策略监视传入消息:"),t("code",[e._v("MonitoringStrategy")]),e._v("。默认情况下,使用轮询策略,每五分钟对传入的文件夹进行一次新消息的轮询。你可以通过在策略上设置"),t("code",[e._v("pollingInterval")]),e._v("属性来更改此间隔。默认情况下,所有"),t("code",[e._v("MonitoringStrategy")]),e._v("实现都会删除已处理的消息。可以通过设置"),t("code",[e._v("deleteMessages")]),e._v("属性来更改此设置。")]),e._v(" "),t("p",[e._v("作为轮询方法的一种替代方法(轮询方法非常低效),有一种使用 IMAP IDLE 的监视策略。IDLE 命令是 IMAP 电子邮件协议的可选扩展,它允许邮件服务器异步向"),t("code",[e._v("MailMessageReceiver")]),e._v("发送新的消息更新。如果使用支持 IDLE 命令的 IMAP 服务器,则可以将"),t("code",[e._v("ImapIdleMonitoringStrategy")]),e._v("插入到"),t("code",[e._v("monitoringStrategy")]),e._v("属性中。除了支持的服务器外,还需要使用 JavaMail1.4.1 或更高版本。")]),e._v(" "),t("p",[e._v("以下配置展示了如何使用服务器端电子邮件支持,覆盖了每 30 秒(30.000 毫秒)检查一次的默认轮询间隔:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n\n')])])]),t("h4",{attrs:{id:"_5-2-5-嵌入式-http-服务器传输"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-5-嵌入式-http-服务器传输"}},[e._v("#")]),e._v(" 5.2.5.嵌入式 HTTP 服务器传输")]),e._v(" "),t("p",[e._v("Spring Web 服务提供了基于 Sun 的 JRE1.6的传输。嵌入式 HTTP 服务器是一个独立的服务器,配置起来很简单。它提供了一种比传统 Servlet 容器更轻的替代品。")]),e._v(" "),t("p",[e._v("使用嵌入式 HTTP 服务器时,不需要外部部署描述符("),t("code",[e._v("web.xml")]),e._v(")。你只需要定义服务器的一个实例,并将其配置为处理传入的请求。核心 Spring 框架中的远程处理模块包含一个用于 HTTP 服务器的方便的工厂 Bean:"),t("code",[e._v("SimpleHttpServerFactoryBean")]),e._v("。最重要的属性是"),t("code",[e._v("contexts")]),e._v(",它将上下文路径映射到相应的"),t("code",[e._v("HttpHandler")]),e._v("实例。")]),e._v(" "),t("p",[e._v("Spring Web 服务提供了"),t("code",[e._v("HttpHandler")]),e._v("接口的两种实现方式:["),t("code",[e._v("WsdlDefinitionHttpHandler")]),e._v('](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/transport/http/wsdldefinitionhttphandler.html)和[<<<](https:///DOCS.io/ Spring-ws/DOCS/current/api/org/api/servicemessler.html)。前者将传入的 GET 请求映射到'),t("code",[e._v("WsdlDefinition")]),e._v("。后者负责处理 Web 服务消息的 POST 请求,因此需要"),t("code",[e._v("WebServiceMessageFactory")]),e._v("(通常是"),t("code",[e._v("SaajSoapMessageFactory")]),e._v(")和"),t("code",[e._v("WebServiceMessageReceiver")]),e._v("(通常是"),t("code",[e._v("SoapMessageDispatcher")]),e._v(")来完成其任务。")]),e._v(" "),t("p",[e._v("要绘制与 Servlet 世界的相似之处,"),t("code",[e._v("contexts")]),e._v("属性在"),t("code",[e._v("web.xml")]),e._v("中扮演 Servlet 映射的角色,而"),t("code",[e._v("WebServiceMessageReceiverHttpHandler")]),e._v("则相当于"),t("code",[e._v("MessageDispatcherServlet")]),e._v("。")]),e._v(" "),t("p",[e._v("以下片段展示了 HTTP 服务器传输的配置示例:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n \n \n\n \n \n \n\n \n \n \n \n \n \n \n \n\n \n \n \n \n\n \n \n \n\n')])])]),t("p",[e._v("有关"),t("code",[e._v("SimpleHttpServerFactoryBean")]),e._v("的更多信息,请参见"),t("a",{attrs:{href:"http://static.springframework.org/spring/docs/2.5.x/api/org/springframework/remoting/support/SimpleHttpServerFactoryBean.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_5-2-6-xmpp-运输"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-6-xmpp-运输"}},[e._v("#")]),e._v(" 5.2.6.XMPP 运输")]),e._v(" "),t("p",[e._v("Spring Web 服务 2.0 引入了对 XMPP 的支持,也称为 Jabber。该支持基于"),t("a",{attrs:{href:"https://www.igniterealtime.org/projects/smack/index.jsp",target:"_blank",rel:"noopener noreferrer"}},[e._v("Smack"),t("OutboundLink")],1),e._v("库。")]),e._v(" "),t("p",[e._v("Spring 用于 XMPP 的 Web 服务支持非常类似于其它传输:有一个用于"),t("code",[e._v("XmppMessageSender")]),e._v("的"),t("code",[e._v("WebServiceTemplate")]),e._v("和一个用于与"),t("code",[e._v("XmppMessageReceiver")]),e._v("一起使用的"),t("code",[e._v("MessageDispatcher")]),e._v("。")]),e._v(" "),t("p",[e._v("下面的示例展示了如何设置服务器端 XMPP 组件:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n \n \n \n \n\n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n\n\n')])])]),t("h4",{attrs:{id:"_5-2-7-姆托姆"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-7-姆托姆"}},[e._v("#")]),e._v(" 5.2.7.姆托姆")]),e._v(" "),t("p",[t("a",{attrs:{href:"https://en.wikipedia.org/wiki/Message_Transmission_Optimization_Mechanism",target:"_blank",rel:"noopener noreferrer"}},[e._v("MTOM"),t("OutboundLink")],1),e._v("是用于向 Web 服务发送和从 Web 服务发送二进制数据的机制。你可以通过"),t("a",{attrs:{href:"https://github.com/spring-projects/spring-ws-samples/tree/main/mtom",target:"_blank",rel:"noopener noreferrer"}},[e._v("MTOM 样本"),t("OutboundLink")],1),e._v("查看如何使用 Spring WS 实现这一点。")]),e._v(" "),t("h3",{attrs:{id:"_5-3-端点"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-3-端点"}},[e._v("#")]),e._v(" 5.3.端点")]),e._v(" "),t("p",[e._v("端点是 Spring-WS 服务器端支持的核心概念。端点提供对应用程序行为的访问,该行为通常由业务服务接口定义。端点解释 XML 请求消息,并使用该输入(通常)调用业务服务上的方法。该服务调用的结果表示为响应消息。 Spring-WS 具有各种各样的端点,并且使用各种方式来处理 XML 消息并创建响应。")]),e._v(" "),t("p",[e._v("你可以通过使用"),t("code",[e._v("@Endpoint")]),e._v("注释对类进行注释来创建端点。在这个类中,你可以通过使用各种各样的参数类型(例如 DOM 元素、JAXB2 对象等)来定义一个或多个处理传入 XML 请求的方法。你可以通过使用另一个注释(通常是"),t("code",[e._v("@PayloadRoot")]),e._v(")来指示方法可以处理的消息类型。")]),e._v(" "),t("p",[e._v("考虑以下示例端点:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('package samples;\n\nimport org.w3c.dom.Element;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.ws.server.endpoint.annotation.Endpoint;\nimport org.springframework.ws.server.endpoint.annotation.PayloadRoot;\nimport org.springframework.ws.soap.SoapHeader;\n\n@Endpoint (1)\npublic class AnnotationOrderEndpoint {\n\n private final OrderService orderService;\n\n @Autowired (2)\n public AnnotationOrderEndpoint(OrderService orderService) {\n this.orderService = orderService;\n }\n\n @PayloadRoot(localPart = "order", namespace = "http://samples") (5)\n public void order(@RequestPayload Element orderElement) { (3)\n Order order = createOrder(orderElement);\n orderService.createOrder(order);\n }\n\n @PayloadRoot(localPart = "orderRequest", namespace = "http://samples") (5)\n @ResponsePayload\n public Order getOrder(@RequestPayload OrderRequest orderRequest, SoapHeader header) { (4)\n checkSoapHeaderForSomething(header);\n return orderService.getOrder(orderRequest.getId());\n }\n\n ...\n\n}\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[e._v("该类被注释为"),t("code",[e._v("@Endpoint")]),e._v(",并将其标记为 Spring-WS 端点。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[e._v("构造函数被标记为"),t("code",[e._v("@Autowired")]),e._v(",以便将"),t("code",[e._v("OrderService")]),e._v("业务服务注入到该端点。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("3")])]),e._v(" "),t("td",[t("code",[e._v("order")]),e._v("方法将"),t("code",[e._v("Element")]),e._v("(注释为"),t("code",[e._v("@RequestPayload")]),e._v(")作为参数。这意味着消息的有效负载将作为 DOM 元素在此方法上传递。该方法具有"),t("code",[e._v("void")]),e._v("返回类型,表示未发送响应消息。"),t("br"),e._v("有关端点方法的更多信息,请参见["),t("code",[e._v("@Endpoint")]),e._v("处理方法](#server-atendpoint-methods)。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("4")])]),e._v(" "),t("td",[t("code",[e._v("getOrder")]),e._v("方法将"),t("code",[e._v("OrderRequest")]),e._v("(也用"),t("code",[e._v("@RequestPayload")]),e._v("注释)作为参数。这个参数是一个 JAXB2 支持的对象(用"),t("code",[e._v("@XmlRootElement")]),e._v("注释)。这意味着消息的有效负载将作为未编组对象传递给该方法。"),t("code",[e._v("SoapHeader")]),e._v("类型也作为参数给出。在调用时,此参数包含请求消息的 SOAP 头。该方法还用"),t("code",[e._v("@ResponsePayload")]),e._v("注释,表示返回值(the"),t("code",[e._v("Order")]),e._v(")用作响应消息的有效负载。"),t("br"),e._v("有关端点方法的更多信息,请参见["),t("code",[e._v("@Endpoint")]),e._v("处理方法](#server-atendpoint-methods)。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("5")])]),e._v(" "),t("td",[e._v("这个端点的两个处理方法被标记为"),t("code",[e._v("@PayloadRoot")]),e._v(",指示该方法可以处理哪种类型的请求消息:对于具有"),t("code",[e._v("orderRequest")]),e._v("本地名称和"),t("code",[e._v("[http://samples](http://samples)")]),e._v("命名空间 URI 的请求,调用"),t("code",[e._v("getOrder")]),e._v("方法。对于具有"),t("code",[e._v("order")]),e._v("本地名称的请求,调用 Order 方法。"),t("br"),e._v("有关"),t("code",[e._v("@PayloadRoot")]),e._v("的更多信息,请参见"),t("a",{attrs:{href:"#server-endpoint-mapping"}},[e._v("端点映射")]),e._v("。")])])])]),e._v(" "),t("p",[e._v("要启用对"),t("code",[e._v("@Endpoint")]),e._v("和相关的 Spring-WS 注释的支持,你需要在 Spring 应用程序上下文中添加以下内容:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n *\n\n\n')])])]),t("p",[e._v("或者,如果使用"),t("code",[e._v("@Configuration")]),e._v("类而不是 Spring XML,则可以使用"),t("code",[e._v("@EnableWs")]),e._v("对配置类进行注释:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("@EnableWs\n@Configuration\npublic class EchoConfig {\n\n // @Bean definitions go here\n\n}\n")])])]),t("p",[e._v("要定制"),t("code",[e._v("@EnableWs")]),e._v("配置,你可以实现"),t("code",[e._v("WsConfigurer")]),e._v(",或者,更好的是,扩展"),t("code",[e._v("WsConfigurerAdapter")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("@Configuration\n@EnableWs\n@ComponentScan(basePackageClasses = { MyConfiguration.class })\npublic class MyConfiguration extends WsConfigurerAdapter {\n\n @Override\n public void addInterceptors(List interceptors) {\n interceptors.add(new MyInterceptor());\n }\n\n @Override\n public void addArgumentResolvers(List argumentResolvers) {\n argumentResolvers.add(new MyArgumentResolver());\n }\n\n // More overridden methods ...\n}\n")])])]),t("p",[e._v("在接下来的几节中,给出了对"),t("code",[e._v("@Endpoint")]),e._v("编程模型的更详细的描述。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("与任何其他 Spring Bean 一样,缺省情况下,端点的作用域为单例。也就是说, Bean 定义的一个实例是为每个容器创建的。作为单例意味着多个线程可以同时使用它,因此端点必须是线程安全的。如果你想使用不同的作用域,例如原型,请参见"),t("a",{attrs:{href:"https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-scopes",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Reference documentation"),t("OutboundLink")],1),e._v("。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("Note that all abstract base classes provided in Spring-WS are thread safe, unless otherwise indicated in the class-level Javadoc.\n")])])]),t("h4",{attrs:{id:"_5-3-1-endpoint处理方法"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-3-1-endpoint处理方法"}},[e._v("#")]),e._v(" 5.3.1."),t("code",[e._v("@Endpoint")]),e._v("处理方法")]),e._v(" "),t("p",[e._v("要使端点实际处理传入的 XML 消息,它需要有一个或多个处理方法。处理方法可以使用范围很广的参数和返回类型。但是,它们通常有一个包含消息有效负载的参数,并且它们返回响应消息的有效负载(如果有的话)。本节将讨论支持哪些参数和返回类型。")]),e._v(" "),t("p",[e._v("为了表示方法可以处理哪种类型的消息,该方法通常使用"),t("code",[e._v("@PayloadRoot")]),e._v("或"),t("code",[e._v("@SoapAction")]),e._v("注释。你可以在"),t("a",{attrs:{href:"#server-endpoint-mapping"}},[e._v("端点映射")]),e._v("中了解有关这些注释的更多信息。")]),e._v(" "),t("p",[e._v("下面的示例展示了一个处理方法:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('@PayloadRoot(localPart = "order", namespace = "http://samples")\npublic void order(@RequestPayload Element orderElement) {\n Order order = createOrder(orderElement);\n orderService.createOrder(order);\n}\n')])])]),t("p",[t("code",[e._v("order")]),e._v("方法将"),t("code",[e._v("Element")]),e._v("(注释为"),t("code",[e._v("@RequestPayload")]),e._v(")作为参数。这意味着消息的有效负载将作为 DOM 元素在此方法上传递。该方法具有"),t("code",[e._v("void")]),e._v("返回类型,表示未发送任何响应消息。")]),e._v(" "),t("h5",{attrs:{id:"处理方法参数"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#处理方法参数"}},[e._v("#")]),e._v(" 处理方法参数")]),e._v(" "),t("p",[e._v("处理方法通常具有一个或多个参数,这些参数引用传入 XML 消息的各个部分。最常见的情况是,处理方法只有一个参数,该参数映射到消息的有效负载,但它也可以映射到请求消息的其他部分,例如 SOAP 头。本节描述了可以在处理方法签名中使用的参数。")]),e._v(" "),t("p",[e._v("要将参数映射到请求消息的有效负载,你需要使用"),t("code",[e._v("@RequestPayload")]),e._v("注释对此参数进行注释。这个注释告诉 Spring-WS 参数需要绑定到请求有效负载。")]),e._v(" "),t("p",[e._v("下表描述了受支持的参数类型。它显示了支持的类型,是否应该用"),t("code",[e._v("@RequestPayload")]),e._v("注释参数,以及任何其他注释。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[e._v("Name")]),e._v(" "),t("th",[e._v("支持的参数类型")]),e._v(" "),t("th",[t("code",[e._v("@RequestPayload")]),e._v(" required?")]),e._v(" "),t("th",[e._v("Additional notes")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[e._v("TrAX")]),e._v(" "),t("td",[t("code",[e._v("javax.xml.transform.Source")]),e._v("和子接口("),t("code",[e._v("DOMSource")]),e._v(","),t("code",[e._v("SAXSource")]),e._v(","),t("code",[e._v("StreamSource")]),e._v(",和"),t("code",[e._v("StAXSource")]),e._v(")")]),e._v(" "),t("td",[e._v("Yes")]),e._v(" "),t("td",[e._v("Enabled by default.")])]),e._v(" "),t("tr",[t("td",[e._v("W3C DOM")]),e._v(" "),t("td",[t("code",[e._v("org.w3c.dom.Element")])]),e._v(" "),t("td",[e._v("Yes")]),e._v(" "),t("td",[e._v("Enabled by default")])]),e._v(" "),t("tr",[t("td",[e._v("dom4j")]),e._v(" "),t("td",[t("code",[e._v("org.dom4j.Element")])]),e._v(" "),t("td",[e._v("Yes")]),e._v(" "),t("td",[e._v("Enabled when dom4j is on the classpath.")])]),e._v(" "),t("tr",[t("td",[e._v("JDOM")]),e._v(" "),t("td",[t("code",[e._v("org.jdom.Element")])]),e._v(" "),t("td",[e._v("Yes")]),e._v(" "),t("td",[e._v("Enabled when JDOM is on the classpath.")])]),e._v(" "),t("tr",[t("td",[e._v("XOM")]),e._v(" "),t("td",[t("code",[e._v("nu.xom.Element")])]),e._v(" "),t("td",[e._v("Yes")]),e._v(" "),t("td",[e._v("Enabled when XOM is on the classpath.")])]),e._v(" "),t("tr",[t("td",[e._v("StAX")]),e._v(" "),t("td",[t("code",[e._v("javax.xml.stream.XMLStreamReader")]),e._v("和"),t("code",[e._v("javax.xml.stream.XMLEventReader")])]),e._v(" "),t("td",[e._v("Yes")]),e._v(" "),t("td",[e._v("Enabled when StAX is on the classpath.")])]),e._v(" "),t("tr",[t("td",[e._v("XPath")]),e._v(" "),t("td",[e._v("任何布尔的,double 的,"),t("code",[e._v("String")]),e._v(","),t("code",[e._v("org.w3c.Node")]),e._v(","),t("code",[e._v("org.w3c.dom.NodeList")]),e._v("的,或者可以从"),t("code",[e._v("String")]),e._v("转换为 Spring "),t("a",{attrs:{href:"https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#core-convert-ConversionService-API",target:"_blank",rel:"noopener noreferrer"}},[e._v("转换服务"),t("OutboundLink")],1),e._v("的类型,并用"),t("code",[e._v("@XPathParam")]),e._v("注释。")]),e._v(" "),t("td",[e._v("No")]),e._v(" "),t("td",[e._v("Enabled by default, see "),t("a",{attrs:{href:"#server-xpath-param"}},[e._v("the section called "),t("code",[e._v("XPathParam")])]),e._v(".")])]),e._v(" "),t("tr",[t("td",[e._v("Message context")]),e._v(" "),t("td",[t("code",[e._v("org.springframework.ws.context.MessageContext")])]),e._v(" "),t("td",[e._v("No")]),e._v(" "),t("td",[e._v("Enabled by default.")])]),e._v(" "),t("tr",[t("td",[e._v("SOAP")]),e._v(" "),t("td",[t("code",[e._v("org.springframework.ws.soap.SoapMessage")]),e._v(","),t("code",[e._v("org.springframework.ws.soap.SoapBody")]),e._v(","),t("code",[e._v("org.springframework.ws.soap.SoapEnvelope")]),e._v(","),t("code",[e._v("org.springframework.ws.soap.SoapHeader")]),e._v(",以及"),t("code",[e._v("org.springframework.ws.soap.SoapHeaderElement")]),e._v("s 与"),t("code",[e._v("@SoapHeader")]),e._v("注释结合使用。")]),e._v(" "),t("td",[e._v("No")]),e._v(" "),t("td",[e._v("Enabled by default.")])]),e._v(" "),t("tr",[t("td",[e._v("JAXB2")]),e._v(" "),t("td",[e._v("用"),t("code",[e._v("javax.xml.bind.annotation.XmlRootElement")]),e._v("和"),t("code",[e._v("javax.xml.bind.JAXBElement")]),e._v("注释的任何类型。")]),e._v(" "),t("td",[e._v("Yes")]),e._v(" "),t("td",[e._v("Enabled when JAXB2 is on the classpath.")])]),e._v(" "),t("tr",[t("td",[e._v("OXM")]),e._v(" "),t("td",[e._v("Spring OXM["),t("code",[e._v("Unmarshaller")]),e._v("](https://DOCS. Spring.io/ Spring/DOCS/current/ Spring-framework-reference/data-access.html#OXM-marshaller-unmarshaller)支持的任何类型。")]),e._v(" "),t("td",[e._v("Yes")]),e._v(" "),t("td",[e._v("Enabled when the "),t("code",[e._v("unmarshaller")]),e._v(" attribute of "),t("code",[e._v("")]),e._v(" is specified.")])])])]),e._v(" "),t("p",[e._v("下面的几个示例展示了可能的方法签名。将请求消息的有效负载作为 DOM"),t("code",[e._v("org.w3c.dom.Element")]),e._v("调用以下方法:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public void handle(@RequestPayload Element element)\n")])])]),t("p",[e._v("以"),t("code",[e._v("javax.xml.transform.dom.DOMSource")]),e._v("作为请求消息的有效负载调用以下方法。"),t("code",[e._v("header")]),e._v("参数绑定到请求消息的 SOAP 头。")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public void handle(@RequestPayload DOMSource domSource, SoapHeader header)\n")])])]),t("p",[e._v("将请求消息的有效负载解组为"),t("code",[e._v("MyJaxb2Object")]),e._v("(注释为"),t("code",[e._v("@XmlRootElement")]),e._v(")时调用以下方法。消息的有效负载也被指定为 DOM"),t("code",[e._v("Element")]),e._v("。整个"),t("a",{attrs:{href:"#message-context"}},[e._v("消息上下文")]),e._v("作为第三个参数传递。")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public void handle(@RequestPayload MyJaxb2Object requestObject, @RequestPayload Element element, Message messageContext)\n")])])]),t("p",[e._v("正如你所看到的,在定义如何处理方法签名时,有很多可能性。你甚至可以扩展此机制以支持你自己的参数类型。参见["),t("code",[e._v("DefaultMethodEndpointAdapter")]),e._v('](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/server/endpoint/adapter/defaultMethodPointadapter.html)和[](")]),e._v(" is specified.")])])])]),e._v(" "),t("p",[e._v("当涉及到定义处理方法签名时,有很多可能性。甚至可以扩展此机制以支持你自己的参数类型。参见["),t("code",[e._v("DefaultMethodEndpointAdapter")]),e._v("](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/server/endpoint/adapter/defaultMethodPointAdapter.html)和[<"),t("code",[e._v("MethodReturnValueHandler")]),e._v("](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/server/endpoint/endpoint/adapter/methandler.html)的类级 Javadoc,以查看如何。")]),e._v(" "),t("h3",{attrs:{id:"_5-4-端点映射"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-4-端点映射"}},[e._v("#")]),e._v(" 5.4.端点映射")]),e._v(" "),t("p",[e._v("端点映射负责将传入消息映射到适当的端点。默认情况下,一些端点映射是启用的——例如,"),t("code",[e._v("PayloadRootAnnotationMethodEndpointMapping")]),e._v("或"),t("code",[e._v("SoapActionAnnotationMethodEndpointMapping")]),e._v("。然而,我们首先需要研究"),t("code",[e._v("EndpointMapping")]),e._v("的一般概念。")]),e._v(" "),t("p",[e._v("一个"),t("code",[e._v("EndpointMapping")]),e._v("传递一个"),t("code",[e._v("EndpointInvocationChain")]),e._v(",其中包含与传入请求匹配的端点,还可能包含应用于请求和响应的端点拦截器列表。当一个请求进来时,"),t("code",[e._v("MessageDispatcher")]),e._v("将它交给端点映射,让它检查请求并提出一个适当的"),t("code",[e._v("EndpointInvocationChain")]),e._v("。然后"),t("code",[e._v("MessageDispatcher")]),e._v("调用端点和链中的任何拦截器。")]),e._v(" "),t("p",[e._v("可配置端点映射的概念非常强大,它可以选择性地包含拦截器(这反过来可以操作请求、响应或两者)。可以在定制的"),t("code",[e._v("EndpointMapping")]),e._v("实现中内置许多支持功能。例如,定制的端点映射不仅可以基于消息的内容,还可以基于特定的 SOAP 头(或者实际上是多个 SOAP 头)来选择端点。")]),e._v(" "),t("p",[e._v("大多数端点映射继承自"),t("code",[e._v("AbstractEndpointMapping")]),e._v(",它提供了一个“拦截器”属性,这是要使用的拦截器列表。"),t("code",[e._v("EndpointInterceptors")]),e._v("在[拦截请求—"),t("code",[e._v("EndpointInterceptor")]),e._v("接口](#server-endpoint-interceptor)中进行了讨论。此外,还有"),t("code",[e._v("defaultEndpoint")]),e._v(",这是当此端点映射不会导致匹配的端点时使用的默认端点。")]),e._v(" "),t("p",[e._v("正如"),t("a",{attrs:{href:"#server-endpoints"}},[e._v("Endpoints")]),e._v("中所解释的,"),t("code",[e._v("@Endpoint")]),e._v("样式允许你在一个端点类中处理多个请求。这是"),t("code",[e._v("MethodEndpointMapping")]),e._v("的责任。此映射决定了将为传入的请求消息调用哪个方法。")]),e._v(" "),t("p",[e._v("有两个端点映射可以指示对方法的请求:"),t("code",[e._v("PayloadRootAnnotationMethodEndpointMapping")]),e._v("和"),t("code",[e._v("SoapActionAnnotationMethodEndpointMapping")]),e._v("你可以通过在应用程序上下文中使用"),t("code",[e._v("")]),e._v("来启用这两个方法。")]),e._v(" "),t("p",[t("code",[e._v("PayloadRootAnnotationMethodEndpointMapping")]),e._v("使用"),t("code",[e._v("@PayloadRoot")]),e._v("注释,使用"),t("code",[e._v("localPart")]),e._v("和"),t("code",[e._v("namespace")]),e._v("元素,用特定的限定名称标记方法。每当出现带有有效负载根元素的限定名称的消息时,都会调用该方法。有关示例,请参见"),t("a",{attrs:{href:"#server-payload-root-annotation"}},[e._v("above")]),e._v("。")]),e._v(" "),t("p",[e._v("或者,"),t("code",[e._v("SoapActionAnnotationMethodEndpointMapping")]),e._v("使用"),t("code",[e._v("@SoapAction")]),e._v("注释来标记具有特定 SOAP 操作的方法。每当带有这个"),t("code",[e._v("SOAPAction")]),e._v("头的消息出现时,都会调用该方法。")]),e._v(" "),t("h4",{attrs:{id:"_5-4-1-ws-addressing"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-4-1-ws-addressing"}},[e._v("#")]),e._v(" 5.4.1.WS-Addressing")]),e._v(" "),t("p",[e._v("WS-Addressing 指定了一种与传输无关的路由机制。它基于"),t("code",[e._v("To")]),e._v("和"),t("code",[e._v("Action")]),e._v("SOAP 头,它们分别指示 SOAP 消息的目的和意图。此外,WS-Addressing 允许你定义一个返回地址(用于正常消息和错误)和一个唯一的消息标识符,该标识符可用于相关性。有关 WS-Addressing 的更多信息,请参见"),t("a",{attrs:{href:"https://en.wikipedia.org/wiki/WS-Addressing",target:"_blank",rel:"noopener noreferrer"}},[e._v("https://en.wikipedia.org/wiki/WS-Addressing"),t("OutboundLink")],1),e._v("。下面的示例显示了一个 WS-Addressing 消息:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n urn:uuid:21363e0d-2645-4eb7-8afd-2f5ee1bb25cf\n \n http://example.com/business/client1\n \n http://example/com/fabrikam\n http://example.com/fabrikam/mail/Delete\n \n \n \n 42\n \n \n\n')])])]),t("p",[e._v("在前面的示例中,目标设置为"),t("code",[e._v("[http://example/com/fabrikam](http://example/com/fabrikam)")]),e._v(",而操作设置为"),t("code",[e._v("[http://example.com/fabrikam/mail/Delete](http://example.com/fabrikam/mail/Delete)")]),e._v("。此外,还有一个消息标识符和一个回复地址。默认情况下,该地址是“匿名”地址,这表明应该使用与请求相同的通道(即 HTTP 响应)发送响应,但它也可以是另一个地址,如本例中所示。")]),e._v(" "),t("p",[e._v("Spring 在 Web 服务中,WS-Addressing 被实现为端点映射。通过使用此映射,可以将 WS-Addressing 操作与端点关联起来,类似于前面描述的"),t("code",[e._v("SoapActionAnnotationMethodEndpointMapping")]),e._v("。")]),e._v(" "),t("h5",{attrs:{id:"使用annotationactionendpointmapping"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用annotationactionendpointmapping"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("AnnotationActionEndpointMapping")])]),e._v(" "),t("p",[t("code",[e._v("AnnotationActionEndpointMapping")]),e._v("类似于"),t("code",[e._v("SoapActionAnnotationMethodEndpointMapping")]),e._v(",但使用 WS-Addressing 头,而不是 SOAP 动作传输头。")]),e._v(" "),t("p",[e._v("要使用"),t("code",[e._v("AnnotationActionEndpointMapping")]),e._v(",请使用"),t("code",[e._v("@Action")]),e._v("注释来注释处理方法,类似于["),t("code",[e._v("@PayloadRoot")]),e._v("和"),t("code",[e._v("@SoapAction")]),e._v("处理方法](#server-atendpoint-methods)和"),t("a",{attrs:{href:"#server-endpoint-mapping"}},[e._v("端点映射")]),e._v("中描述的注释。下面的示例展示了如何做到这一点:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('package samples;\n\nimport org.springframework.ws.server.endpoint.annotation.Endpoint;\nimport org.springframework.ws.soap.addressing.server.annotation.Action\n\n@Endpoint\npublic class AnnotationOrderEndpoint {\n private final OrderService orderService;\n\n public AnnotationOrderEndpoint(OrderService orderService) {\n this.orderService = orderService;\n }\n\n @Action("http://samples/RequestOrder")\n public Order getOrder(OrderRequest orderRequest) {\n return orderService.getOrder(orderRequest.getId());\n }\n\n @Action("http://samples/CreateOrder")\n public void order(Order order) {\n orderService.createOrder(order);\n }\n\n}\n')])])]),t("p",[e._v("前面的映射路由请求具有"),t("code",[e._v("Action")]),e._v("的"),t("code",[e._v("[http://samples/RequestOrder](http://samples/RequestOrder)")]),e._v("到"),t("code",[e._v("getOrder")]),e._v("方法的 WS-Addressing。带有"),t("code",[e._v("[http://samples/CreateOrder](http://samples/CreateOrder)")]),e._v("的请求被路由到"),t("code",[e._v("order")]),e._v("方法。")]),e._v(" "),t("p",[e._v("默认情况下,"),t("code",[e._v("AnnotationActionEndpointMapping")]),e._v("既支持 1.0(2006 年 5 月),也支持 2004 年 8 月版本的 WS-Addressing。这两个版本最受欢迎,可以与 Axis1 和 2、JAX-WS、Xfire、Windows 通信基础和 Windows 服务增强 3.0 互操作。如果有必要,可以将规范的特定版本注入"),t("code",[e._v("versions")]),e._v("属性。")]),e._v(" "),t("p",[e._v("除了"),t("code",[e._v("@Action")]),e._v("注释之外,还可以使用"),t("code",[e._v("@Address")]),e._v("注释对类进行注释。如果设置了值,则将该值与传入消息的"),t("code",[e._v("To")]),e._v("头属性进行比较。")]),e._v(" "),t("p",[e._v("最后,还有"),t("code",[e._v("messageSenders")]),e._v("属性,这是将响应消息发送到非匿名的、未绑定的地址所必需的。你可以在此属性中设置"),t("code",[e._v("MessageSender")]),e._v("实现,就像在"),t("code",[e._v("WebServiceTemplate")]),e._v("上一样。见"),t("a",{attrs:{href:"#client-transports"}},[e._v("URI 和传输")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_5-4-2-拦截请求-endpointinterceptor接口"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-4-2-拦截请求-endpointinterceptor接口"}},[e._v("#")]),e._v(" 5.4.2.拦截请求—"),t("code",[e._v("EndpointInterceptor")]),e._v("接口")]),e._v(" "),t("p",[e._v("端点映射机制具有端点拦截器的概念。当你希望将特定功能应用于某些请求时,这些功能可能非常有用——例如,处理与安全相关的 SOAP 头或记录请求和响应消息。")]),e._v(" "),t("p",[e._v("端点拦截器通常是通过在应用程序上下文中使用"),t("code",[e._v("")]),e._v("元素来定义的。在这个元素中,你可以定义应用于该应用程序上下文中定义的所有端点的端点拦截器 bean。或者,你可以使用"),t("code",[e._v("")]),e._v("或"),t("code",[e._v("")]),e._v("元素来指定拦截器应该为哪个有效负载根名称或 SOAP 操作应用。下面的示例展示了如何做到这一点:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n\n\n\n')])])]),t("p",[e._v("在前面的示例中,我们定义了一个拦截所有请求和响应的“全局”拦截器("),t("code",[e._v("MyGlobalInterceptor")]),e._v(")。我们还定义了一个拦截器,该拦截器仅应用于将"),t("code",[e._v("[http://www.example.com](http://www.example.com)")]),e._v("作为有效负载根名称空间的 XML 消息。除了"),t("code",[e._v("namespaceUri")]),e._v("之外,我们还可以定义一个"),t("code",[e._v("localPart")]),e._v("属性,以进一步限制拦截器所应用的消息。最后,我们定义了两个拦截器,当消息具有"),t("code",[e._v("[http://www.example.com/SoapAction](http://www.example.com/SoapAction)")]),e._v("SOAP 操作时,这些拦截器将应用于该消息。请注意,第二个拦截器实际上是对"),t("code",[e._v("")]),e._v("元素之外的 Bean 定义的引用。你可以在"),t("code",[e._v("")]),e._v("元素内部的任何地方使用 Bean 引用。")]),e._v(" "),t("p",[e._v("当使用"),t("code",[e._v("@Configuration")]),e._v("类时,可以从"),t("code",[e._v("WsConfigurerAdapter")]),e._v("扩展到添加拦截器:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("@Configuration\n@EnableWs\npublic class MyWsConfiguration extends WsConfigurerAdapter {\n\n @Override\n public void addInterceptors(List interceptors) {\n interceptors.add(new MyPayloadRootInterceptor());\n }\n\n}\n")])])]),t("p",[e._v("拦截器必须从"),t("code",[e._v("org.springframework.ws.server")]),e._v("包实现"),t("code",[e._v("EndpointInterceptor")]),e._v("接口。该接口定义了三种方法,一种可用于处理请求消息"),t("strong",[e._v("在此之前")]),e._v("处理实际端点,一种可用于处理正常响应消息,另一种可用于处理故障消息。第二个被称为"),t("strong",[e._v("之后")]),e._v("的端点被处理。这三种方法应该提供足够的灵活性,以进行各种前置和后置处理。")]),e._v(" "),t("p",[e._v("拦截器上的"),t("code",[e._v("handleRequest(..)")]),e._v("方法返回一个布尔值。你可以使用此方法中断或继续处理调用链。当此方法返回"),t("code",[e._v("true")]),e._v("时,端点处理链将继续进行。当返回"),t("code",[e._v("false")]),e._v("时,"),t("code",[e._v("MessageDispatcher")]),e._v("将其解释为拦截器本身已经处理了事情,并且不继续处理其他拦截器和调用链中的实际端点。"),t("code",[e._v("handleResponse(..)")]),e._v("和"),t("code",[e._v("handleFault(..)")]),e._v("方法也有一个布尔返回值。当这些方法返回"),t("code",[e._v("false")]),e._v("时,响应将不会被发送回客户端。")]),e._v(" "),t("p",[e._v("你可以在 Web 服务中使用许多标准"),t("code",[e._v("EndpointInterceptor")]),e._v("实现。此外,还有"),t("code",[e._v("XwsSecurityInterceptor")]),e._v(",它在["),t("code",[e._v("XwsSecurityInterceptor")]),e._v("](#security-xws-security-interceptor)中进行了描述。")]),e._v(" "),t("h5",{attrs:{id:"payloadlogginginterceptor和soapenvelopelogginginterceptor"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#payloadlogginginterceptor和soapenvelopelogginginterceptor"}},[e._v("#")]),e._v(" "),t("code",[e._v("PayloadLoggingInterceptor")]),e._v("和"),t("code",[e._v("SoapEnvelopeLoggingInterceptor")])]),e._v(" "),t("p",[e._v("在开发 Web 服务时,记录传入和传出的 XML 消息可能很有用。 Spring WS 通过"),t("code",[e._v("PayloadLoggingInterceptor")]),e._v("和"),t("code",[e._v("SoapEnvelopeLoggingInterceptor")]),e._v("类来促进这一点。前者只将消息的有效负载记录到 Commons 日志中。后者记录整个 SOAP 信封,包括 SOAP 头。下面的示例展示了如何在端点映射中定义"),t("code",[e._v("PayloadLoggingInterceptor")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v(' \n \n \n')])])]),t("p",[e._v("这两个拦截器都有两个属性,"),t("code",[e._v("logRequest")]),e._v("和"),t("code",[e._v("logResponse")]),e._v(",可以将其设置为"),t("code",[e._v("false")]),e._v(",以禁用请求或响应消息的日志记录。")]),e._v(" "),t("p",[e._v("对于"),t("code",[e._v("PayloadLoggingInterceptor")]),e._v(",你也可以使用"),t("code",[e._v("WsConfigurerAdapter")]),e._v("方法,如前面所述。")]),e._v(" "),t("h5",{attrs:{id:"payloadvalidatinginterceptor"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#payloadvalidatinginterceptor"}},[e._v("#")]),e._v(" "),t("code",[e._v("PayloadValidatingInterceptor")])]),e._v(" "),t("p",[e._v("使用契约优先的开发风格的好处之一是,我们可以使用模式来验证传入和传出的 XML 消息。 Spring-WS 通过"),t("code",[e._v("PayloadValidatingInterceptor")]),e._v("促进了这一点。此拦截器需要对一个或多个 W3CXML 或 RELAXNG 模式的引用,并且可以设置为验证请求、响应或两者。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("请注意,请求验证听起来可能是个好主意,但它会使生成的 Web 服务变得非常严格。通常,只有在端点能够获得足够的信息来满足请求的情况下,请求是否有效才真正重要。验证响应是一个好主意,因为端点应该遵循其模式。记住波斯特定律:"),t("br"),e._v("“在你做的事情上要保守,在你接受别人的东西上要自由。”")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("下面的示例使用"),t("code",[e._v("PayloadValidatingInterceptor")]),e._v("。在这个示例中,我们使用"),t("code",[e._v("/WEB-INF/orders.xsd")]),e._v("中的模式来验证响应,而不是验证请求。请注意,"),t("code",[e._v("PayloadValidatingInterceptor")]),e._v("还可以通过设置"),t("code",[e._v("schemas")]),e._v("属性来接受多个模式。")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n')])])]),t("p",[e._v("当然,你也可以使用"),t("code",[e._v("WsConfigurerAdapter")]),e._v("方法,如前面所述,用于"),t("code",[e._v("PayloadValidatingInterceptor")]),e._v("。")]),e._v(" "),t("h5",{attrs:{id:"使用payloadtransforminginterceptor"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用payloadtransforminginterceptor"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("PayloadTransformingInterceptor")])]),e._v(" "),t("p",[e._v("Spring 为了将有效负载转换为另一种 XML 格式,Web 服务提供了"),t("code",[e._v("PayloadTransformingInterceptor")]),e._v("。这个端点拦截器基于 XSLT 样式表,在支持 Web 服务的多个版本时特别有用,因为你可以将较旧的消息格式转换为较新的格式。下面的示例使用"),t("code",[e._v("PayloadTransformingInterceptor")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n\n')])])]),t("p",[e._v("在前面的示例中,我们通过使用"),t("code",[e._v("/WEB-INF/oldRequests.xslt")]),e._v("转换请求,并通过使用"),t("code",[e._v("/WEB-INF/oldResponses.xslt")]),e._v("转换响应消息。请注意,由于端点拦截器是在端点映射级别注册的,因此你可以创建一个应用于“旧样式”消息的端点映射,并将拦截器添加到该映射中。因此,转换仅适用于这些“旧式”消息。")]),e._v(" "),t("p",[e._v("对于"),t("code",[e._v("PayloadTransformingInterceptor")]),e._v(",你也可以使用"),t("code",[e._v("WsConfigurerAdapter")]),e._v("方法,如前面所述。")]),e._v(" "),t("h3",{attrs:{id:"_5-5-处理异常"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-5-处理异常"}},[e._v("#")]),e._v(" 5.5.处理异常")]),e._v(" "),t("p",[e._v("Spring-WS 提供了"),t("code",[e._v("EndpointExceptionResolvers")]),e._v(",以减轻在你的消息被匹配该请求的端点处理时发生意外异常的痛苦。端点异常解析器有点类似于可以在 Web 应用程序描述符"),t("code",[e._v("web.xml")]),e._v("中定义的异常映射。然而,它们提供了一种更灵活的处理异常的方法。它们提供了有关抛出异常时调用的端点的信息。此外,处理异常的编程方式为你提供了更多关于如何适当响应的选项。你不需要通过提供异常和堆栈跟踪来暴露应用程序的内部,而是可以以任何你想要的方式处理异常——例如,通过返回带有特定错误代码和字符串的 SOAP 错误。")]),e._v(" "),t("p",[e._v("端点异常解析器由"),t("code",[e._v("MessageDispatcher")]),e._v("自动拾取,因此不需要显式配置。")]),e._v(" "),t("p",[e._v("除了实现"),t("code",[e._v("EndpointExceptionResolver")]),e._v("接口(这只是实现"),t("code",[e._v("resolveException(MessageContext, endpoint, Exception)")]),e._v("方法的问题)外,还可以使用所提供的实现之一。最简单的实现是"),t("code",[e._v("SimpleSoapExceptionResolver")]),e._v(",它创建一个 SOAP1.1 服务器或 SOAP1.2 接收器故障,并使用异常消息作为故障字符串。"),t("code",[e._v("SimpleSoapExceptionResolver")]),e._v("是默认值,但是可以通过显式地添加另一个解析器来重写它。")]),e._v(" "),t("h4",{attrs:{id:"_5-5-1-soapfaultmappingexceptionresolver"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-5-1-soapfaultmappingexceptionresolver"}},[e._v("#")]),e._v(" 5.5.1."),t("code",[e._v("SoapFaultMappingExceptionResolver")])]),e._v(" "),t("p",[t("code",[e._v("SoapFaultMappingExceptionResolver")]),e._v("是一种更复杂的实现。这个解析器允许你获取任何可能被抛出的异常的类名,并将其映射到一个 SOAP 错误:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n org.springframework.oxm.ValidationFailureException=CLIENT,Invalid request\n \n \n \n\n')])])]),t("p",[e._v("键值和默认端点使用"),t("code",[e._v("faultCode,faultString,locale")]),e._v("格式,其中只需要错误代码。如果未设置错误字符串,它将默认为异常消息。如果语言未设置,则默认为英语。前面的配置将类型"),t("code",[e._v("ValidationFailureException")]),e._v("的异常映射到具有"),t("code",[e._v("Invalid request")]),e._v("的故障字符串的客户端 SOAP 故障,如下所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n SOAP-ENV:Client\n Invalid request\n \n \n\n')])])]),t("p",[e._v("如果发生任何其他异常,它将返回默认的故障:服务器端故障,异常消息作为故障字符串。")]),e._v(" "),t("h4",{attrs:{id:"_5-5-2-使用soapfaultannotationexceptionresolver"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-5-2-使用soapfaultannotationexceptionresolver"}},[e._v("#")]),e._v(" 5.5.2.使用"),t("code",[e._v("SoapFaultAnnotationExceptionResolver")])]),e._v(" "),t("p",[e._v("你还可以使用"),t("code",[e._v("@SoapFault")]),e._v("注释来注释异常类,以指示每当引发异常时应返回的 SOAP 错误。要获取这些注释,你需要将"),t("code",[e._v("SoapFaultAnnotationExceptionResolver")]),e._v("添加到应用程序上下文中。注释的元素包括错误代码枚举、错误字符串或原因以及语言。下面的示例显示了这样的例外情况:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("package samples;\n\nimport org.springframework.ws.soap.server.endpoint.annotation.FaultCode;\nimport org.springframework.ws.soap.server.endpoint.annotation.SoapFault;\n\n@SoapFault(faultCode = FaultCode.SERVER)\npublic class MyBusinessException extends Exception {\n\n public MyClientException(String message) {\n super(message);\n }\n}\n")])])]),t("p",[e._v("每当在端点调用过程中使用构造函数字符串"),t("code",[e._v("MyBusinessException")]),e._v("抛出"),t("code",[e._v('"Oops!"')]),e._v("时,都会产生以下响应:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n SOAP-ENV:Server\n Oops!\n \n \n\n')])])]),t("h3",{attrs:{id:"_5-6-服务器端测试"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-6-服务器端测试"}},[e._v("#")]),e._v(" 5.6.服务器端测试")]),e._v(" "),t("p",[e._v("当涉及到测试你的 Web 服务端点时,你有两种可能的方法:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("编写单元测试,其中提供(模拟)参数供端点使用。")]),e._v(" "),t("p",[e._v("这种方法的优点是非常容易实现(特别是对于用"),t("code",[e._v("@Endpoint")]),e._v("注释的类)。缺点是,你实际上并不是在测试通过网络发送的 XML 消息的确切内容。")])]),e._v(" "),t("li",[t("p",[e._v("编写集成测试,测试消息的内容。")])])]),e._v(" "),t("p",[e._v("第一种方法可以通过模拟框架(如 EasyMock、JMock 等)轻松实现。下一节将重点讨论如何使用 Spring Web 服务 2.0 中介绍的测试特性编写集成测试。")]),e._v(" "),t("h4",{attrs:{id:"_5-6-1-编写服务器端集成测试"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-6-1-编写服务器端集成测试"}},[e._v("#")]),e._v(" 5.6.1.编写服务器端集成测试")]),e._v(" "),t("p",[e._v("Spring Web 服务 2.0 引入了对创建端点集成测试的支持。在这种上下文中,端点是处理消息的类(参见"),t("a",{attrs:{href:"#server-endpoints"}},[e._v("Endpoints")]),e._v(")。")]),e._v(" "),t("p",[e._v("集成测试支持位于"),t("code",[e._v("org.springframework.ws.test.server")]),e._v("包中。包中的核心类是"),t("code",[e._v("MockWebServiceClient")]),e._v("。其基本思想是,该客户机创建一个请求消息,然后将其发送到在标准"),t("code",[e._v("MessageDispatcherServlet")]),e._v("应用程序上下文中配置的端点(参见["),t("code",[e._v("MessageDispatcherServlet")]),e._v("](#message-dispatcher- Servlet))。这些端点处理消息并创建响应。然后,客户机接收到此响应,并根据已注册的期望对其进行验证。")]),e._v(" "),t("p",[t("code",[e._v("MockWebServiceClient")]),e._v("的典型用法是:。")]),e._v(" "),t("ol",[t("li",[t("p",[e._v("通过调用"),t("code",[e._v("MockWebServiceClient.createClient(ApplicationContext)")]),e._v("或"),t("code",[e._v("MockWebServiceClient.createClient(WebServiceMessageReceiver, WebServiceMessageFactory)")]),e._v("来创建"),t("code",[e._v("MockWebServiceClient")]),e._v("实例。")])]),e._v(" "),t("li",[t("p",[e._v("通过调用"),t("code",[e._v("sendRequest(RequestCreator)")]),e._v("发送请求消息,可能是通过使用"),t("code",[e._v("RequestCreator")]),e._v("中提供的默认"),t("code",[e._v("RequestCreator")]),e._v("实现(可以静态导入)。")])]),e._v(" "),t("li",[t("p",[e._v("通过调用"),t("code",[e._v("andExpect(ResponseMatcher)")]),e._v("来设置响应期望,可能使用"),t("code",[e._v("ResponseMatcher")]),e._v("中提供的默认"),t("code",[e._v("ResponseMatcher")]),e._v("实现(可以静态导入)。可以通过链接"),t("code",[e._v("andExpect(ResponseMatcher)")]),e._v("调用来设置多个期望。")])])]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("请注意,"),t("code",[e._v("MockWebServiceClient")]),e._v("(以及相关的类)提供了一个“fluent”API,因此你通常可以使用 IDE 中的代码完成功能来指导你完成设置模拟服务器的过程。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("还请注意,你可以在单元测试中依赖于 Spring Web 服务中可用的标准日志记录功能。有时,检查请求或响应消息以找出特定测试失败的原因可能是有用的。有关更多信息,请参见"),t("a",{attrs:{href:"#logging"}},[e._v("消息日志记录和跟踪")]),e._v("。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("例如,考虑以下 Web 服务端点类:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import org.springframework.ws.server.endpoint.annotation.Endpoint;\nimport org.springframework.ws.server.endpoint.annotation.RequestPayload;\nimport org.springframework.ws.server.endpoint.annotation.ResponsePayload;\n\n@Endpoint (1)\npublic class CustomerEndpoint {\n\n @ResponsePayload (2)\n public CustomerCountResponse getCustomerCount( (2)\n @RequestPayload CustomerCountRequest request) { (2)\n CustomerCountResponse response = new CustomerCountResponse();\n response.setCustomerCount(10);\n return response;\n }\n\n}\n")])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[e._v("用"),t("code",[e._v("@Endpoint")]),e._v("注释的"),t("code",[e._v("CustomerEndpoint")]),e._v("。见"),t("a",{attrs:{href:"#server-endpoints"}},[e._v("Endpoints")]),e._v("。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[t("code",[e._v("getCustomerCount()")]),e._v("方法以"),t("code",[e._v("CustomerCountRequest")]),e._v("为参数,并返回"),t("code",[e._v("CustomerCountResponse")]),e._v("。这两个类都是 Marshaller 支持的对象。例如,JAXB2 可以支持"),t("code",[e._v("@XmlRootElement")]),e._v("注释。")])])])]),e._v(" "),t("p",[e._v("下面的示例显示了"),t("code",[e._v("CustomerEndpoint")]),e._v("的典型测试:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import javax.xml.transform.Source;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.test.context.ContextConfiguration;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.xml.transform.StringSource;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport org.springframework.ws.test.server.MockWebServiceClient; (1)\nimport static org.springframework.ws.test.server.RequestCreators.*; (1)\nimport static org.springframework.ws.test.server.ResponseMatchers.*; (1)\n\n@RunWith(SpringJUnit4ClassRunner.class) (2)\n@ContextConfiguration("spring-ws-servlet.xml") (2)\npublic class CustomerEndpointIntegrationTest {\n\n @Autowired\n private ApplicationContext applicationContext; (3)\n\n private MockWebServiceClient mockClient;\n\n @Before\n public void createClient() {\n mockClient = MockWebServiceClient.createClient(applicationContext); (4)\n }\n\n @Test\n public void customerEndpoint() throws Exception {\n Source requestPayload = new StringSource(\n "" +\n "John Doe" +\n "");\n Source responsePayload = new StringSource(\n "" +\n "10" +\n "");\n\n mockClient.sendRequest(withPayload(requestPayload)). (5)\n andExpect(payload(responsePayload)); (5)\n }\n}\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[t("code",[e._v("CustomerEndpointIntegrationTest")]),e._v("导入"),t("code",[e._v("MockWebServiceClient")]),e._v(",静态导入"),t("code",[e._v("RequestCreators")]),e._v("和"),t("code",[e._v("ResponseMatchers")]),e._v("。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[e._v("该测试使用在 Spring 框架中提供的标准测试设施。这不是必需的,但通常是设置测试的最简单方法。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("3")])]),e._v(" "),t("td",[e._v("应用程序上下文是标准的 Spring-WS 应用程序上下文(参见["),t("code",[e._v("MessageDispatcherServlet")]),e._v("](#message-dispatcher- Servlet)),从"),t("code",[e._v("spring-ws-servlet.xml")]),e._v("读取。在这种情况下,应用程序上下文包含对"),t("code",[e._v("CustomerEndpoint")]),e._v("的 Bean 定义(或者可能使用"),t("code",[e._v("")]),e._v(")。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("4")])]),e._v(" "),t("td",[e._v("在"),t("code",[e._v("@Before")]),e._v("方法中,我们使用"),t("code",[e._v("createClient")]),e._v("工厂方法创建"),t("code",[e._v("MockWebServiceClient")]),e._v("。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("5")])]),e._v(" "),t("td",[e._v("我们通过调用"),t("code",[e._v("sendRequest()")]),e._v("并使用静态导入的"),t("code",[e._v("RequestCreators")]),e._v("提供的"),t("code",[e._v("RequestCreator")]),e._v("来发送请求(请参见[using"),t("code",[e._v("RequestCreator")]),e._v(")和"),t("code",[e._v("RequestCreators")]),e._v("(#server-test-request-creator))。"),t("br"),t("br"),e._v("我们还通过调用静态导入的"),t("code",[e._v("payload()")]),e._v("提供的"),t("code",[e._v("ResponseMatcher")]),e._v("(参见[using"),t("code",[e._v("ResponseMatcher")]),e._v("和"),t("code",[e._v("ResponseMatchers")]),e._v("(#server-test-response-response-matcher-matcher=“967”))来设置响应期望。但是 IDE 的代码完成功能非常有帮助。在输入"),t("code",[e._v("sendRequest(")]),e._v("之后,你的 IDE 可以为你提供一个可能的请求创建策略的列表,前提是静态导入"),t("code",[e._v("RequestCreators")]),e._v("。这同样适用于"),t("code",[e._v("andExpect()")]),e._v(",前提是静态导入"),t("code",[e._v("ResponseMatchers")]),e._v("。")])])])]),e._v(" "),t("h4",{attrs:{id:"_5-6-2-使用requestcreator和requestcreators"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-6-2-使用requestcreator和requestcreators"}},[e._v("#")]),e._v(" 5.6.2.使用"),t("code",[e._v("RequestCreator")]),e._v("和"),t("code",[e._v("RequestCreators")])]),e._v(" "),t("p",[e._v("最初,"),t("code",[e._v("MockWebServiceClient")]),e._v("需要为要使用的端点创建一个请求消息。客户机为此目的使用"),t("code",[e._v("RequestCreator")]),e._v("策略接口:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public interface RequestCreator {\n\n WebServiceMessage createRequest(WebServiceMessageFactory messageFactory)\n throws IOException;\n\n}\n")])])]),t("p",[e._v("你可以编写你自己的这个接口的实现,通过使用消息工厂来创建请求消息,但是你当然不必这样做。"),t("code",[e._v("RequestCreators")]),e._v("类提供了一种基于"),t("code",[e._v("withPayload()")]),e._v("方法中的给定有效负载创建"),t("code",[e._v("RequestCreator")]),e._v("的方法。通常静态导入"),t("code",[e._v("RequestCreators")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_5-6-3-使用responsematcher和responsematchers"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-6-3-使用responsematcher和responsematchers"}},[e._v("#")]),e._v(" 5.6.3.使用"),t("code",[e._v("ResponseMatcher")]),e._v("和"),t("code",[e._v("ResponseMatchers")])]),e._v(" "),t("p",[e._v("当端点处理了请求消息并且接收到了响应时,"),t("code",[e._v("MockWebServiceClient")]),e._v("可以验证此响应消息是否满足某些期望。客户机为此目的使用"),t("code",[e._v("ResponseMatcher")]),e._v("策略接口:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public interface ResponseMatcher {\n\n void match(WebServiceMessage request,\n WebServiceMessage response)\n throws IOException, AssertionError;\n\n}\n")])])]),t("p",[e._v("再次,你可以编写你自己的这个接口的实现,在消息不满足你的期望时抛出"),t("code",[e._v("AssertionError")]),e._v("实例,但是你当然不必这样做,因为"),t("code",[e._v("ResponseMatchers")]),e._v("类提供了标准的"),t("code",[e._v("ResponseMatcher")]),e._v("实现,供你在测试中使用。你通常静态地导入这个类。")]),e._v(" "),t("p",[t("code",[e._v("ResponseMatchers")]),e._v("类提供以下响应匹配器:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[t("code",[e._v("ResponseMatchers")]),e._v("方法")]),e._v(" "),t("th",[e._v("Description")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("code",[e._v("payload()")])]),e._v(" "),t("td",[e._v("Expects a given response payload.")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("validPayload()")])]),e._v(" "),t("td",[e._v("Expects the response payload to validate against given XSD schemas.")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("xpath()")])]),e._v(" "),t("td",[e._v("Expects a given XPath expression to exist, not exist, or evaluate to a given value.")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("soapHeader()")])]),e._v(" "),t("td",[e._v("Expects a given SOAP header to exist in the response message.")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("noFault()")])]),e._v(" "),t("td",[e._v("Expects that the response message does not contain a SOAP Fault.")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("mustUnderstandFault()")]),e._v(","),t("code",[e._v("clientOrSenderFault()")]),e._v(","),t("code",[e._v("serverOrReceiverFault()")]),e._v(",和"),t("code",[e._v("versionMismatchFault()")])]),e._v(" "),t("td",[e._v("Expects the response message to contain a specific SOAP Fault.")])])])]),e._v(" "),t("p",[e._v("你可以通过链接"),t("code",[e._v("andExpect()")]),e._v("调用来设置多个响应期望:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("mockClient.sendRequest(...).\n andExpect(payload(expectedResponsePayload)).\n andExpect(validPayload(schemaResource));\n")])])]),t("p",[e._v("有关"),t("code",[e._v("ResponseMatchers")]),e._v("提供的响应匹配器的更多信息,请参见"),t("a",{attrs:{href:"https://docs.spring.io/spring-ws/docs/current/api/org/springframework/ws/test/server/ResponseMatchers.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("h2",{attrs:{id:"_6-在客户端上使用-spring-web-服务"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-在客户端上使用-spring-web-服务"}},[e._v("#")]),e._v(" 6.在客户端上使用 Spring Web 服务")]),e._v(" "),t("p",[e._v("Spring-WS 提供了一个客户端 Web 服务 API,该 API 允许对 Web 服务进行一致的、XML 驱动的访问。它还迎合了 Marshaller 和 Unmarshaller 的使用,这样你的服务层代码就可以专门处理 Java 对象了。")]),e._v(" "),t("p",[t("code",[e._v("org.springframework.ws.client.core")]),e._v("包提供了使用客户端访问 API 的核心功能。它包含的模板类简化了 Web 服务的使用,很像 JDBC 的核心 Spring "),t("code",[e._v("JdbcTemplate")]),e._v("所做的那样。 Spring 模板类的共同设计原则是提供辅助方法来执行公共操作,并且为了更复杂的使用,将回调接口委托给用户实现。Web 服务模板遵循相同的设计。这些类为用户提供了各种方便的方法。")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("XML 消息的发送和接收")])]),e._v(" "),t("li",[t("p",[e._v("在发送对象之前将对象编组为 XML")])]),e._v(" "),t("li",[t("p",[e._v("允许多种运输方式")])])]),e._v(" "),t("h3",{attrs:{id:"_6-1-使用客户端-api"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-1-使用客户端-api"}},[e._v("#")]),e._v(" 6.1.使用客户端 API")]),e._v(" "),t("p",[e._v("本节描述如何使用客户端 API。有关如何使用服务器端 API,请参见"),t("a",{attrs:{href:"#server"}},[e._v("Creating a Web service with Spring-WS")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"服务器端-ws-addressing6-1-1-webservicetemplate"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#服务器端-ws-addressing6-1-1-webservicetemplate"}},[e._v("#")]),e._v(" "),t("a",{attrs:{href:"#server-ws-addressing"}},[e._v("服务器端 WS-Addressing")]),e._v("6.1.1."),t("code",[e._v("WebServiceTemplate")])]),e._v(" "),t("p",[t("code",[e._v("WebServiceTemplate")]),e._v("是 Spring-WS 中用于客户端 Web 服务访问的核心类。它包含用于发送"),t("code",[e._v("Source")]),e._v("对象和以"),t("code",[e._v("Source")]),e._v("或"),t("code",[e._v("Result")]),e._v("接收响应消息的方法。此外,它还可以在跨传输发送对象之前将对象封送到 XML,并将任何响应 XML 重新封送到对象中。")]),e._v(" "),t("h5",{attrs:{id:"uri-和-transports"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#uri-和-transports"}},[e._v("#")]),e._v(" URI 和 Transports")]),e._v(" "),t("p",[t("code",[e._v("WebServiceTemplate")]),e._v("类使用 URI 作为消息目的地。你可以在模板本身上设置"),t("code",[e._v("defaultUri")]),e._v("属性,也可以在调用模板上的方法时显式地提供 URI。URI 解析为"),t("code",[e._v("WebServiceMessageSender")]),e._v(",它负责跨传输层发送 XML 消息。你可以使用"),t("code",[e._v("messageSender")]),e._v("或"),t("code",[e._v("messageSenders")]),e._v("类的属性来设置一个或多个消息发送者。")]),e._v(" "),t("h6",{attrs:{id:"http-传输"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#http-传输"}},[e._v("#")]),e._v(" HTTP 传输")]),e._v(" "),t("p",[e._v("用于通过 HTTP 发送消息的"),t("code",[e._v("WebServiceMessageSender")]),e._v("接口有两种实现方式。默认的实现是"),t("code",[e._v("HttpUrlConnectionMessageSender")]),e._v(",它使用 Java 本身提供的功能。另一种选择是"),t("code",[e._v("HttpComponentsMessageSender")]),e._v(",它使用"),t("a",{attrs:{href:"https://hc.apache.org/httpcomponents-client-ga",target:"_blank",rel:"noopener noreferrer"}},[e._v("Apache HttpComponents HttpClient"),t("OutboundLink")],1),e._v("。如果你需要更高级、更易用的功能(如身份验证、HTTP 连接池等),请使用后者。")]),e._v(" "),t("p",[e._v("要使用 HTTP 传输,可以将"),t("code",[e._v("defaultUri")]),e._v("设置为"),t("code",[e._v("[http://example.com/services](http://example.com/services)")]),e._v("之类的值,或者为其中一个方法提供"),t("code",[e._v("uri")]),e._v("参数。")]),e._v(" "),t("p",[e._v("下面的示例展示了如何为 HTTP 传输使用默认配置:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n \n \n \n\n\n')])])]),t("p",[e._v("下面的示例展示了如何覆盖默认配置,以及如何使用 Apache HttpClient 使用 HTTP 身份验证进行身份验证:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n\n')])])]),t("h6",{attrs:{id:"jms-传输"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#jms-传输"}},[e._v("#")]),e._v(" JMS 传输")]),e._v(" "),t("p",[e._v("对于通过 JMS 发送消息, Spring Web 服务提供"),t("code",[e._v("JmsMessageSender")]),e._v("。这个类使用 Spring 框架的功能将"),t("code",[e._v("WebServiceMessage")]),e._v("转换为 JMS"),t("code",[e._v("Message")]),e._v(",在"),t("code",[e._v("Queue")]),e._v("或"),t("code",[e._v("Topic")]),e._v("上发送它,并接收响应(如果有的话)。")]),e._v(" "),t("p",[e._v("要使用"),t("code",[e._v("JmsMessageSender")]),e._v(",你需要将"),t("code",[e._v("defaultUri")]),e._v("或"),t("code",[e._v("SecurityContextHolder")]),e._v("参数设置为一个 JMS URI,该 URI 至少由"),t("code",[e._v("jms:")]),e._v("前缀和一个目标名称组成。JMS URI 的一些示例是:"),t("code",[e._v("jms:SomeQueue")]),e._v("、"),t("code",[e._v("jms:SomeTopic?priority=3&deliveryMode=NON_PERSISTENT")]),e._v("和"),t("code",[e._v("jms:RequestQueue?replyToName=ResponseName")]),e._v("。有关此 URI 语法的更多信息,请参见[Javadoc for"),t("code",[e._v("JmsMessageSender")]),e._v("](https://DOCS. Spring.io/ Spring-ws/DOCS/current/api/org/springframework/ws/transport/jms/jmssagesender.html)。")]),e._v(" "),t("p",[e._v("默认情况下,"),t("code",[e._v("JmsMessageSender")]),e._v("发送 JMS"),t("code",[e._v("WebServiceMessageSender")]),e._v(",但是你可以通过在 JMS URI 上使用"),t("code",[e._v("messageType")]),e._v("参数(例如,"),t("code",[e._v("jms:Queue?messageType=TEXT_MESSAGE")]),e._v(")来覆盖此内容以使用"),t("code",[e._v("TextMessages")]),e._v("。请注意,"),t("code",[e._v("BytesMessages")]),e._v("是首选类型,因为"),t("code",[e._v("TextMessages")]),e._v("不可靠地支持附件和字符编码。")]),e._v(" "),t("p",[e._v("下面的示例展示了如何结合 ActiveMQ 连接工厂使用 JMS 传输:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n \n \n\n \n \n \n \n \n \n \n \n \n\n\n')])])]),t("h6",{attrs:{id:"电子邮件传输"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#电子邮件传输"}},[e._v("#")]),e._v(" 电子邮件传输")]),e._v(" "),t("p",[e._v("Spring Web 服务还提供了一种电子邮件传输,你可以使用它通过 SMTP 发送 Web 服务消息,并通过 POP3 或 IMAP 检索它们。客户端电子邮件功能包含在"),t("code",[e._v("MailMessageSender")]),e._v("类中。这个类从请求"),t("code",[e._v("WebServiceMessage")]),e._v("创建一个电子邮件消息,并通过 SMTP 发送它。然后,它等待响应消息到达传入的 POP3 或 IMAP 服务器。")]),e._v(" "),t("p",[e._v("要使用"),t("code",[e._v("MailMessageSender")]),e._v(",将"),t("code",[e._v("defaultUri")]),e._v("或"),t("code",[e._v("uri")]),e._v("参数设置为"),t("code",[e._v("mailto")]),e._v("URI——例如,"),t("code",[e._v("mailto:[[email protected]](/cdn-cgi/l/email-protection)")]),e._v("或"),t("code",[e._v("mailto:[[email protected]](/cdn-cgi/l/email-protection)?subject=SOAP%20Test")]),e._v("。确保消息发送方正确配置了"),t("code",[e._v("transportUri")]),e._v(",这表明服务器用于发送请求(通常是 SMTP 服务器),以及"),t("code",[e._v("storeUri")]),e._v(",这表明服务器要轮询响应(通常是 POP3 或 IMAP 服务器)。")]),e._v(" "),t("p",[e._v("下面的示例展示了如何使用电子邮件传输:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n \n \n \n \n \n \n \n \n \n \n\n\n')])])]),t("h6",{attrs:{id:"xmpp-传输"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#xmpp-传输"}},[e._v("#")]),e._v(" XMPP 传输")]),e._v(" "),t("p",[e._v("Spring Web 服务 2.0 引入了一种 XMPP 传输,你可以使用它通过 XMPP 发送和接收 Web 服务消息。客户端 XMPP 功能包含在"),t("code",[e._v("XmppMessageSender")]),e._v("类中。该类从请求"),t("code",[e._v("WebServiceMessage")]),e._v("创建一个 XMPP 消息,并通过 XMPP 发送它。然后,它会监听收到的响应消息。")]),e._v(" "),t("p",[e._v("要使用"),t("code",[e._v("XmppMessageSender")]),e._v(",请将"),t("code",[e._v("defaultUri")]),e._v("或"),t("code",[e._v("uri")]),e._v("参数设置为"),t("code",[e._v("XmppMessageSender")]),e._v("URI——例如,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("。发送方还需要"),t("code",[e._v("XMPPConnection")]),e._v("才能工作,这可以通过使用"),t("code",[e._v("org.springframework.ws.transport.xmpp.support.XmppConnectionFactoryBean")]),e._v("方便地创建。")]),e._v(" "),t("p",[e._v("下面的示例展示了如何使用 XMPP 传输:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n\n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n\n\n')])])]),t("h5",{attrs:{id:"messagefactory消息工厂"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#messagefactory消息工厂"}},[e._v("#")]),e._v(" "),t("code",[e._v("messageFactory")]),e._v("消息工厂")]),e._v(" "),t("p",[e._v("除了消息发送者之外,"),t("code",[e._v("WebServiceTemplate")]),e._v("还需要一个 Web 服务消息工厂。SOAP 有两个消息工厂:"),t("code",[e._v("SaajSoapMessageFactory")]),e._v("和"),t("code",[e._v("AxiomSoapMessageFactory")]),e._v("。如果没有指定消息工厂(通过设置"),t("code",[e._v("messageFactory")]),e._v("属性), Spring-WS 默认使用"),t("code",[e._v("SaajSoapMessageFactory")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_6-1-2-发送和接收webservicemessage"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-1-2-发送和接收webservicemessage"}},[e._v("#")]),e._v(" 6.1.2.发送和接收"),t("code",[e._v("WebServiceMessage")])]),e._v(" "),t("p",[t("code",[e._v("WebServiceTemplate")]),e._v("包含许多发送和接收 Web 服务消息的方便方法。有一些方法可以接受并返回"),t("code",[e._v("Source")]),e._v(",也有一些方法可以返回"),t("code",[e._v("Result")]),e._v("。此外,还有将对象封送和解封送到 XML 的方法。下面的示例向 Web 服务发送一个简单的 XML 消息:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import java.io.StringReader;\nimport javax.xml.transform.stream.StreamResult;\nimport javax.xml.transform.stream.StreamSource;\n\nimport org.springframework.ws.WebServiceMessageFactory;\nimport org.springframework.ws.client.core.WebServiceTemplate;\nimport org.springframework.ws.transport.WebServiceMessageSender;\n\npublic class WebServiceClient {\n\n private static final String MESSAGE =\n "Hello, Web Service World";\n\n private final WebServiceTemplate webServiceTemplate = new WebServiceTemplate();\n\n public void setDefaultUri(String defaultUri) {\n webServiceTemplate.setDefaultUri(defaultUri);\n }\n\n // send to the configured default URI\n public void simpleSendAndReceive() {\n StreamSource source = new StreamSource(new StringReader(MESSAGE));\n StreamResult result = new StreamResult(System.out);\n webServiceTemplate.sendSourceAndReceiveToResult(source, result);\n }\n\n // send to an explicit URI\n public void customSendAndReceive() {\n StreamSource source = new StreamSource(new StringReader(MESSAGE));\n StreamResult result = new StreamResult(System.out);\n webServiceTemplate.sendSourceAndReceiveToResult("http://localhost:8080/AnotherWebService",\n source, result);\n }\n\n}\n')])])]),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\n \n \n \n\n\n')])])]),t("p",[e._v("前面的示例使用"),t("code",[e._v("WebServiceTemplate")]),e._v("向位于"),t("code",[e._v("[http://localhost:8080/WebService](http://localhost:8080/WebService)")]),e._v("的 Web 服务发送“你好,世界”消息(在"),t("code",[e._v("simpleSendAndReceive()")]),e._v("方法的情况下),并将结果写到控制台。将"),t("code",[e._v("WebServiceTemplate")]),e._v("注入默认的 URI,因为 Java 代码中没有显式地提供 URI,所以使用该 URI。")]),e._v(" "),t("p",[e._v("注意,"),t("code",[e._v("WebServiceTemplate")]),e._v("类在配置后是线程安全的(假设它的所有依赖项也是线程安全的,这是 Spring-WS 附带的所有依赖项的情况),因此多个对象可以使用相同的共享"),t("code",[e._v("MailMessageSender")]),e._v("实例。"),t("code",[e._v("WebServiceTemplate")]),e._v("公开了一个零参数构造函数和"),t("code",[e._v("messageFactory")]),e._v("和"),t("code",[e._v("messageSender")]),e._v(" Bean 属性,你可以使用这些属性来构造实例(通过使用 Spring 容器或普通 Java 代码)。或者,可以考虑从 Spring-WS 的"),t("code",[e._v("messageSender")]),e._v("便利基类派生,它公开了方便的 Bean 属性以实现简单的配置。(你不必扩展这个基类。它仅作为一种便利类提供。")]),e._v(" "),t("h4",{attrs:{id:"_6-1-3-发送和接收-pojo-编组和解组"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-1-3-发送和接收-pojo-编组和解组"}},[e._v("#")]),e._v(" 6.1.3.发送和接收 POJO——编组和解组")]),e._v(" "),t("p",[e._v("为了方便普通 Java 对象的发送,"),t("code",[e._v("WebServiceTemplate")]),e._v("具有许多"),t("code",[e._v("send(..)")]),e._v("方法,这些方法将"),t("code",[e._v("Object")]),e._v("作为消息数据内容的参数。在"),t("code",[e._v("WebServiceTemplate")]),e._v("类中的方法"),t("code",[e._v("send(..)")]),e._v("将请求对象到 XML 的转换委托给"),t("code",[e._v("Marshaller")]),e._v(",并将响应 XML 到对象的转换委托给"),t("code",[e._v("Unmarshaller")]),e._v("。(有关编组和解组器的更多信息,请参见"),t("a",{attrs:{href:"https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#oxm-marshaller-unmarshaller",target:"_blank",rel:"noopener noreferrer"}},[e._v("the Spring Framework reference documentation"),t("OutboundLink")],1),e._v("。)通过使用编组器,你的应用程序代码可以专注于正在发送或接收的业务对象,而不必关注它如何表示为 XML 的详细信息。要使用编组功能,必须使用"),t("code",[e._v("WebServiceTemplate")]),e._v("类的"),t("code",[e._v("marshaller")]),e._v("和"),t("code",[e._v("unmarshaller")]),e._v("属性设置编组器和解组器。")]),e._v(" "),t("h4",{attrs:{id:"_6-1-4-使用webservicemessagecallback"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-1-4-使用webservicemessagecallback"}},[e._v("#")]),e._v(" 6.1.4.使用"),t("code",[e._v("WebServiceMessageCallback")])]),e._v(" "),t("p",[e._v("为了适应在消息上设置 SOAP 头和其他设置,"),t("code",[e._v("WebServiceMessageCallback")]),e._v("接口允许你在消息创建之后但在消息发送之前访问该消息。下面的示例演示了如何在通过编组对象创建的消息上设置 SOAP 动作头:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('public void marshalWithSoapActionHeader(MyObject o) {\n\n webServiceTemplate.marshalSendAndReceive(o, new WebServiceMessageCallback() {\n\n public void doWithMessage(WebServiceMessage message) {\n ((SoapMessage)message).setSoapAction("http://tempuri.org/Action");\n }\n });\n}\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("请注意,你也可以使用"),t("code",[e._v("org.springframework.ws.soap.client.core.SoapActionCallback")]),e._v("来设置 SOAP 动作标头。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h5",{attrs:{id:"ws-addressing"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#ws-addressing"}},[e._v("#")]),e._v(" WS-Addressing")]),e._v(" "),t("p",[e._v("Spring 除了"),t("a",{attrs:{href:"#server-ws-addressing"}},[e._v("服务器端 WS-Addressing")]),e._v("支持外,Web 服务还在客户端支持此规范。")]),e._v(" "),t("p",[e._v("为了在客户机上设置 WS-Addressing 头,你可以使用"),t("code",[e._v("RequestMatchers")]),e._v("。此回调以所需的动作标头作为参数。它还具有用于指定 WS-Addressing 版本的构造函数和"),t("code",[e._v("To")]),e._v("头。如果没有指定,"),t("code",[e._v("To")]),e._v("头默认为正在建立的连接的 URL。")]),e._v(" "),t("p",[e._v("下面的示例将"),t("code",[e._v("Action")]),e._v("标头设置为"),t("code",[e._v("[http://samples/RequestOrder](http://samples/RequestOrder)")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('webServiceTemplate.marshalSendAndReceive(o, new ActionCallback("http://samples/RequestOrder"));\n')])])]),t("h4",{attrs:{id:"_6-1-5-使用webservicemessageextractor"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-1-5-使用webservicemessageextractor"}},[e._v("#")]),e._v(" 6.1.5.使用"),t("code",[e._v("WebServiceMessageExtractor")])]),e._v(" "),t("p",[t("code",[e._v("WebServiceMessageExtractor")]),e._v("接口是一个低级的回调接口,你可以完全控制从接收到的"),t("code",[e._v("Object")]),e._v("中提取"),t("code",[e._v("Object")]),e._v("的过程。"),t("code",[e._v("WebServiceTemplate")]),e._v("在提供的"),t("code",[e._v("WebServiceMessageExtractor")]),e._v("上调用"),t("code",[e._v("extractData(..)")]),e._v("方法,同时与服务资源的底层连接仍处于打开状态。下面的示例显示了"),t("code",[e._v("WebServiceMessageExtractor")]),e._v("的作用:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public void marshalWithSoapActionHeader(final Source s) {\n final Transformer transformer = transformerFactory.newTransformer();\n webServiceTemplate.sendAndReceive(new WebServiceMessageCallback() {\n public void doWithMessage(WebServiceMessage message) {\n transformer.transform(s, message.getPayloadResult());\n },\n new WebServiceMessageExtractor() {\n public Object extractData(WebServiceMessage message) throws IOException {\n // do your own transforms with message.getPayloadResult()\n // or message.getPayloadSource()\n }\n }\n });\n}\n")])])]),t("h3",{attrs:{id:"_6-2-客户端测试"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-2-客户端测试"}},[e._v("#")]),e._v(" 6.2.客户端测试")]),e._v(" "),t("p",[e._v("当涉及到测试你的 Web 服务客户机(即使用"),t("code",[e._v("WebServiceTemplate")]),e._v("访问 Web 服务的类)时,你有两种可能的方法:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("编写单元测试,模拟"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("类、"),t("code",[e._v("WebServiceOperations")]),e._v("接口或完整的客户端类。")]),e._v(" "),t("p",[e._v("这种方法的优点是容易实现。缺点是,你实际上并不是在测试通过连接发送的 XML 消息的确切内容,尤其是在模拟整个客户机类时。")])]),e._v(" "),t("li",[t("p",[e._v("编写集成测试,测试消息的内容。")])])]),e._v(" "),t("p",[e._v("第一种方法可以通过模拟框架(如 EasyMock、JMock 等)轻松实现。下一节将重点讨论如何使用 Spring Web 服务 2.0 中介绍的测试特性编写集成测试。")]),e._v(" "),t("h4",{attrs:{id:"_6-2-1-编写客户端集成测试"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-2-1-编写客户端集成测试"}},[e._v("#")]),e._v(" 6.2.1.编写客户端集成测试")]),e._v(" "),t("p",[e._v("Spring Web 服务 2.0 引入了对创建 Web 服务客户端集成测试的支持。在这种情况下,客户机是使用"),t("code",[e._v("WebServiceTemplate")]),e._v("访问 Web 服务的类。")]),e._v(" "),t("p",[e._v("集成测试支持位于"),t("code",[e._v("org.springframework.ws.test.client")]),e._v("包中。包中的核心类是"),t("code",[e._v("MockWebServiceServer")]),e._v("。其基本思想是,Web 服务模板连接到这个模拟服务器,并向其发送一个请求消息,然后模拟服务器根据已注册的期望对其进行验证。如果期望得到满足,那么模拟服务器将准备一个响应消息,并将其发送回模板。")]),e._v(" "),t("p",[t("code",[e._v("MockWebServiceServer")]),e._v("的典型用法是:。")]),e._v(" "),t("ol",[t("li",[t("p",[e._v("通过调用"),t("code",[e._v("MockWebServiceServer.createServer(WebServiceTemplate)")]),e._v("、"),t("code",[e._v("MockWebServiceServer.createServer(WebServiceGatewaySupport)")]),e._v("或"),t("code",[e._v("MockWebServiceServer.createServer(ApplicationContext)")]),e._v("来创建"),t("code",[e._v("MockWebServiceServer")]),e._v("实例。")])]),e._v(" "),t("li",[t("p",[e._v("通过调用"),t("code",[e._v("expect(RequestMatcher)")]),e._v("来设置请求期望,可能需要使用"),t("code",[e._v("RequestMatcher")]),e._v("中提供的默认"),t("code",[e._v("RequestMatcher")]),e._v("实现(可以静态导入)。可以通过链接"),t("code",[e._v("andExpect(RequestMatcher)")]),e._v("调用来设置多个期望。")])]),e._v(" "),t("li",[t("p",[e._v("通过调用"),t("code",[e._v("andRespond(ResponseCreator)")]),e._v("创建适当的响应消息,可能需要使用"),t("code",[e._v("ResponseCreator")]),e._v("中提供的默认"),t("code",[e._v("ResponseCreator")]),e._v("实现(可以静态导入)。")])]),e._v(" "),t("li",[t("p",[e._v("正常地使用"),t("code",[e._v("WebServiceTemplate")]),e._v(",或者直接通过客户端代码。")])]),e._v(" "),t("li",[t("p",[e._v("请拨打"),t("code",[e._v("MockWebServiceServer.verify()")]),e._v(",以确保所有的期望都得到了满足。")])])]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("请注意,"),t("code",[e._v("MockWebServiceServer")]),e._v("(以及相关的类)提供了一个“fluent”API,因此你通常可以使用 IDE 中的代码完成功能来指导你完成设置模拟服务器的过程。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("还请注意,你可以在单元测试中依赖于 Spring Web 服务中可用的标准日志记录功能。有时,检查请求或响应消息以找出特定测试失败的原因可能是有用的。有关更多信息,请参见"),t("a",{attrs:{href:"#logging"}},[e._v("消息日志记录和跟踪")]),e._v("。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("例如,考虑以下 Web 服务客户机类:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.ws.client.core.support.WebServiceGatewaySupport;\n\npublic class CustomerClient extends WebServiceGatewaySupport { (1)\n\n public int getCustomerCount() {\n CustomerCountRequest request = new CustomerCountRequest(); (2)\n request.setCustomerName("John Doe");\n\n CustomerCountResponse response =\n (CustomerCountResponse) getWebServiceTemplate().marshalSendAndReceive(request); (3)\n\n return response.getCustomerCount();\n }\n\n}\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[t("code",[e._v("CustomerClient")]),e._v("扩展了"),t("code",[e._v("WebServiceGatewaySupport")]),e._v(",这为它提供了一个"),t("code",[e._v("webServiceTemplate")]),e._v("属性。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[t("code",[e._v("CustomerCountRequest")]),e._v("是一个由编组器支持的对象。例如,JAXB2 可以支持"),t("code",[e._v("@XmlRootElement")]),e._v("注释。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("3")])]),e._v(" "),t("td",[t("code",[e._v("CustomerClient")]),e._v("使用"),t("code",[e._v("WebServiceTemplate")]),e._v("提供的"),t("code",[e._v("WebServiceTemplate")]),e._v("将请求对象封送到 SOAP 消息中,并将其发送到 Web 服务。响应对象被解组为"),t("code",[e._v("CustomerCountResponse")]),e._v("。")])])])]),e._v(" "),t("p",[e._v("下面的示例显示了"),t("code",[e._v("CustomerClient")]),e._v("的典型测试:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import javax.xml.transform.Source;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.test.context.ContextConfiguration;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport org.springframework.xml.transform.StringSource;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.assertEquals;\n\nimport org.springframework.ws.test.client.MockWebServiceServer; (1)\nimport static org.springframework.ws.test.client.RequestMatchers.*; (1)\nimport static org.springframework.ws.test.client.ResponseCreators.*; (1)\n\n@RunWith(SpringJUnit4ClassRunner.class) (2)\n@ContextConfiguration("integration-test.xml") (2)\npublic class CustomerClientIntegrationTest {\n\n @Autowired\n private CustomerClient client; (3)\n\n private MockWebServiceServer mockServer; (4)\n\n @Before\n public void createServer() throws Exception {\n mockServer = MockWebServiceServer.createServer(client);\n }\n\n @Test\n public void customerClient() throws Exception {\n Source requestPayload = new StringSource(\n "" +\n "John Doe" +\n "");\n Source responsePayload = new StringSource(\n "" +\n "10" +\n "");\n\n mockServer.expect(payload(requestPayload)).andRespond(withPayload(responsePayload));(5)\n\n int result = client.getCustomerCount(); (6)\n assertEquals(10, result); (6)\n\n mockServer.verify(); (7)\n }\n\n}\n')])])]),t("table",[t("thead",[t("tr",[t("th",[t("strong",[e._v("1")])]),e._v(" "),t("th",[t("code",[e._v("CustomerClientIntegrationTest")]),e._v("导入"),t("code",[e._v("MockWebServiceServer")]),e._v(",静态导入"),t("code",[e._v("RequestMatchers")]),e._v("和"),t("code",[e._v("ResponseCreators")]),e._v("。")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("strong",[e._v("2")])]),e._v(" "),t("td",[e._v("该测试使用 Spring 框架中提供的标准测试设施。这不是必需的,但通常是设置测试的最简单方法。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("3")])]),e._v(" "),t("td",[t("code",[e._v("CustomerClient")]),e._v("在"),t("code",[e._v("integration-test.xml")]),e._v("中配置,并使用"),t("code",[e._v("@Autowired")]),e._v("连接到此测试中。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("4")])]),e._v(" "),t("td",[e._v("在"),t("code",[e._v("@Before")]),e._v("方法中,我们使用"),t("code",[e._v("createServer")]),e._v("工厂方法创建"),t("code",[e._v("MockWebServiceServer")]),e._v("方法。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("5")])]),e._v(" "),t("td",[e._v("我们通过使用静态导入的"),t("code",[e._v("RequestMatchers")]),e._v("提供的"),t("code",[e._v("payload()")]),e._v("调用"),t("code",[e._v("RequestMatcher")]),e._v("来定义期望(参见[使用"),t("code",[e._v("RequestMatcher")]),e._v(")和"),t("code",[e._v("RequestMatchers")]),e._v("])。"),t("br"),t("br"),e._v("我们还通过调用"),t("code",[e._v("andRespond()")]),e._v("与静态导入的"),t("code",[e._v("withPayload()``ResponseCreator")]),e._v("(参见[使用"),t("code",[e._v("ResponseCreator")]),e._v("和"),t("code",[e._v("ResponseCreator")]),e._v("](client-test-responsion-#)来设置响应)。但是 IDE 的代码完成功能非常有帮助。在键入"),t("code",[e._v("expect(")]),e._v("之后,IDE 可以为你提供一个可能的请求匹配策略列表,前提是静态导入"),t("code",[e._v("RequestMatchers")]),e._v("。如果以静态方式导入"),t("code",[e._v("ResponseCreators")]),e._v(",则适用于"),t("code",[e._v("andRespond(")]),e._v("。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("6")])]),e._v(" "),t("td",[e._v("我们在"),t("code",[e._v("CustomerClient")]),e._v("上调用"),t("code",[e._v("getCustomerCount()")]),e._v(",从而使用"),t("code",[e._v("WebServiceTemplate")]),e._v("。到目前为止,模板已经为“测试模式”设置好了,因此这个方法调用不会产生真正的连接。我们还根据方法调用的结果进行了一些 JUnit 断言。")])]),e._v(" "),t("tr",[t("td",[t("strong",[e._v("7")])]),e._v(" "),t("td",[e._v("我们在"),t("code",[e._v("MockWebServiceServer")]),e._v("上调用"),t("code",[e._v("verify()")]),e._v(",以验证是否实际收到了预期的消息。")])])])]),e._v(" "),t("h4",{attrs:{id:"_6-2-2-使用requestmatcher和requestmatchers"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-2-2-使用requestmatcher和requestmatchers"}},[e._v("#")]),e._v(" 6.2.2.使用"),t("code",[e._v("RequestMatcher")]),e._v("和"),t("code",[e._v("RequestMatchers")])]),e._v(" "),t("p",[e._v("为了验证请求消息是否满足某些期望,"),t("code",[e._v("MockWebServiceServer")]),e._v("使用"),t("code",[e._v("RequestMatcher")]),e._v("策略接口。此接口定义的契约如下:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public interface RequestMatcher {\n\n void match(URI uri,\n WebServiceMessage request)\n throws IOException,\n AssertionError;\n}\n")])])]),t("p",[e._v("你可以编写你自己的这个接口的实现,当消息不满足你的期望时抛出"),t("code",[e._v("RequestMatcher")]),e._v("异常,但是你当然不必这样做。"),t("code",[e._v("RequestMatchers")]),e._v("类提供了标准的"),t("code",[e._v("RequestMatcher")]),e._v("实现,供你在测试中使用。你通常静态地导入这个类。")]),e._v(" "),t("p",[t("code",[e._v("RequestMatcher")]),e._v("类提供了以下请求匹配器:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[t("code",[e._v("RequestMatchers")]),e._v(" method")]),e._v(" "),t("th",[e._v("说明")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("code",[e._v("anything()")])]),e._v(" "),t("td",[e._v("期待任何类型的请求。")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("payload()")])]),e._v(" "),t("td",[e._v("期望给定的请求有效负载。")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("validPayload()")])]),e._v(" "),t("td",[e._v("期望请求有效负载根据给定的 XSD 模式进行验证。")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("xpath()")])]),e._v(" "),t("td",[e._v("期望给定的 XPath 表达式存在、不存在或求值到给定值。")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("soapHeader()")])]),e._v(" "),t("td",[e._v("期望请求消息中存在给定的 SOAP 头。")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("connectionTo()")])]),e._v(" "),t("td",[e._v("期望到给定 URL 的连接。")])])])]),e._v(" "),t("p",[e._v("你可以通过链接"),t("code",[e._v("andExpect()")]),e._v("调用来设置多个请求期望:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('mockServer.expect(connectionTo("http://example.com")).\n andExpect(payload(expectedRequestPayload)).\n andExpect(validPayload(schemaResource)).\n andRespond(...);\n')])])]),t("p",[e._v("有关"),t("code",[e._v("RequestMatchers")]),e._v("提供的请求匹配器的更多信息,请参见"),t("a",{attrs:{href:"https://docs.spring.io/spring-ws/docs/current/api/org/springframework/ws/test/client/RequestMatchers.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_6-2-3-使用responsecreator和responsecreators"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-2-3-使用responsecreator和responsecreators"}},[e._v("#")]),e._v(" 6.2.3.使用"),t("code",[e._v("ResponseCreator")]),e._v("和"),t("code",[e._v("ResponseCreators")])]),e._v(" "),t("p",[e._v("当请求消息经过验证并满足已定义的期望时,"),t("code",[e._v("MockWebServiceServer")]),e._v("将为"),t("code",[e._v("WebServiceTemplate")]),e._v("创建一个要使用的响应消息。服务器为此目的使用"),t("code",[e._v("ResponseCreator")]),e._v("策略接口:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("public interface ResponseCreator {\n\n WebServiceMessage createResponse(URI uri,\n WebServiceMessage request,\n WebServiceMessageFactory messageFactory)\n throws IOException;\n\n}\n")])])]),t("p",[e._v("再一次,你可以编写你自己的这个接口的实现,通过使用消息工厂来创建响应消息,但是你当然不必这样做,因为"),t("code",[e._v("ResponseCreator")]),e._v("类提供了标准的"),t("code",[e._v("ResponseCreator")]),e._v("实现,供你在测试中使用。你通常静态地导入这个类。")]),e._v(" "),t("p",[t("code",[e._v("ResponseCreators")]),e._v("类提供以下响应:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[t("code",[e._v("ResponseCreators")]),e._v(" method")]),e._v(" "),t("th",[e._v("说明")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("code",[e._v("withPayload()")])]),e._v(" "),t("td",[e._v("使用给定的有效负载创建响应消息。")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("withError()")])]),e._v(" "),t("td",[e._v("在响应连接中创建错误。这个方法为你提供了测试错误处理的机会。")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("withException()")])]),e._v(" "),t("td",[e._v("从响应连接读取时抛出异常。这个方法为你提供了测试异常处理的机会。")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("withMustUnderstandFault()")]),e._v(", "),t("code",[e._v("withClientOrSenderFault()")]),e._v(", "),t("code",[e._v("withServerOrReceiverFault()")]),e._v(", or "),t("code",[e._v("withVersionMismatchFault()")])]),e._v(" "),t("td",[e._v("创建带有给定 SOAP 错误的响应消息。这个方法为你提供了测试错误处理的机会。")])])])]),e._v(" "),t("p",[e._v("有关"),t("code",[e._v("RequestMatchers")]),e._v("提供的请求匹配器的更多信息,请参见"),t("a",{attrs:{href:"https://docs.spring.io/spring-ws/docs/current/api/org/springframework/ws/test/client/RequestMatchers.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Javadoc"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("h2",{attrs:{id:"_7-使用-spring-ws-保护你的-web-服务"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-使用-spring-ws-保护你的-web-服务"}},[e._v("#")]),e._v(" 7.使用 Spring-WS 保护你的 Web 服务")]),e._v(" "),t("p",[e._v("本章将解释如何将 WS-Security 方面添加到 Web 服务中。我们关注 WS-Security 的三个不同领域:")]),e._v(" "),t("ul",[t("li",[t("p",[t("strong",[e._v("认证")]),e._v(":这是确定委托人是否是他们所声称的人的过程。在这种情况下,“主体”通常是指可以在应用程序中执行操作的用户、设备或其他系统。")])]),e._v(" "),t("li",[t("p",[t("strong",[e._v("数字签名")]),e._v(":消息的数字签名是基于文档和签名者的私钥的一条信息。它是通过使用散列函数和私人签名函数(使用签名者的私钥加密)创建的。")])]),e._v(" "),t("li",[t("p",[t("strong",[e._v("加密和解密")]),e._v(":加密是将数据转换为没有适当密钥就无法读取的形式的过程。它主要是用来隐藏信息,不让任何人知道它不是为谁准备的。解密是加密的反面。它是将加密数据转换回可读形式的过程。")])])]),e._v(" "),t("p",[e._v("这三个区域是通过使用"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("或"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("来实现的,我们在["),t("code",[e._v("XwsSecurityInterceptor")]),e._v("](#security-xws-security-interceptor)和[using"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("](#security-wss4j-security-interceptor)中分别进行了描述")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("请注意,WS-Security(特别是加密和签名)需要大量的内存,并且可能会降低性能。如果性能对你很重要,那么你可能想要考虑不使用 WS-Security 或使用基于 HTTP 的安全性。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h3",{attrs:{id:""}},[t("a",{staticClass:"header-anchor",attrs:{href:"#"}},[e._v("#")])]),e._v(" "),t("p",[t("code",[e._v("XwsSecurityInterceptor")]),e._v("是一个"),t("code",[e._v("EndpointInterceptor")]),e._v("(参见[拦截请求-"),t("code",[e._v("EndpointInterceptor")]),e._v("接口](#server-endpoint-interceptor)),它基于 Sun 的 XML 和 Web 服务安全包。这个 WS-Security 实现是 Java Web 服务开发人员包("),t("a",{attrs:{href:"http://java.sun.com/webservices/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Java WSDP"),t("OutboundLink")],1),e._v(")的一部分。")]),e._v(" "),t("p",[e._v("像任何其他端点拦截器一样,它是在端点映射中定义的(参见"),t("a",{attrs:{href:"#server-endpoint-mapping"}},[e._v("端点映射")]),e._v(")。这意味着你可以选择性地添加 WS-Security 支持。一些端点映射需要它,而另一些则不需要。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("请注意,XWSS 既需要 Sun1.5JDK,也需要 Sun Saaj 参考实现。WSS4J 拦截器没有这些需求(参见[using"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("](#security-wss4j-security-interceptor))。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[t("code",[e._v("XwsSecurityInterceptor")]),e._v("需要一个安全策略文件来操作。这个 XML 文件告诉拦截器从传入的 SOAP 消息中需要哪些安全方面,以及向传出的消息中添加哪些方面。策略文件的基本格式在下面的小节中进行了说明,但是你可以找到更深入的教程"),t("a",{attrs:{href:"http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp564887",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("。你可以使用"),t("code",[e._v("policyConfiguration")]),e._v("属性设置策略,这需要 Spring 资源。策略文件可以包含多个元素——例如,需要对传入消息使用用户名令牌,并对所有传出消息进行签名。它包含一个"),t("code",[e._v("SecurityConfiguration")]),e._v("元素(而不是"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("元素)作为它的根。")]),e._v(" "),t("p",[e._v("此外,安全拦截器需要一个或多个"),t("code",[e._v("CallbackHandler")]),e._v("实例来操作。这些处理程序用于检索证书、私钥、验证用户凭据,等等。 Spring-WS 为最常见的安全问题提供了处理程序——例如,针对 Spring 安全身份验证管理器进行身份验证,并基于 X509 证书对传出消息进行签名。下面的小节指出了要在哪个安全问题上使用什么回调处理程序。你可以使用"),t("code",[e._v("callbackHandler")]),e._v("或"),t("code",[e._v("callbackHandlers")]),e._v("属性来设置回调处理程序。")]),e._v(" "),t("p",[e._v("下面的示例展示了如何连接"),t("code",[e._v("XwsSecurityInterceptor")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n ...\n\n')])])]),t("p",[e._v("这个拦截器是通过使用 Classpath 上的"),t("code",[e._v("securityPolicy.xml")]),e._v("文件来配置的。它使用了稍后在文件中定义的两个回调处理程序。")]),e._v(" "),t("h4",{attrs:{id:"_7-1-1-密钥存储库"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-1-1-密钥存储库"}},[e._v("#")]),e._v(" 7.1.1.密钥存储库")]),e._v(" "),t("p",[e._v("对于大多数加密操作,你都使用标准的"),t("code",[e._v("java.security.KeyStore")]),e._v("对象。这些操作包括证书验证、消息签名、签名验证和加密。它们不包括用户名和时间戳验证。本节旨在为你提供一些关于密钥存储库和 Java 工具的背景知识,你可以使用这些工具将密钥和证书存储在密钥存储库文件中。这些信息大多与 Spring-WS 无关,而是与 Java 的一般加密特性有关。")]),e._v(" "),t("p",[t("code",[e._v("java.security.KeyStore")]),e._v("类表示用于加密密钥和证书的存储工具。它可以包含三种不同的元素:")]),e._v(" "),t("ul",[t("li",[t("p",[t("strong",[e._v("私钥")]),e._v(":这些密钥用于自我验证。私钥伴随着相应的公钥的证书链。在 WS-Security 字段中,这是消息签名和消息解密的帐户。")])]),e._v(" "),t("li",[t("p",[t("strong",[e._v("对称按键")]),e._v(":对称(或秘密)密钥也用于消息加密和解密——区别在于双方(发送方和接收方)共享相同的密钥。")])]),e._v(" "),t("li",[t("p",[t("strong",[e._v("可信证书")]),e._v(":这些 X509 证书被称为“受信任证书”,因为密钥存储库所有者相信证书中的公钥确实属于证书的所有者。在 WS-Security 中,这些证书用于证书验证、签名验证和加密。")])])]),e._v(" "),t("h5",{attrs:{id:"使用keytool"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用keytool"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("keytool")])]),e._v(" "),t("p",[t("code",[e._v("keytool")]),e._v("程序是一个密钥和证书管理实用程序,它与你的 Java 虚拟机一起提供。你可以使用此工具创建新的密钥库,向它们添加新的私钥和证书,等等。提供"),t("code",[e._v("keytool")]),e._v("命令的完整引用超出了本文档的范围,但是你可以在命令行中找到"),t("a",{attrs:{href:"http://java.sun.com/j2se/1.5.0/docs/tooldocs/windows/keytool.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("或使用"),t("code",[e._v("@XmlRootElement")]),e._v("命令的引用。")]),e._v(" "),t("h5",{attrs:{id:"使用keystorefactorybean"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用keystorefactorybean"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("KeyStoreFactoryBean")])]),e._v(" "),t("p",[e._v("要使用 Spring 配置轻松加载密钥库,可以使用"),t("code",[e._v("KeyStoreFactoryBean")]),e._v("。它有一个资源位置属性,你可以将其设置为指向要加载的密钥库的路径。可以给出一个密码来检查密钥存储库数据的 Integrity。如果没有给出密码,则不执行 Integrity 检查。下面的清单配置了"),t("code",[e._v("KeyStoreFactoryBean")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("如果没有指定 location 属性,就会创建一个新的空密钥库,这很可能不是你想要的。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h5",{attrs:{id:"keystorecallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#keystorecallbackhandler"}},[e._v("#")]),e._v(" KeystoRecallBackHandler")]),e._v(" "),t("p",[e._v("要在"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("中使用密钥存储库,你需要定义"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("。这个回调有三个类型为"),t("code",[e._v("keystore")]),e._v("的属性:("),t("code",[e._v("keyStore")]),e._v(","),t("code",[e._v("trustStore")]),e._v(",和"),t("code",[e._v("symmetricStore")]),e._v(")。处理程序所使用的确切存储取决于该处理程序要执行的加密操作。对于私钥操作,使用"),t("code",[e._v("keyStore")]),e._v("。对于对称的键操作,使用"),t("code",[e._v("symmetricStore")]),e._v("。为了确定信任关系,使用了"),t("code",[e._v("trustStore")]),e._v("。下表表明了这一点:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[e._v("加密操作")]),e._v(" "),t("th",[e._v("Keystore used")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[e._v("证书验证")]),e._v(" "),t("td",[e._v("First "),t("code",[e._v("keyStore")]),e._v(", then "),t("code",[e._v("trustStore")])])]),e._v(" "),t("tr",[t("td",[e._v("基于私钥的解密技术")]),e._v(" "),t("td",[t("code",[e._v("keyStore")])])]),e._v(" "),t("tr",[t("td",[e._v("基于对称密钥的解密方法")]),e._v(" "),t("td",[t("code",[e._v("symmetricStore")])])]),e._v(" "),t("tr",[t("td",[e._v("基于公钥证书的加密方法")]),e._v(" "),t("td",[t("code",[e._v("trustStore")])])]),e._v(" "),t("tr",[t("td",[e._v("基于对称密钥的加密方法")]),e._v(" "),t("td",[t("code",[e._v("symmetricStore")])])]),e._v(" "),t("tr",[t("td",[e._v("签名")]),e._v(" "),t("td",[t("code",[e._v("keyStore")])])]),e._v(" "),t("tr",[t("td",[e._v("签名验证")]),e._v(" "),t("td",[t("code",[e._v("trustStore")])])])])]),e._v(" "),t("p",[e._v("此外,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("具有"),t("code",[e._v("privateKeyPassword")]),e._v("属性,应将其设置为解锁"),t("code",[e._v("keyStore")]),e._v("中包含的私钥。")]),e._v(" "),t("p",[e._v("如果"),t("code",[e._v("symmetricStore")]),e._v("未设置,则默认为"),t("code",[e._v("keyStore")]),e._v("。如果未设置键或信任存储区,则回调处理程序使用标准的 Java 机制来加载或创建它。参见"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("的 Javadoc 来了解这个机制是如何工作的。")]),e._v(" "),t("p",[e._v("例如,如果希望使用"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("来验证传入的证书或签名,则可以使用信任存储区:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n \n \n \n \n\n')])])]),t("p",[e._v("如果你想使用它来解密传入的证书或对传出的消息进行签名,则可以使用密钥存储区:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n\n \n \n \n \n\n')])])]),t("p",[e._v("下面的部分指出了"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("可以在哪里使用,以及为特定的加密操作设置哪些属性。")]),e._v(" "),t("h4",{attrs:{id:"_7-1-2-认证"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-1-2-认证"}},[e._v("#")]),e._v(" 7.1.2.认证")]),e._v(" "),t("p",[e._v("正如"),t("a",{attrs:{href:"#security"}},[e._v("本章导言")]),e._v("中所述,身份验证是确定主体是否是他们所声称的人的任务。在 WS-Security 中,身份验证可以采取两种形式:使用用户名和密码令牌(使用纯文本密码或密码摘要)或使用 X509 证书。")]),e._v(" "),t("h5",{attrs:{id:"纯文本用户名身份验证"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#纯文本用户名身份验证"}},[e._v("#")]),e._v(" 纯文本用户名身份验证")]),e._v(" "),t("p",[e._v("最简单的用户名身份验证形式使用纯文本密码。在这种情况下,SOAP 消息包含一个"),t("code",[e._v("UsernameToken")]),e._v("元素,它本身包含一个"),t("code",[e._v("Username")]),e._v("元素和一个"),t("code",[e._v("Password")]),e._v("元素,它包含纯文本密码。纯文本身份验证可以与 HTTP 服务器提供的基本身份验证进行比较。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("请注意,纯文本密码不是很安全。因此,如果你使用传输层,你应该始终向它们添加额外的安全措施(例如,使用 HTTPS 而不是普通的 HTTP)。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("为了要求每条传入的消息都包含带有纯文本密码的"),t("code",[e._v("UsernameToken")]),e._v(",安全策略文件应该包含一个"),t("code",[e._v("RequireUsernameToken")]),e._v("元素,并将"),t("code",[e._v("passwordDigestRequired")]),e._v("属性设置为"),t("code",[e._v("false")]),e._v("。你可以找到可能的子元素"),t("a",{attrs:{href:"http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp567459",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("的引用。下面的清单显示了如何包含"),t("code",[e._v("RequireUsernameToken")]),e._v("元素:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n ...\n \n ...\n\n')])])]),t("p",[e._v("如果不存在用户名令牌,"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("将向发送方返回一个 SOAP 错误。如果存在,它将向注册的处理程序发送一个"),t("code",[e._v("PasswordValidationCallback")]),e._v("和一个"),t("code",[e._v("PlainTextPasswordRequest")]),e._v("。在 Spring-WS 中,有三个类处理这个特定的回调。")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("["),t("code",[e._v("SimplePasswordValidationCallbackHandler")]),e._v("](#security-simple-password-validation-callback-handler)")])]),e._v(" "),t("li",[t("p",[e._v("["),t("code",[e._v("XwsSecurityInterceptor")]),e._v("](#using-springplaintextpasswordvalidationcallbackhandler)")])]),e._v(" "),t("li",[t("p",[e._v("["),t("code",[e._v("JaasPlainTextPasswordValidationCallbackHandler")]),e._v("](#using-jaasplaintextpasswordvalidationcallbackhandler)")])])]),e._v(" "),t("h6",{attrs:{id:"使用simplepasswordvalidationcallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用simplepasswordvalidationcallbackhandler"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("SimplePasswordValidationCallbackHandler")])]),e._v(" "),t("p",[e._v("最简单的密码验证处理程序是"),t("code",[e._v("SimplePasswordValidationCallbackHandler")]),e._v("。此处理程序针对内存中的"),t("code",[e._v("Properties")]),e._v("对象验证密码,你可以将该对象指定为"),t("code",[e._v("users")]),e._v("属性:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n Ernie\n \n \n\n')])])]),t("p",[e._v("在这种情况下,我们只允许用户“BERT”使用密码“ERNIE”登录。")]),e._v(" "),t("h6",{attrs:{id:"使用springplaintextpasswordvalidationcallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用springplaintextpasswordvalidationcallbackhandler"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("SpringPlainTextPasswordValidationCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("SpringPlainTextPasswordValidationCallbackHandler")]),e._v("使用"),t("a",{attrs:{href:"https://spring.io/projects/spring-security",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Security"),t("OutboundLink")],1),e._v("对用户进行身份验证。描述 Spring 安全性超出了本文的范围,但它是一个成熟的安全框架。你可以在"),t("a",{attrs:{href:"https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Security reference documentation"),t("OutboundLink")],1),e._v("中阅读有关它的更多信息。")]),e._v(" "),t("p",[t("code",[e._v("AuthenticationManager")]),e._v("需要"),t("code",[e._v("AuthenticationManager")]),e._v("才能操作。它使用此管理器针对它创建的"),t("code",[e._v("UsernamePasswordAuthenticationToken")]),e._v("进行身份验证。如果身份验证成功,则将令牌存储在"),t("code",[e._v("SecurityContextHolder")]),e._v("中。你可以使用"),t("code",[e._v("authenticationManager")]),e._v("属性设置身份验证管理器:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n \n \n \n \n \n \n \n\n \n ...\n\n')])])]),t("h6",{attrs:{id:"使用jaasplaintextpasswordvalidationcallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用jaasplaintextpasswordvalidationcallbackhandler"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("JaasPlainTextPasswordValidationCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("JaasPlainTextPasswordValidationCallbackHandler")]),e._v("基于标准"),t("a",{attrs:{href:"http://java.sun.com/products/jaas/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Java 身份验证和授权服务"),t("OutboundLink")],1),e._v("。提供对 JAAS 的完整介绍超出了本文的范围,但是"),t("a",{attrs:{href:"http://www.javaworld.com/javaworld/jw-09-2002/jw-0913-jaas.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("好的教程"),t("OutboundLink")],1),e._v("是可用的。")]),e._v(" "),t("p",[t("code",[e._v("JaasPlainTextPasswordValidationCallbackHandler")]),e._v("只需要一个"),t("code",[e._v("loginContextName")]),e._v("就可以操作。它使用这个名称创建一个新的 JAAS,并使用 SOAP 消息中提供的用户名和密码处理标准的 JAAS和。这意味着此回调处理程序与在"),t("code",[e._v("login()")]),e._v("阶段(这是标准行为)期间触发这些回调的任何 JAAS"),t("code",[e._v("LoginModule")]),e._v("集成。")]),e._v(" "),t("p",[e._v("你可以按以下方式连接"),t("code",[e._v("JaasPlainTextPasswordValidationCallbackHandler")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n\n')])])]),t("p",[e._v("在这种情况下,回调处理程序使用名为"),t("code",[e._v("LoginContext")]),e._v("的"),t("code",[e._v("MyLoginModule")]),e._v("。这个模块应该在"),t("code",[e._v("jaas.config")]),e._v("文件中定义,如"),t("a",{attrs:{href:"http://www.javaworld.com/javaworld/jw-09-2002/jw-0913-jaas.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("前面提到的教程"),t("OutboundLink")],1),e._v("中所解释的那样。")]),e._v(" "),t("h5",{attrs:{id:"摘要用户名身份验证"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#摘要用户名身份验证"}},[e._v("#")]),e._v(" 摘要用户名身份验证")]),e._v(" "),t("p",[e._v("在使用密码摘要时,SOAP 消息还包含一个"),t("code",[e._v("UsernameToken")]),e._v("元素,该元素本身包含一个"),t("code",[e._v("Username")]),e._v("元素和一个"),t("code",[e._v("Password")]),e._v("元素。不同之处在于,密码不是以纯文本的形式发送的,而是以摘要的形式发送的。收件人将此摘要与他根据用户的已知密码计算出的摘要进行比较,如果它们是相同的,则对用户进行身份验证。这种方法类似于 HTTP 服务器提供的摘要身份验证。")]),e._v(" "),t("p",[e._v("要要求每个传入的消息都包含带有密码摘要的"),t("code",[e._v("UsernameToken")]),e._v("元素,安全策略文件应该包含一个"),t("code",[e._v("securityPolicy.xml")]),e._v("元素,并将"),t("code",[e._v("passwordDigestRequired")]),e._v("属性设置为"),t("code",[e._v("true")]),e._v("。另外,"),t("code",[e._v("nonceRequired")]),e._v("属性应该设置为"),t("code",[e._v("true")]),e._v(":你可以找到可能的子元素"),t("a",{attrs:{href:"http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp567459",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("的引用。下面的清单展示了如何定义"),t("code",[e._v("RequireUsernameToken")]),e._v("元素:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n ...\n \n ...\n\n')])])]),t("p",[e._v("如果不存在用户名令牌,"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("将向发送方返回一个 SOAP 错误。如果它存在,它将向注册的处理程序发送一个"),t("code",[e._v("PasswordValidationCallback")]),e._v("和一个"),t("code",[e._v("DigestPasswordRequest")]),e._v("。在 Spring-WS 中,有两个类处理这个特定的回调:"),t("code",[e._v("SimplePasswordValidationCallbackHandler")]),e._v("和"),t("code",[e._v("SpringDigestPasswordValidationCallbackHandler")]),e._v("。")]),e._v(" "),t("h6",{attrs:{id:"使用simplepasswordvalidationcallbackhandler-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用simplepasswordvalidationcallbackhandler-2"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("SimplePasswordValidationCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("SimplePasswordValidationCallbackHandler")]),e._v("既可以处理纯文本密码,也可以处理密码摘要。在[using"),t("code",[e._v("SimplePasswordValidationCallbackHandler")]),e._v("](#security-simple-password-validation-callback-handler)中进行了描述。")]),e._v(" "),t("h6",{attrs:{id:"使用springdigestpasswordvalidationcallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用springdigestpasswordvalidationcallbackhandler"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("SpringDigestPasswordValidationCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("SpringDigestPasswordValidationCallbackHandler")]),e._v("需要 Spring 证券"),t("code",[e._v("UserDetailService")]),e._v("才能操作。它使用此服务检索令牌中指定的用户的密码。然后将包含在此 Details 对象中的密码摘要与消息中的摘要进行比较。如果它们相等,则用户已成功地进行了身份验证,并且"),t("code",[e._v("UsernamePasswordAuthenticationToken")]),e._v("存储在"),t("code",[e._v("SecurityContextHolder")]),e._v("中。你可以使用"),t("code",[e._v("userDetailsService")]),e._v("属性设置服务。此外,还可以设置"),t("code",[e._v("userCache")]),e._v("属性,以缓存已加载的用户详细信息。下面的示例展示了如何做到这一点:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n \n ...\n\n')])])]),t("h5",{attrs:{id:"证书认证"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#证书认证"}},[e._v("#")]),e._v(" 证书认证")]),e._v(" "),t("p",[e._v("一种更安全的认证方式是使用 X509 证书。在此场景中,SOAP 消息包含"),t("code",[e._v("BinarySecurityToken")]),e._v(",其中包含 x509 证书的 base64 编码版本。该证书由收件人使用以进行身份验证。存储在消息中的证书也用于对消息进行签名(参见"),t("a",{attrs:{href:"#security-verifying-signatures"}},[e._v("验证签名")]),e._v(")。")]),e._v(" "),t("p",[e._v("为了确保所有传入的 SOAP 消息都带有"),t("code",[e._v("BinarySecurityToken")]),e._v(",安全策略文件应该包含"),t("code",[e._v("RequireSignature")]),e._v("元素。这个元素还可以携带其他元素,这些元素在"),t("a",{attrs:{href:"#security-verifying-signatures"}},[e._v("验证签名")]),e._v("中被覆盖。你可以找到可能的子元素"),t("a",{attrs:{href:"http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp565769",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("的引用。下面的清单展示了如何定义"),t("code",[e._v("RequireSignature")]),e._v("元素:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n ...\n \n ...\n\n')])])]),t("p",[e._v("当一条没有证书的消息到达时,"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("将向发送方返回一个 SOAP 错误。如果它存在,它将触发"),t("code",[e._v("CertificateValidationCallback")]),e._v("。 Spring-WS 中的三个处理程序处理此回调以用于身份验证:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("["),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("](#using-keystoRecallbackhandler)")])]),e._v(" "),t("li",[t("p",[e._v("["),t("code",[e._v("SpringCertificateValidationCallbackHandler")]),e._v("](#using-springcertificateValidationCallbackHandler)")])]),e._v(" "),t("li",[t("p",[e._v("["),t("code",[e._v("JaasCertificateValidationCallbackHandler")]),e._v("](#using-jaascertificateValidationCallbackHandler)")])])]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("在大多数情况下,证书身份验证之前应该有证书验证,因为你希望仅针对有效的证书进行身份验证。无效的证书,例如过期日期已经过的证书或不在受信任证书存储区中的证书,应该被忽略。"),t("br"),t("br"),e._v("在 Spring-WS 术语中,这意味着"),t("code",[e._v("SpringCertificateValidationCallbackHandler")]),e._v("或"),t("code",[e._v("JaasCertificateValidationCallbackHandler")]),e._v("应该在"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("之前。这可以通过在"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("的配置中设置"),t("code",[e._v("callbackHandlers")]),e._v("属性的顺序来实现:"),t("br"),t("br"),t("code",[e._v('
class="org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor">








')]),t("br"),e._v("拦截器首先使用密钥存储库确定消息中的证书是否有效,然后对其进行身份验证。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h6",{attrs:{id:"使用keystorecallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用keystorecallbackhandler"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("KeyStoreCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("KeyStoreCallbackHandler")]),e._v("使用标准的 Java 密钥存储库来验证证书。此证书验证过程包括以下步骤:。")]),e._v(" "),t("ol",[t("li",[t("p",[e._v("处理程序检查证书是否在私有"),t("code",[e._v("keyStore")]),e._v("中。如果它是,它是有效的。")])]),e._v(" "),t("li",[t("p",[e._v("如果证书不在私钥存储库中,则处理程序将检查当前日期和时间是否在证书中给出的有效期内。如果不是,则证书无效。如果是这样,它将继续进行最后一步。")])]),e._v(" "),t("li",[t("p",[e._v("将为证书创建一个认证路径。这基本上意味着处理程序确定证书是否由"),t("code",[e._v("trustStore")]),e._v("中的任何证书颁发机构颁发。如果可以成功地构建一个认证路径,则该证书是有效的。否则,证书无效。")])])]),e._v(" "),t("p",[e._v("要将"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("用于证书验证目的,你很可能只需要设置"),t("code",[e._v("trustStore")]),e._v("属性:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n \n \n \n \n\n')])])]),t("p",[e._v("使用前面示例中显示的设置,要验证的证书必须位于信任存储区本身,或者信任存储区必须包含颁发证书的证书颁发机构。")]),e._v(" "),t("h6",{attrs:{id:"使用springcertificatevalidationcallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用springcertificatevalidationcallbackhandler"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("SpringCertificateValidationCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("SpringCertificateValidationCallbackHandler")]),e._v("需要 Spring 证券"),t("code",[e._v("AuthenticationManager")]),e._v("才能操作。它使用此管理器对它创建的"),t("code",[e._v("X509AuthenticationToken")]),e._v("进行身份验证。配置的身份验证管理器需要提供一个能够处理这个令牌的提供者(通常是"),t("code",[e._v("X509AuthenticationProvider")]),e._v("的实例)。如果身份验证成功,则将令牌存储在"),t("code",[e._v("SecurityContextHolder")]),e._v("中。你可以使用"),t("code",[e._v("authenticationManager")]),e._v("属性设置身份验证管理器:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n\n \n ...\n\n')])])]),t("p",[e._v("在这种情况下,我们使用自定义用户详细信息服务来基于证书获得身份验证详细信息。有关针对 X509 证书的身份验证的更多信息,请参见"),t("a",{attrs:{href:"http://www.springframework.org/security",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Security reference documentation"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("h6",{attrs:{id:"使用jaascertificatevalidationcallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用jaascertificatevalidationcallbackhandler"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("JaasCertificateValidationCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("JaasCertificateValidationCallbackHandler")]),e._v("需要"),t("code",[e._v("loginContextName")]),e._v("才能操作。它使用证书的这个名称和"),t("code",[e._v("X500Principal")]),e._v("创建一个新的 JAAS"),t("code",[e._v("LoginContext")]),e._v("。这意味着此回调处理程序与处理 X500 主体的任何 JAAS"),t("code",[e._v("LoginModule")]),e._v("集成。")]),e._v(" "),t("p",[e._v("你可以按以下方式连接"),t("code",[e._v("JaasCertificateValidationCallbackHandler")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n MyLoginModule\n\n')])])]),t("p",[e._v("在这种情况下,回调处理程序使用名为"),t("code",[e._v("LoginContext")]),e._v("的"),t("code",[e._v("MyLoginModule")]),e._v("。这个模块应该在你的"),t("code",[e._v("jaas.config")]),e._v("文件中定义,并且应该能够针对 X500 主体进行身份验证。")]),e._v(" "),t("h4",{attrs:{id:"_7-1-3-数字签名"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-1-3-数字签名"}},[e._v("#")]),e._v(" 7.1.3.数字签名")]),e._v(" "),t("p",[e._v("消息的数字签名是基于文档和签名者的私钥的一条信息。与 WS-Security 中的签名相关的两个主要任务是:验证签名和签名消息。")]),e._v(" "),t("h5",{attrs:{id:"验证签名"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#验证签名"}},[e._v("#")]),e._v(" 验证签名")]),e._v(" "),t("p",[e._v("与"),t("a",{attrs:{href:"#security-certificate-authentication"}},[e._v("基于证书的身份验证")]),e._v("一样,已签名的消息包含"),t("code",[e._v("BinarySecurityToken")]),e._v(",其中包含用于对消息进行签名的证书。此外,它还包含一个"),t("code",[e._v("SignedInfo")]),e._v("块,该块指示消息的哪一部分已签名。")]),e._v(" "),t("p",[e._v("为了确保所有传入的 SOAP 消息都带有"),t("code",[e._v("BinarySecurityToken")]),e._v(",安全策略文件应该包含"),t("code",[e._v("RequireSignature")]),e._v("元素。它还可以包含"),t("code",[e._v("SignatureTarget")]),e._v("元素,该元素指定预期要签名的目标消息部分和各种其他子元素。你还可以定义要使用的私钥别名、是否使用对称密钥而不是私钥以及许多其他属性。你可以找到可能的子元素"),t("a",{attrs:{href:"http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp565769",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("的引用。下面的清单配置了"),t("code",[e._v("RequireSignature")]),e._v("元素:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n\n')])])]),t("p",[e._v("如果签名不存在,"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("将向发送方返回一个 SOAP 错误。如果存在,它将向已注册的处理程序触发"),t("code",[e._v("SignatureVerificationKeyCallback")]),e._v("。在 Spring-WS 中,有一个类处理这个特定的回调:"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("。")]),e._v(" "),t("h6",{attrs:{id:"使用keystorecallbackhandler-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用keystorecallbackhandler-2"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("KeyStoreCallbackHandler")])]),e._v(" "),t("p",[e._v("如"),t("a",{attrs:{href:"#security-key-store-callback-handler"}},[e._v("KeystoRecallBackHandler")]),e._v("中所述,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("使用"),t("code",[e._v("java.security.KeyStore")]),e._v("来处理各种加密回调,包括签名验证。对于签名验证,处理程序使用"),t("code",[e._v("trustStore")]),e._v("属性:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n \n \n \n \n\n')])])]),t("h5",{attrs:{id:"签名消息"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#签名消息"}},[e._v("#")]),e._v(" 签名消息")]),e._v(" "),t("p",[e._v("签名消息时,"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("将"),t("code",[e._v("BinarySecurityToken")]),e._v("添加到消息中。它还添加了一个"),t("code",[e._v("SignedInfo")]),e._v("块,该块指示消息的哪一部分已签名。")]),e._v(" "),t("p",[e._v("要对所有传出的 SOAP 消息进行签名,安全策略文件应该包含一个"),t("code",[e._v("Sign")]),e._v("元素。它还可以包含"),t("code",[e._v("SignatureTarget")]),e._v("元素,该元素指定预期要签名的目标消息部分和各种其他子元素。你还可以定义要使用的私钥别名、是否使用对称密钥而不是私钥以及许多其他属性。你可以找到可能的子元素"),t("a",{attrs:{href:"http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp565497",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("的引用。下面的示例包含一个"),t("code",[e._v("Sign")]),e._v("元素:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n\t\n\n')])])]),t("p",[t("code",[e._v("XwsSecurityInterceptor")]),e._v("向注册处理程序发送"),t("code",[e._v("SignatureKeyCallback")]),e._v("。在 Spring-WS 中,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("类处理这个特定的回调。")]),e._v(" "),t("h6",{attrs:{id:"使用keystorecallbackhandler-3"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用keystorecallbackhandler-3"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("KeyStoreCallbackHandler")])]),e._v(" "),t("p",[e._v("如"),t("a",{attrs:{href:"#security-key-store-callback-handler"}},[e._v("KeystoRecallBackHandler")]),e._v("中所述,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("使用"),t("code",[e._v("java.security.KeyStore")]),e._v("来处理各种加密回调,包括签名消息。对于添加签名,处理程序使用"),t("code",[e._v("keyStore")]),e._v("属性。此外,你必须设置"),t("code",[e._v("privateKeyPassword")]),e._v("属性来解锁用于签名的私钥。下面的示例使用"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n\n \n \n \n \n\n')])])]),t("h4",{attrs:{id:"_7-1-4-解密和加密"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-1-4-解密和加密"}},[e._v("#")]),e._v(" 7.1.4.解密和加密")]),e._v(" "),t("p",[e._v("加密时,将消息转换为只能使用适当密钥读取的窗体。可以对消息进行解密,以显示原始的可读消息。")]),e._v(" "),t("h5",{attrs:{id:"解密"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#解密"}},[e._v("#")]),e._v(" 解密")]),e._v(" "),t("p",[e._v("要解密传入的 SOAP 消息,安全策略文件应该包含"),t("code",[e._v("RequireEncryption")]),e._v("元素。该元素还可以携带一个"),t("code",[e._v("EncryptionTarget")]),e._v("元素,该元素指示消息的哪一部分应该加密,并携带一个"),t("code",[e._v("SymmetricKey")]),e._v(",指示应该使用共享秘密而不是常规私钥来解密消息。你可以读取对其他元素"),t("a",{attrs:{href:"http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp565951",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("的描述。下面的示例使用"),t("code",[e._v("RequireEncryption")]),e._v("元素:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n\n')])])]),t("p",[e._v("如果传入消息未加密,"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("将向发送方返回一个 SOAP ault。如果存在,它将向已注册的处理程序触发"),t("code",[e._v("DecryptionKeyCallback")]),e._v("。在 Spring-WS 中,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("类处理这个特定的回调。")]),e._v(" "),t("h6",{attrs:{id:"使用keystorecallbackhandler-4"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用keystorecallbackhandler-4"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("KeyStoreCallbackHandler")])]),e._v(" "),t("p",[e._v("如"),t("a",{attrs:{href:"#security-key-store-callback-handler"}},[e._v("KeystoRecallBackHandler")]),e._v("中所述,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("使用"),t("code",[e._v("java.security.KeyStore")]),e._v("来处理各种加密回调,包括解密。对于解密,处理程序使用"),t("code",[e._v("keyStore")]),e._v("属性。此外,你必须设置"),t("code",[e._v("privateKeyPassword")]),e._v("属性来解锁用于解密的私钥。对于基于对称密钥的解密,它使用"),t("code",[e._v("symmetricStore")]),e._v("。下面的示例使用"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n\n \n \n \n \n\n')])])]),t("h5",{attrs:{id:"加密"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#加密"}},[e._v("#")]),e._v(" 加密")]),e._v(" "),t("p",[e._v("要对传出的 SOAP 消息进行加密,安全策略文件应该包含一个"),t("code",[e._v("Encrypt")]),e._v("元素。该元素还可以携带一个"),t("code",[e._v("EncryptionTarget")]),e._v("元素,该元素指示消息的哪一部分应该加密,以及一个"),t("code",[e._v("SymmetricKey")]),e._v("表示应该使用共享秘密而不是常规公钥来加密消息。你可以读取对其他元素"),t("a",{attrs:{href:"http://java.sun.com/webservices/docs/1.6/tutorial/doc/XWS-SecurityIntro4.html#wp565951",target:"_blank",rel:"noopener noreferrer"}},[e._v("here"),t("OutboundLink")],1),e._v("的描述。下面的示例使用"),t("code",[e._v("Encrypt")]),e._v("元素:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n\n')])])]),t("p",[t("code",[e._v("XwsSecurityInterceptor")]),e._v("向注册处理程序发送"),t("code",[e._v("EncryptionKeyCallback")]),e._v(",以检索加密信息。在 Spring-WS 中,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("类处理这个特定的回调。")]),e._v(" "),t("h6",{attrs:{id:"使用keystorecallbackhandler-5"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用keystorecallbackhandler-5"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("KeyStoreCallbackHandler")])]),e._v(" "),t("p",[e._v("正如"),t("a",{attrs:{href:"#security-key-store-callback-handler"}},[e._v("KeystoRecallBackHandler")]),e._v("中所描述的,"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("使用"),t("code",[e._v("java.security.KeyStore")]),e._v("来处理各种加密回调,包括加密。对于基于公钥的加密,处理程序使用"),t("code",[e._v("trustStore")]),e._v("属性。对于基于对称密钥的加密,它使用"),t("code",[e._v("symmetricStore")]),e._v("。下面的示例使用"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n \n \n \n \n\n')])])]),t("h4",{attrs:{id:"_7-1-5-安全异常处理"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-1-5-安全异常处理"}},[e._v("#")]),e._v(" 7.1.5.安全异常处理")]),e._v(" "),t("p",[e._v("当一个 securement 或验证操作失败时,"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("分别抛出一个"),t("code",[e._v("WsSecuritySecurementException")]),e._v("或"),t("code",[e._v("WsSecurityValidationException")]),e._v("。这些异常绕过"),t("a",{attrs:{href:"#server-endpoint-exception-resolver"}},[e._v("标准异常处理机制")]),e._v(",但由拦截器本身处理。")]),e._v(" "),t("p",[t("code",[e._v("WsSecuritySecurementException")]),e._v("异常由"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("的"),t("code",[e._v("handleSecurementException")]),e._v("方法处理。默认情况下,此方法会记录错误并停止对消息的进一步处理。")]),e._v(" "),t("p",[e._v("类似地,"),t("code",[e._v("WsSecurityValidationException")]),e._v("异常由"),t("code",[e._v("handleValidationException")]),e._v("方法处理。默认情况下,该方法创建一个 SOAP1.1 客户机或 SOAP1.2Sender Fault,并将其作为响应发送回去。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[t("code",[e._v("handleSecurementException")]),e._v("和"),t("code",[e._v("handleValidationException")]),e._v("都是受保护的方法,你可以覆盖该方法以更改其默认行为。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h3",{attrs:{id:"_7-2-使用wss4jsecurityinterceptor"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-2-使用wss4jsecurityinterceptor"}},[e._v("#")]),e._v(" 7.2.使用"),t("code",[e._v("Wss4jSecurityInterceptor")])]),e._v(" "),t("p",[t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("是基于"),t("a",{attrs:{href:"https://ws.apache.org/wss4j/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Apache’s WSS4J"),t("OutboundLink")],1),e._v("的"),t("code",[e._v("EndpointInterceptor")]),e._v("(参见[拦截请求-"),t("code",[e._v("EndpointInterceptor")]),e._v("接口](# 服务器-端点-拦截器))。")]),e._v(" "),t("p",[e._v("WSS4J 实现了以下标准:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("OASIS Web 服务安全:SOAP 消息安全 1.0 标准 200401,2004 年 3 月。")])]),e._v(" "),t("li",[t("p",[e._v("用户名令牌配置文件 V1.0")])]),e._v(" "),t("li",[t("p",[e._v("X.509 令牌配置文件 V1.0")])])]),e._v(" "),t("p",[e._v("此拦截器支持由"),t("code",[e._v("AxiomSoapMessageFactory")]),e._v("和"),t("code",[e._v("SaajSoapMessageFactory")]),e._v("创建的消息。")]),e._v(" "),t("h4",{attrs:{id:"_7-2-1-配置wss4jsecurityinterceptor"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-2-1-配置wss4jsecurityinterceptor"}},[e._v("#")]),e._v(" 7.2.1.配置"),t("code",[e._v("Wss4jSecurityInterceptor")])]),e._v(" "),t("p",[e._v("WSS4J 不使用外部配置文件。拦截器完全由属性配置。该拦截器调用的验证和安全操作分别通过"),t("code",[e._v("validationActions")]),e._v("和"),t("code",[e._v("securementActions")]),e._v("属性指定。动作以空格分隔的字符串传递。下面的清单展示了一个配置示例:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n ...\n \n ...\n\n')])])]),t("p",[e._v("下表显示了可用的验证操作:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[e._v("Validation action")]),e._v(" "),t("th",[e._v("说明")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("code",[e._v("UsernameToken")])]),e._v(" "),t("td",[e._v("验证用户名令牌")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("Timestamp")])]),e._v(" "),t("td",[e._v("验证时间戳")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("Encrypt")])]),e._v(" "),t("td",[e._v("解密消息")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("Signature")])]),e._v(" "),t("td",[e._v("验证签名")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("NoSecurity")])]),e._v(" "),t("td",[e._v("未执行动作")])])])]),e._v(" "),t("p",[e._v("下表显示了可用的安全操作:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[e._v("Securement action")]),e._v(" "),t("th",[e._v("说明")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("code",[e._v("UsernameToken")])]),e._v(" "),t("td",[e._v("添加用户名令牌")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("UsernameTokenSignature")])]),e._v(" "),t("td",[e._v("添加用户名令牌和签名用户名令牌密钥")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("Timestamp")])]),e._v(" "),t("td",[e._v("添加时间戳")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("Encrypt")])]),e._v(" "),t("td",[e._v("对响应进行加密")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("Signature")])]),e._v(" "),t("td",[e._v("签署回应")])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("NoSecurity")])]),e._v(" "),t("td",[e._v("未执行动作")])])])]),e._v(" "),t("p",[e._v("行动的顺序是重要的,并由拦截器强制执行。如果其安全操作的执行顺序与"),t("code",[e._v("validationActions")]),e._v("指定的顺序不同,则拦截器将拒绝传入的 SOAP 消息。")]),e._v(" "),t("h4",{attrs:{id:"_7-2-2-处理数码证书"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-2-2-处理数码证书"}},[e._v("#")]),e._v(" 7.2.2.处理数码证书")]),e._v(" "),t("p",[e._v("对于需要与密钥库或证书处理交互的加密操作(签名、加密和解密操作),WSS4J 需要"),t("code",[e._v("org.apache.ws.security.components.crypto.Crypto")]),e._v("的实例。")]),e._v(" "),t("p",[t("code",[e._v("Crypto")]),e._v("实例可以从 wss4j 的"),t("code",[e._v("CryptoFactory")]),e._v("获得,或者更方便地使用 Spring-ws"),t("code",[e._v("CryptoFactoryBean")]),e._v("。")]),e._v(" "),t("h5",{attrs:{id:"cryptofactorybean"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#cryptofactorybean"}},[e._v("#")]),e._v(" CryptoFactoryBean")]),e._v(" "),t("p",[e._v("Spring-WS 提供了一种方便的工厂 Bean,"),t("code",[e._v("CryptoFactoryBean")]),e._v(",它通过强类型属性(首选)或通过"),t("code",[e._v("Properties")]),e._v("对象构造和配置"),t("code",[e._v("Crypto")]),e._v("实例。")]),e._v(" "),t("p",[e._v("默认情况下,"),t("code",[e._v("CryptoFactoryBean")]),e._v("返回"),t("code",[e._v("org.apache.ws.security.components.crypto.Merlin")]),e._v("的实例。你可以通过设置"),t("code",[e._v("cryptoProvider")]),e._v("属性(或其等效的"),t("code",[e._v("org.apache.ws.security.crypto.provider")]),e._v("字符串属性)来更改这一点。")]),e._v(" "),t("p",[e._v("以下示例配置使用"),t("code",[e._v("CryptoFactoryBean")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n\n')])])]),t("h4",{attrs:{id:"_7-2-3-认证"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-2-3-认证"}},[e._v("#")]),e._v(" 7.2.3.认证")]),e._v(" "),t("p",[e._v("本节讨论如何使用"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("进行身份验证。")]),e._v(" "),t("h5",{attrs:{id:"验证用户名令牌"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#验证用户名令牌"}},[e._v("#")]),e._v(" 验证用户名令牌")]),e._v(" "),t("p",[e._v("Spring-WS 提供了一组回调处理程序来与 Spring 安全性集成。此外,还提供了一个简单的回调处理程序"),t("code",[e._v("SimplePasswordValidationCallbackHandler")]),e._v(",用内存中的"),t("code",[e._v("Properties")]),e._v("对象配置用户和密码。")]),e._v(" "),t("p",[e._v("回调处理程序是通过"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("属性的"),t("code",[e._v("validationCallbackHandler")]),e._v("配置的。")]),e._v(" "),t("h6",{attrs:{id:"使用simplepasswordvalidationcallbackhandler-3"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用simplepasswordvalidationcallbackhandler-3"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("SimplePasswordValidationCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("SimplePasswordValidationCallbackHandler")]),e._v("针对内存中的"),t("code",[e._v("Properties")]),e._v("对象验证纯文本和摘要用户名令牌。你可以按以下方式配置它:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n Ernie\n \n \n\n')])])]),t("h6",{attrs:{id:"使用springsecuritypasswordvalidationcallbackhandler"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#使用springsecuritypasswordvalidationcallbackhandler"}},[e._v("#")]),e._v(" 使用"),t("code",[e._v("SpringSecurityPasswordValidationCallbackHandler")])]),e._v(" "),t("p",[t("code",[e._v("SpringSecurityPasswordValidationCallbackHandler")]),e._v("通过使用 Spring 安全性"),t("code",[e._v("UserDetailService")]),e._v("来操作,从而验证纯文本和摘要密码。它使用此服务检索令牌中指定的用户的密码(或密码摘要)。然后将这个 Details 对象中包含的密码(或密码摘要)与消息中的摘要进行比较。如果它们相等,则用户已成功地进行了身份验证,并且"),t("code",[e._v("UsernamePasswordAuthenticationToken")]),e._v("存储在"),t("code",[e._v("SecurityContextHolder")]),e._v("中。你可以使用"),t("code",[e._v("userDetailsService")]),e._v("设置服务。此外,你可以设置"),t("code",[e._v("userCache")]),e._v("属性,以缓存已加载的用户详细信息,如下所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n \n ...\n\n')])])]),t("h5",{attrs:{id:"添加用户名令牌"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#添加用户名令牌"}},[e._v("#")]),e._v(" 添加用户名令牌")]),e._v(" "),t("p",[e._v("向传出消息添加用户名令牌就像在"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("的"),t("code",[e._v("securementActions")]),e._v("属性中添加"),t("code",[e._v("UsernameToken")]),e._v("并指定"),t("code",[e._v("securementUsername")]),e._v("和"),t("code",[e._v("securementPassword")]),e._v("一样简单。")]),e._v(" "),t("p",[e._v("可以通过设置"),t("code",[e._v("securementPasswordType")]),e._v("属性来设置密码类型。对于纯文本密码,可能的值是"),t("code",[e._v("PasswordText")]),e._v(",对于摘要密码,可能的值是"),t("code",[e._v("PasswordDigest")]),e._v(",这是默认值。")]),e._v(" "),t("p",[e._v("下面的示例生成带有摘要密码的用户名令牌:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n')])])]),t("p",[e._v("如果选择了纯文本密码类型,则可以通过设置"),t("code",[e._v("securementUsernameTokenElements")]),e._v("属性来指示拦截器添加"),t("code",[e._v("Nonce")]),e._v("和"),t("code",[e._v("Created")]),e._v("元素。该值必须是一个列表,该列表包含由空格分隔的所需元素的名称(区分大小写)。")]),e._v(" "),t("p",[e._v("下面的示例生成一个带有纯文本密码、"),t("code",[e._v("Nonce")]),e._v("和"),t("code",[e._v("Created")]),e._v("元素的用户名令牌:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n\n')])])]),t("h5",{attrs:{id:"证书认证-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#证书认证-2"}},[e._v("#")]),e._v(" 证书认证")]),e._v(" "),t("p",[e._v("由于证书认证类似于数字签名,WSS4J 将其作为签名验证和安全的一部分来处理。具体地说,"),t("code",[e._v("securementSignatureKeyIdentifier")]),e._v("属性必须设置为"),t("code",[e._v("DirectReference")]),e._v(",以便指示 WSS4J 生成包含 X509 证书的"),t("code",[e._v("BinarySecurityToken")]),e._v("元素,并将其包含在传出消息中。证书的名称和密码分别通过"),t("code",[e._v("securementUsername")]),e._v("和"),t("code",[e._v("securementPassword")]),e._v("属性传递,如下例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n\n')])])]),t("p",[e._v("对于证书验证,应用常规签名验证:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n\n')])])]),t("p",[e._v("在验证结束时,拦截器将委托给默认的 WSS4J 实现,从而自动验证证书的有效性。如果需要,可以通过重新定义"),t("code",[e._v("verifyCertificateTrust")]),e._v("方法来更改此行为。")]),e._v(" "),t("p",[e._v("有关更多详细信息,请参见"),t("a",{attrs:{href:"#security-wss4j-digital-signatures"}},[e._v("数字签名")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_7-2-4-安全时间戳"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-2-4-安全时间戳"}},[e._v("#")]),e._v(" 7.2.4.安全时间戳")]),e._v(" "),t("p",[e._v("本节描述"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("中可用的各种时间戳选项。")]),e._v(" "),t("h5",{attrs:{id:"验证时间戳"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#验证时间戳"}},[e._v("#")]),e._v(" 验证时间戳")]),e._v(" "),t("p",[e._v("要验证时间戳,将"),t("code",[e._v("Timestamp")]),e._v("添加到"),t("code",[e._v("validationActions")]),e._v("属性。通过将"),t("code",[e._v("timestampStrict")]),e._v("设置为"),t("code",[e._v("true")]),e._v(",并通过设置"),t("code",[e._v("timeToLive")]),e._v("属性,指定服务器端的实时(默认:300),可以覆盖由 SOAP 消息的发起者指定的时间戳语义。拦截器总是拒绝已经过期的时间戳,无论"),t("code",[e._v("timeToLive")]),e._v("的值是多少。")]),e._v(" "),t("p",[e._v("在下面的示例中,拦截器将时间戳有效性窗口限制为 10 秒,拒绝该窗口之外的任何有效时间戳令牌:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n\n')])])]),t("h5",{attrs:{id:"添加时间戳"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#添加时间戳"}},[e._v("#")]),e._v(" 添加时间戳")]),e._v(" "),t("p",[e._v("将"),t("code",[e._v("Timestamp")]),e._v("添加到"),t("code",[e._v("securementActions")]),e._v("属性会在传出消息中生成一个时间戳头。"),t("code",[e._v("timestampPrecisionInMilliseconds")]),e._v("属性指定生成的时间戳的精度是否以毫秒为单位。默认值是"),t("code",[e._v("true")]),e._v("。以下清单添加了一个时间戳:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n\n')])])]),t("h4",{attrs:{id:"_7-2-5-数字签名"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-2-5-数字签名"}},[e._v("#")]),e._v(" 7.2.5.数字签名")]),e._v(" "),t("p",[e._v("本节介绍"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("中可用的各种签名选项。")]),e._v(" "),t("h5",{attrs:{id:"验证签名-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#验证签名-2"}},[e._v("#")]),e._v(" 验证签名")]),e._v(" "),t("p",[e._v("要指示"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v(","),t("code",[e._v("validationActions")]),e._v("必须包含"),t("code",[e._v("Signature")]),e._v("操作。此外,"),t("code",[e._v("validationSignatureCrypto")]),e._v("属性必须指向包含发起者的公共证书的密钥库:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n\n')])])]),t("h5",{attrs:{id:"签名消息-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#签名消息-2"}},[e._v("#")]),e._v(" 签名消息")]),e._v(" "),t("p",[e._v("通过将"),t("code",[e._v("Signature")]),e._v("动作添加到"),t("code",[e._v("securementActions")]),e._v("中,可以对发出的消息进行签名。要使用的私钥的别名和密码分别由"),t("code",[e._v("securementUsername")]),e._v("和"),t("code",[e._v("securementPassword")]),e._v("属性指定。"),t("code",[e._v("securementSignatureCrypto")]),e._v("必须指向包含私钥的密钥库:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n\n')])])]),t("p",[e._v("此外,你可以通过设置"),t("code",[e._v("securementSignatureAlgorithm")]),e._v("属性来定义签名算法。")]),e._v(" "),t("p",[e._v("可以通过设置"),t("code",[e._v("securementSignatureKeyIdentifier")]),e._v("属性来定制要使用的键标识符类型。只有"),t("code",[e._v("IssuerSerial")]),e._v("和"),t("code",[e._v("DirectReference")]),e._v("对签名有效。")]),e._v(" "),t("p",[t("code",[e._v("securementSignatureParts")]),e._v("属性控制消息的哪一部分已签名。此属性的值是用分号分隔的元素名称的列表,这些名称标识要签名的元素。签名部分的一般形式是"),t("code",[e._v("{}{namespace}Element")]),e._v("。请注意,第一个空括号仅用于加密部分。默认的行为是对 SOAP 主体进行签名。")]),e._v(" "),t("p",[e._v("下面的示例展示了如何在 Spring Web 服务 echo 示例中对"),t("code",[e._v("echoResponse")]),e._v("元素进行签名:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n')])])]),t("p",[e._v("要指定没有名称空间的元素,请使用字符串"),t("code",[e._v("Null")]),e._v("(区分大小写)作为名称空间名称。")]),e._v(" "),t("p",[e._v("如果请求中没有其他元素的本地名称"),t("code",[e._v("Body")]),e._v(",则 SOAP 名称空间标识符可以为空("),t("code",[e._v("{}")]),e._v(")。")]),e._v(" "),t("h5",{attrs:{id:"签名确认"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#签名确认"}},[e._v("#")]),e._v(" 签名确认")]),e._v(" "),t("p",[e._v("通过将"),t("code",[e._v("enableSignatureConfirmation")]),e._v("设置为"),t("code",[e._v("true")]),e._v(",可以启用签名确认。请注意,签名确认操作跨越了请求和响应。这意味着,即使没有相应的安全操作,"),t("code",[e._v("secureResponse")]),e._v("和"),t("code",[e._v("validateRequest")]),e._v("也必须设置为"),t("code",[e._v("true")]),e._v("(这是默认值)。下面的示例将"),t("code",[e._v("enableSignatureConfirmation")]),e._v("属性设置为"),t("code",[e._v("true")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n\n')])])]),t("h4",{attrs:{id:"_7-2-6-解密和加密"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-2-6-解密和加密"}},[e._v("#")]),e._v(" 7.2.6.解密和加密")]),e._v(" "),t("p",[e._v("本节介绍"),t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("中可用的各种解密和加密选项。")]),e._v(" "),t("h5",{attrs:{id:"解密-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#解密-2"}},[e._v("#")]),e._v(" 解密")]),e._v(" "),t("p",[e._v("解密传入的 SOAP 消息需要将"),t("code",[e._v("Encrypt")]),e._v("动作添加到"),t("code",[e._v("validationActions")]),e._v("属性中。配置的其余部分取决于消息中出现的关键信息。(这是因为 WSS4J 只需要对加密的密钥进行加密,而嵌入的密钥名称验证则委托给回调处理程序。")]),e._v(" "),t("p",[e._v("要使用嵌入式加密对称密钥("),t("code",[e._v("xenc:EncryptedKey")]),e._v("元素)解密消息,"),t("code",[e._v("validationDecryptionCrypto")]),e._v("需要指向包含解密私钥的密钥库。此外,"),t("code",[e._v("validationCallbackHandler")]),e._v("必须注入一个"),t("code",[e._v("org.springframework.ws.soap.security.wss4j.callback.KeyStoreCallbackHandler")]),e._v(",该密码指定了密钥的密码:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n \n\n')])])]),t("p",[e._v("为了支持使用嵌入式密钥名称("),t("code",[e._v("ds:KeyName")]),e._v("元素)对消息进行解密,你可以配置一个"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v(",它使用对称密钥指向密钥库。"),t("code",[e._v("symmetricKeyPassword")]),e._v("属性指示密钥的密码,密钥名称是由"),t("code",[e._v("ds:KeyName")]),e._v("元素指定的:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n \n \n\n')])])]),t("h5",{attrs:{id:"加密-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#加密-2"}},[e._v("#")]),e._v(" 加密")]),e._v(" "),t("p",[e._v("将"),t("code",[e._v("Encrypt")]),e._v("添加到"),t("code",[e._v("securementActions")]),e._v("中,可以对传出的消息进行加密。通过设置"),t("code",[e._v("securementEncryptionUser")]),e._v("属性,你可以设置用于加密的证书的别名。通过"),t("code",[e._v("securementEncryptionCrypto")]),e._v("属性访问证书所在的密钥存储库。由于加密依赖于公共证书,因此不需要传递密码。下面的示例使用"),t("code",[e._v("securementEncryptionCrypto")]),e._v("属性:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n\n')])])]),t("p",[e._v("你可以通过几种方式定制加密:要使用的密钥标识符类型由"),t("code",[e._v("securementEncryptionKeyIdentifier")]),e._v("属性定义。可能的值是"),t("code",[e._v("IssuerSerial")]),e._v(","),t("code",[e._v("X509KeyIdentifier")]),e._v(","),t("code",[e._v("DirectReference")]),e._v(","),t("code",[e._v("Thumbprint")]),e._v(","),t("code",[e._v("SKIKeyIdentifier")]),e._v(",和"),t("code",[e._v("EmbeddedKeyName")]),e._v("。")]),e._v(" "),t("p",[e._v("如果选择"),t("code",[e._v("EmbeddedKeyName")]),e._v("类型,则需要指定用于加密的密钥。与其他密钥标识符类型一样,密钥的别名在"),t("code",[e._v("securementEncryptionUser")]),e._v("属性中设置。但是,WSS4J 需要一个回调处理程序来获取秘钥。因此,你必须为"),t("code",[e._v("securementCallbackHandler")]),e._v("提供一个指向适当密钥存储库的"),t("code",[e._v("KeyStoreCallbackHandler")]),e._v("。默认情况下,生成的 WS-Security 头中的"),t("code",[e._v("ds:KeyName")]),e._v("元素接受"),t("code",[e._v("securementEncryptionUser")]),e._v("属性的值。要表示不同的名称,可以使用所需的值设置"),t("code",[e._v("securementEncryptionEmbeddedKeyName")]),e._v("。在下一个示例中,传出消息是用别名"),t("code",[e._v("secretKey")]),e._v("的密钥加密的,而"),t("code",[e._v("myKey")]),e._v("出现在"),t("code",[e._v("ds:KeyName")]),e._v("元素中:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n')])])]),t("p",[t("code",[e._v("securementEncryptionKeyTransportAlgorithm")]),e._v("属性定义了使用哪种算法来加密生成的对称密钥。支持的值是"),t("code",[e._v("[http://www.w3.org/2001/04/xmlenc#rsa-1_5](https://www.w3.org/2001/04/xmlenc#rsa-1_5)")]),e._v("(这是默认值)和"),t("code",[e._v("[http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p](https://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p)")]),e._v("。")]),e._v(" "),t("p",[e._v("你可以通过设置"),t("code",[e._v("securementEncryptionSymAlgorithm")]),e._v("属性来设置要使用的对称加密算法。支持的值是"),t("code",[e._v("[http://www.w3.org/2001/04/xmlenc#aes128-cbc](https://www.w3.org/2001/04/xmlenc#aes128-cbc)")]),e._v("(默认)、"),t("code",[e._v("[http://www.w3.org/2001/04/xmlenc#tripledes-cbc](https://www.w3.org/2001/04/xmlenc#tripledes-cbc)")]),e._v("、"),t("code",[e._v("[http://www.w3.org/2001/04/xmlenc#aes256-cbc](https://www.w3.org/2001/04/xmlenc#aes256-cbc)")]),e._v("和"),t("code",[e._v("[http://www.w3.org/2001/04/xmlenc#aes192-cbc](https://www.w3.org/2001/04/xmlenc#aes192-cbc)")]),e._v("。")]),e._v(" "),t("p",[e._v("最后,"),t("code",[e._v("securementEncryptionParts")]),e._v("属性定义了消息的哪些部分是加密的。此属性的值是用分号分隔的元素名称的列表,这些名称标识要加密的元素。一个加密模式说明符和一个命名空间标识(分别位于一对花括号内)可以放在每个元素名称之前。加密模式说明符是"),t("code",[e._v("{Content}")]),e._v("或"),t("code",[e._v("{Element}")]),e._v("参见 W3CXML 加密规范中关于元素和内容加密的区别。下面的示例从 echo 样本中标识"),t("code",[e._v("echoResponse")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('\n')])])]),t("p",[e._v("请注意,元素名、名称空间标识符和加密修饰符是区分大小写的。你可以省略加密修饰符和名称空间标识符。如果你这样做,那么加密模式默认为"),t("code",[e._v("Content")]),e._v(",并且名称空间被设置为 SOAP 名称空间。")]),e._v(" "),t("p",[e._v("要指定没有名称空间的元素,请使用值"),t("code",[e._v("Null")]),e._v("(区分大小写)作为名称空间名称。如果没有指定列表,那么处理程序将默认以"),t("code",[e._v("Content")]),e._v("模式加密 SOAP 主体。")]),e._v(" "),t("h4",{attrs:{id:"_7-2-7-安全异常处理"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-2-7-安全异常处理"}},[e._v("#")]),e._v(" 7.2.7.安全异常处理")]),e._v(" "),t("p",[t("code",[e._v("Wss4jSecurityInterceptor")]),e._v("的异常处理与"),t("code",[e._v("XwsSecurityInterceptor")]),e._v("的异常处理相同。有关更多信息,请参见"),t("a",{attrs:{href:"#security-xws-exception-handling"}},[e._v("安全异常处理")]),e._v("。")]),e._v(" "),t("h1",{attrs:{id:"iii-其他资源"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#iii-其他资源"}},[e._v("#")]),e._v(" iii.其他资源")]),e._v(" "),t("p",[e._v("除了这个参考文档之外,还有许多其他资源可以帮助你了解如何使用 Spring Web 服务。本节列举了这些额外的第三方资源。")]),e._v(" "),t("h2",{attrs:{id:"书目"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#书目"}},[e._v("#")]),e._v(" 书目")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("[Waldo-94]Jim Waldo,Ann Wollrath 和 Sam Kendall。"),t("em",[e._v("关于分布式计算的一个注记")]),e._v("。Springer Verlag。1994")])]),e._v(" "),t("li",[t("p",[e._v("[高山]Steve Loughran&Edmund Smith。"),t("em",[e._v("重新思考 Java SOAP 堆栈")]),e._v("。2005 年 5 月 17 日。2005 年 IEEE Telephone Laboratories,Inc.")])]),e._v(" "),t("li",[t("p",[e._v("[effective-Enterprise-java]Ted Neward。Scott Meyers。"),t("em",[e._v("有效的 EnterpriseJava")]),e._v("。Addison-Wesley。2004")])]),e._v(" "),t("li",[t("p",[e._v("[Effective-xml]Elliotte Rusty Harold。Scott Meyers。"),t("em",[e._v("有效 XML")]),e._v("。Addison-Wesley。2004")])])])])}),[],!1,null,null,null);a.default=s.exports}}]);