本篇文章剖析了 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
https://camel.apache.org/
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 致力的指标。