無需二次開發,SOAP-to-REST 簡化企業用户的業務遷移和整合
本篇文章分析了 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 努力的目標。
- 什麼是 LuaJIT?為什麼 Apache APISIX 選擇了 LuaJIT?
- 為什麼 APISIX Ingress 是比 Emissary-ingress 更好的選擇?
- 無需二次開發,SOAP-to-REST 簡化企業用户的業務遷移和整合
- API 網關日誌的價值,你瞭解多少?
- API Gateway vs Load Balancer:選擇適合你的網絡流量管理組件
- 從 1 秒到 10 毫秒!在 APISIX 中減少 Prometheus 請求阻塞
- 微服務為什麼要用到 API 網關?
- 備戰一年半,我們讓最火的開源網關上了雲
- APISIX 是怎麼保護用户的敏感數據不被泄露的?
- 如何使用 Kubernetes 實現應用程序的彈性伸縮
- 詳解 APISIX Lua 動態調試插件 inspect
- 藉助 APISIX Ingress,實現與註冊中心的無縫集成
- 多雲和混合雲場景下的 API 管理:挑戰與選擇
- 從 HTTP 到 gRPC:APISIX 中 etcd 操作的遷移之路
- 關於 OAuth 你又瞭解哪些?