無需二次開發,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 版權協議): http://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 版權協議): http://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 版權協議): http://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 為例:
http://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,例如 http://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 你又瞭解哪些?