本篇文章分析了 SOAP-to-REST 的多种实现方式,并介绍如何使用 APISIX 做零代码代理。
作者罗锦华,API7.ai 技术专家/技术工程师,开源项目 pgcat,lua-resty-ffi,lua-resty-inspect 的作者。
1. 什么是 Web Service
Web Service 由万维网联盟 (W3C) 定义为一种软件系统,旨在支持通过网络进行可互操作的计算机间交互。
Web Service 完成特定任务或任务集,并且由名称为 Web Service 描述语言 (WSDL) 的标准 XML 表示法中的服务描述进行描述。服务描述提供了与服务交互必需的所有详细信息,包括消息格式(用于详细说明操作)、传输协议和位置。
其他系统使用 SOAP 消息与 Web Service 进行交互,通常是通过将 HTTP 与 XML 序列化和其他 Web 相关标准一起使用。
Web Service 的架构图(注意现实中 Service broker 是可选的):
图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikipedia.org/wiki/Web_service
WSDL 接口隐藏服务实现方式的详细信息,这样服务的使用便独立于实现服务的硬件或软件平台,以及编写服务所使用的编程语言。
基于 Web Service 的应用程序是松耦合、面向组件和跨技术的实现。 Web Service 可以单独使用,也可以与其他 Web Service 一起用于执行复杂的聚集或业务事务。
Web Service 是 Service-oriented architecture (SOA) 的实现单元,SOA 是用来替换单体系统的一种设计方法,也就是说,一个庞大的系统可以拆分为多个 Web Service,然后组合起来对外作为一个大的黑盒提供业务逻辑。流行的基于容器的微服务就是 Web Service 最新替代品,但是很多旧系统都已经基于 Web Service 来实现和运作,所以虽然技术日新月异,兼容这些系统也是一个刚性需求。
WSDL (Web Services Description Language)
WSDL 是用于描述 Web Service 的一种 XML 表示法。 WSDL 定义告诉客户如何编写 Web Service 请求,并且描述了由 Web Service 提供程序提供的接口。
WSDL 定义划分为多个单独部分,分别指定 Web Service 的逻辑接口和物理详细信息。物理详细信息既包括诸如 HTTP 端口号等端点信息,还包括指定如何表示 SOAP 有效内容和使用哪种传输方法的绑定信息。
图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikipedia.org/wiki/Web_Services_Description_Language
- 一个 WSDL 文件可以包含多个 service
- 一个 service 可以包含多个 port
- 一个 port 定义了 URL 地址(每个 port 都可能不同),可以包含多个 operation
- 每个 operation 包含 input type 和 output type
- type 定义了消息结构:消息由哪些字段组成,每个字段的类型(可嵌套),以及字段个数约束
1.1 什么是 SOAP
SOAP 是在 Web Service 交互中使用的 XML 消息格式。 SOAP 消息通常通过 HTTP 或 JMS 发送,但也可以使用其他传输协议。 WSDL 定义描述了特定 Web Service 中的 SOAP 使用。
常用的 SOAP 有两个版本:SOAP 1.1 和 SOAP 1.2。
图片来源(遵循 CC 3.0 BY-SA 版权协议): https://en.wikipedia.org/wiki/SOAP
SOAP 消息包含以下部分:
- Header 元信息,一般为空
- Body
- WSDL 里面定义的消息类型
- 对于响应类型,除了成功响应,还有错误消息,它也是结构化的
例子:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header></SOAP-ENV:Header>
<SOAP-ENV:Body>
<ns2:getCountryResponse xmlns:ns2="http://spring.io/guides/gs-producing-web-service">
<ns2:country>
<ns2:name>Spain</ns2:name>
<ns2:population>46704314</ns2:population>
<ns2:capital>Madrid</ns2:capital>
<ns2:currency>EUR</ns2:currency>
</ns2:country>
</ns2:getCountryResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
1.2 什么是 REST
Web Service 其实是一种抽象概念,本身可以有任何实现,例如 REST 就是一种流行实现方式。
REST,即 Representational State Transfer 的缩写,直译就是表现层状态转化。 REST 这个词,是 Roy Thomas Fielding 在他 2000 年的博士论文中提出的。当时候正是互联网蓬勃发展的时期,软件开发和网络之间的交互需要一个实用的定义。
长期以来,软件研究主要关注软件设计的分类、设计方法的演化,很少客观地评估不同的设计选择对系统行为的影响。而相反地,网络研究主要关注系统之间通信行为的细节、如何改进特定通信机制的表现,常常忽视了一个事实,那就是改变应用程序的互动风格比改变互动协议,对整体表现有更大的影响。我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。
访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。HTTP 协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”。而这种转化是建立在表现层之上的,所以就是“表现层状态转化”。
REST 四个基本原则:
- 使用 HTTP 动词:GET POST PUT DELETE;
- 无状态连接,服务器端不应保存过多上下文状态,即每个请求都是独立的;
- 为每个资源设置 URI;
- 通过
x-www-form-urlencoded
或者 JSON 作为数据格式;
将 SOAP 转换为 REST,可以方便用户用 RESTFul 的方式访问传统的 Web Service,降低 SOAP client 的开发成本,如果能动态适配任何 Web Service,零代码开发,那就更完美了。
REST 最大的好处是没有 schema,开发方便,而且 JSON 的可读性更高,冗余度更低。
2. SOAP-to-REST 代理的传统实现
2.1 手工模板转换
这种方式需要为 Web Service 的每个 operation 提供 request 和 response 的转换模板,这也是很多网关产品使用的方式。
我们可以使用 APISIX 的 body transformer plugin 来做简单的 SOAP-to-REST 代理,实践一下这种方式。
作为例子,我们对上述 WSDL 文件里面的 CountriesPortService
的 getCountry
操作,根据类型定义构造 XML 格式的请求模板。
这里我们将 JSON 里面的 name 字段填写到 getCountryRequest 里面的 name 字段。
req_template=$(cat <<EOF | awk '{gsub(/"/,"\\\"");};1' | awk '{$1=$1};1' | tr -d '\r\n'
<?xml version="1.0"?>
<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
<soap-env:Body>
<ns0:getCountryRequest xmlns:ns0="http://spring.io/guides/gs-producing-web-service">
<ns0:name>{{_escape_xml(name)}}</ns0:name>
</ns0:getCountryRequest>
</soap-env:Body>
</soap-env:Envelope>
EOF
)
对于响应,就要提供 XML-to-JSON 模板,稍微复杂(如果要考虑 SOAP 版本间 fault 的差异,那就更复杂了),因为需要判断是否成功响应:
- 成功响应,直接将字段一一对应填入 JSON
- 失败响应,也就是 fault,我们需要另外的 JSON 结构,并且判断一些可选字段是否存在
rsp_template=$(cat <<EOF | awk '{gsub(/"/,"\\\"");};1' | awk '{$1=$1};1' | tr -d '\r\n'
{% if Envelope.Body.Fault == nil then %}
{
"currency":"{{Envelope.Body.getCountryResponse.country.currency}}",
"population":{{Envelope.Body.getCountryResponse.country.population}},
"capital":"{{Envelope.Body.getCountryResponse.country.capital}}",
"name":"{{Envelope.Body.getCountryResponse.country.name}}"
}
{% else %}
{
"message":{*_escape_json(Envelope.Body.Fault.faultstring[1])*},
"code":"{{Envelope.Body.Fault.faultcode}}"
{% if Envelope.Body.Fault.faultactor ~= nil then %}
, "actor":"{{Envelope.Body.Fault.faultactor}}"
{% end %}
}
{% end %}
EOF
)
配置 APISIX 路由并且做测试:
curl http://127.0.0.1:9180/apisix/admin/routes/1 \
-H 'X-API-KEY: xxx' -X PUT -d '
{
"methods": ["POST"],
"uri": "/ws/getCountry",
"plugins": {
"body-transformer": {
"request": {
"template": "'"$req_template"'"
},
"response": {
"template": "'"$rsp_template"'"
}
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"localhost:8080": 1
}
}
}'
curl -s http://127.0.0.1:9080/ws/getCountry \
-H 'content-type: application/json' \
-X POST -d '{"name": "Spain"}' | jq
{
"currency": "EUR",
"population": 46704314,
"capital": "Madrid",
"name": "Spain"
}
# Fault response
{
"message": "Your name is required.",
"code": "SOAP-ENV:Server"
}
可见,这种方式需要人工去读懂 WSDL 文件里面每一个操作的定义,并且也要搞清楚每个操作对应的 web service 地址。如果 WSDL 文件庞大,包含大量操作和复杂的嵌套类型定义,那么这种做法是很麻烦的,调试困难,容易出错。
2.2 Apache Camel
Camel 是一个著名的 Java 整合框架,用于实现对不同协议和业务逻辑相互转换的路由管道,SOAP-to-REST 只是它的其中一个用途。
使用 Camel 需要下载并导入 WSDL 文件,生成 SOAP client 的 stub 代码,使用 Java 编写代码:
- 定义 REST endpoint
- 定义协议转换路由,例如 JSON 字段如何映射到 SOAP 字段
我们以温度单位转换的 Web Service 为例:
https://apps.learnwebservices.com/services/tempconverter?wsdl
- 通过 maven 根据 WSDL 文件生成 SOAP client 的代码
cxf-codegen-plugin
会为我们生成 SOAP client endpoint,用于访问 Web Service。
<build>
<plugins>
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<wsdlOptions>
<wsdlOption>
<wsdl>src/main/resources/TempConverter.wsdl</wsdl>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
- 编写 SOAP client bean
注意这里我们记住 bean 的名字是 cxfConvertTemp
,后面定义 Camel 路由用到。
import com.learnwebservices.services.tempconverter.TempConverterEndpoint;
@Configuration
public class CxfBeans {
@Value("${endpoint.wsdl}")
private String SOAP_URL;
@Bean(name = "cxfConvertTemp")
public CxfEndpoint buildCxfEndpoint() {
CxfEndpoint cxf = new CxfEndpoint();
cxf.setAddress(SOAP_URL);
cxf.setServiceClass(TempConverterEndpoint.class);
return cxf;
}
}
- 先编写下游 REST 的路由
从这个路由我们可以看到它定义了 RESTFul 风格的 URL 及其参数定义,并且定义了每个 URL 的下一跳路由。例如/convert/celsius/to/fahrenheit/{num}
,将 URL 里面最后一个部分作为参数(double 类型)提供给下一跳路由direct:celsius-to-fahrenheit
。
rest("/convert")
.get("/celsius/to/fahrenheit/{num}")
.consumes("text/plain").produces("text/plain")
.description("Convert a temperature in Celsius to Fahrenheit")
.param().name("num").type(RestParamType.path).description("Temperature in Celsius").dataType("int").endParam()
.to("direct:celsius-to-fahrenheit");
- 最后编写上游 SOAP 路由及上下游的转换
from("direct:celsius-to-fahrenheit")
.removeHeaders("CamelHttp*")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
// 初始化 SOAP 请求
// 将下游参数 num 填写到 body,body 就是一个简单的 double 类型
CelsiusToFahrenheitRequest c = new CelsiusToFahrenheitRequest();
c.setTemperatureInCelsius(Double.valueOf(exchange.getIn().getHeader("num").toString()));
exchange.getIn().setBody(c);
}
})
// 指定 SOAP operation 和 namespace
// 在 application.properties 文件定义
.setHeader(CxfConstants.OPERATION_NAME, constant("{{endpoint.operation.celsius.to.fahrenheit}}"))
.setHeader(CxfConstants.OPERATION_NAMESPACE, constant("{{endpoint.namespace}}"))
// 交给 WSDL 生成的 SOAP client bean 去发包
.to("cxf:bean:cxfConvertTemp")
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
// 处理 SOAP 响应
// 将 body,也就是 double 类型的值填充到字符串里面去
// 将字符串返回给下游
MessageContentsList response = (MessageContentsList) exchange.getIn().getBody();
CelsiusToFahrenheitResponse r = (CelsiusToFahrenheitResponse) response.get(0);
exchange.getIn().setBody("Temp in Farenheit: " + r.getTemperatureInFahrenheit());
}
})
.to("mock:output");
- 测试
curl localhost:9090/convert/celsius/to/fahrenheit/50
Temp in Farenheit: 122.0
可见,通过 Camel 做 SOAP-to-REST,就要针对所有 operation 用 Java 代码定义路由和转换逻辑,需要开发成本。
同理,如果 WSDL 包含很多 service 和 operation,那么走 Camel 这种方式来做代理,也是比较痛苦的。
2.3 结论
我们总结一下传统方式的弊端。
模板 | Camel | |
---|---|---|
WSDL | 人工解析 | 通过 maven 去生成代码 |
上游 | 人工解析 | 自动转换 |
定义 body | 提供模板做判断和转换 | 编写转换代码 |
获取参数 | nginx 变量 | 在代码里面自定义或者调用 SOAP client 接口获取 |
这两种方式都有开发成本,并且对每一个新的 Web Service,都需要重复这个开发成本。
开发成本与 Web Service 的复杂度成正比。
3. APISIX 的 SOAP-to-REST 代理
传统的代理方式,要不提供转换模板,要不编写转换代码,都需要用户深度分析 WSDL 文件,有不可忽视的开发成本。
APISIX 提供了一种自动化的方式,自动分析 WSDL 文件,自动为每个操作提供转换逻辑,为用户消除开发成本。
3.1 无代码自动转换
使用 APISIX SOAP 代理:
- 无需手工解析或导入 WSDL 文件
- 无需定义转换模板
- 无需编写任何转换或耦合代码。
用户只需要配置 WSDL 的 URL,APISIX 会自动做转换,它适用于任何 Web Service,是通用程序,无需再针对特定需求做二次开发。
3.2 动态配置
- WSDL URL 可绑定在任何路由,和其他 APISIX 资源对象一样,可在运行时更新配置,配置更改是动态生效的,无需重启 APISIX。
- WSDL 文件里面包含的 service URL(可能有多个 URL),也就是上游地址,会被自动识别并且用作 SOAP 上游,无需用户去解析并配置。
3.3 实现机制
- 从 WSDL URL 获取 WSDL 文件内容,分析后自动生成 proxy 对象
- proxy 对象负责协议转换
- 根据 JSON 输入生成合规的 SOAP XML 请求
- 将 SOAP XML 响应转换为 JSON 响应
- 访问 Web Service,自动处理 SOAP 协议细节,例如 Fault 类型的响应
- 支持 SOAP1.1和 SOAP1.2,以及若干扩展特性,例如 WS-Addressing
3.4 配置示例
SOAP 插件的配置参数说明:
参数 | 必选? | 说明 |
---|---|---|
wsdl_url |
是 | WSDL URL,例如 https://apps.learnwebservices.com/services/tempconverter?wsdl
|
operation |
否 | 操作名,可来自任何 nginx 变量,例如$arg_operation 或者$http_soap_operation
|
service |
否 | 服务名,如果一个 WSDL 文件包含多个服务,可通过这个参数来指定访问哪个服务 |
ca_cert |
否 | 校验服务端证书的 CA 证书内容 |
client_cert |
否 | 用于 MTLS 的 client 证书内容 |
client_key |
否 | 用于 MTLS 的 client 私钥内容 |
测试:
# 配置 APISIX 路由,使用 SOAP 插件
# 注意这里一条路由能执行所有操作,用 URL 参数来指定操作名
# 这也体现了动态代理的好处,不需要再手工去分析 WSDL 里面每一个操作
curl http://127.0.0.1:9180/apisix/admin/routes/1 \
-H 'X-API-KEY: xxx' -X PUT -d '
{
"methods": ["POST"],
"uri": "/ws",
"plugins": {
"soap": {
"wsdl_url": "http://localhost:8080/ws/countries.wsdl",
"operation": "$arg_operation",
"service": "<use alternative service defined in wsdl if exist>",
"ca_cert": "<ca cert file content>",
"client_cert":"<client cert file content>",
"client_key":"<client key file content>"
}
}
}'
curl 'http://127.0.0.1:9080/ws?operation=getCountry' \
-X POST -d '{"name": "Spain"}'
# 成功响应
HTTP/1.1 200 OK
Date: Tue, 06 Dec 2022 08:07:48 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.99.0
{"currency":"EUR","population":46704314,"capital":"Madrid","name":"Spain"}
# 失败响应
HTTP/1.1 502 Bad Gateway
Date: Tue, 03 Jan 2023 13:43:33 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.99.0
{"message":"Your name is required.","actor":null,"code":"SOAP-ENV:Server","subcodes":null,"detail":null}
4. 结论
Web Service 发展至今,有大量企业用户使用传统的 SOAP based Web Service 提供服务,这些服务由于历史原因和成本考虑,不适合做 RESTFul 的完全重构,所以 SOAP-to-REST 对不少企业用户有刚性需求。
APISIX 提供的 SOAP-to-REST 插件,能实现零代码的代理功能,可动态配置,无需二次开发,有利于企业用户的零成本业务迁移和整合。
关于 API7.ai 与 APISIX
API7.ai 是一家提供 API 处理和分析的开源基础软件公司,于 2019 年开源了新一代云原生 API 网关 -- APISIX 并捐赠给 Apache 软件基金会。此后,API7.ai 一直积极投入支持 Apache APISIX 的开发、维护和社区运营。与千万贡献者、使用者、支持者一起做出世界级的开源项目,是 API7.ai 努力的目标。