Spring Web Service ─ Web Service 簡介與 Server 端實作
前言
在開發 Web 應用程式時,免不了需要串接他方提供的 API,除了常見的 Json格式外,偶爾也會遇到要求傳送 SOAP 格式的 Web Service。查閱資料的過程中,發現 Spring 有針對 Web Service 集成的套件 ─ Spring Web Service 十分快速便能滿足需求。
什麼是Web Service
Web Service 透過標準的 Web 協議提供服務,目的是讓來自不同平台的應用程式不用借助第三方軟體或是其他手段都可以直接使用服務。Web Service 主要根據 SOAP 協議進行傳遞 XML 格式消息,它的核心定義通常有 SOAP、WSDL、UDDI:
SOAP:
簡單物件存取協定(Simple Object Access Protocol)是交換資料的一種協議規範。
WSDL:
Web服務描述語言(Web Services Description Language)是描述 Web Service 發布的 XML 格式。
UDDI:
統一描述、發現和集成(Universal Description, Discovery, and Integration)的縮寫。它是一個基於 XML 的跨平台的描述規範,可以使世界範圍內的企業在網際網路上發布自己所提供的服務。
Spring Web Service
Spring Web Service(以下簡稱 Spring-WS)是專門處理 Web Service 構建與消費的集成產品,對於 XML 有相當靈活的處理,並且支援 WS-Security 以及與 Spring Security 集成。
創建 Web Service 有兩種開發樣式:Contract Last(後契約方法)和 Contract First(先契約方法)。後契約方法是從構建 Java 代碼開始,爾後生成Web 契約(即WSDL);當使用先契約方法時,則是由構建 WSDL 契約再用 Java 來實現所述契約。Spring-WS 是採用契約優先的方式,在 Spring 文件中有做詳盡的描述,請見 Why Contract First。
Spring-WS 是由多個 Spring 模組組成,它們的依賴關係如下:
- Spring-WS Core(spring-ws-core.jar):
提供 Server 端訊息調度和服務端點,以及 Client 端 Web Service 模板的模組。 - Spring XML(spring-xml.jar):
提供各種 XML 的支持類。 - Spring OXM(spring-oxm.jar):
提供 Java 物件和 XML 物件互相轉換的工具模組。 - Spring-WS Support(spring-ws-support.jar):
提供一些額外支援的模組,如 JMS(Java Message Service)或是 Email 等。 - Spring-WS Security (spring-ws-security.jar):
提供與 Core 模組與 Spring-Security 的集成,像是對 SOAP 訊息添加令牌(Token)、簽名(Sign)和加解密,以及用 Spring Security 實作身份驗證和授權。
大多時候我們是實做呼叫他方 Web Service 的 Client 端,為了可以完整測試,我們先建立 Web Service 的 Server 端開始,Client 端相關實作請參考 Spring Web Service ─ Client 端實作。
建立Web Service Provider
1. 添加依賴
在 Spring 專案添加 Maven 依賴 spring-boot-starter-web-services 和 wsdl4j,前者是 Spring-WS 的集成套件、後者是用於生成wsdl文件。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web-services</artifactId>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
</dependency>
2. 建立 XSD 檔
依據先契約的方式,我們在 resources 中建立 xsd 檔 ─ 用來描述服務請求和回應的實體,以下是簡單的書本服務描述,請求需帶入書本的 ISBN,服務則會回應該 ISBN 的書本資訊,服務啟動後 Spring-WS 會將這個文件發布為 WSDL 文件,而 Client 端可透過 WSDL 來建立與服務溝通的物件。
src/main/resources/book.xsd
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.example.com/book"
xmlns:tns="http://www.example.com/book" elementFormDefault="qualified">
<xs:element name="getBookRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="isbn" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="getBookResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="book" type="tns:book" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="book">
<xs:sequence>
<xs:element name="isbn" type="xs:string" />
<xs:element name="name" type="xs:string" />
<xs:element name="author" type="xs:string" />
<xs:element name="publishing" type="xs:string" />
<xs:element name="edition" type="xs:int" />
</xs:sequence>
</xs:complexType>
</xs:schema>
3. 生成 Java 物件
這裡提供兩種生成方式,一是用 maven plugin、二是用 command line 方式產生。
- 用 maven plugin 方式:
<schemaDirectory> ─ xsd 檔所在位置。
<outputDirectory> ─ 生成後的 Java 物件目標路徑。
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxb2-maven-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>xjc</id>
<goals>
<goal>xjc</goal>
</goals>
</execution>
</executions>
<configuration>
<schemaDirectory>${project.basedir}/src/main/resources/</schemaDirectory>
<outputDirectory>${project.basedir}/src/main/java</outputDirectory>
<clearOutputDir>false</clearOutputDir>
</configuration>
</plugin>
- command line 方式:
[目標路徑] ─ 若無目標路徑則以 command line 當下的路徑為準。
<schema file/URL/dir/jar> ─ xsd 檔所在位置
xjc [目標路徑] <schema file/URL/dir/jar>
值得注意的是,xsd 檔中的 targetNamespace 即是 Java 物件的路徑。比如說 targetNamespace="http://www.example.com/book",產生出的物件則是放在目標路徑(或 command line 當前路徑)/com/example/book中。
4. 加入服務資料來源
建立一個 Class 來模擬資料來源:
package com.example.demo;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import com.example.book.Book;
@Component
public class BookRepository {
private static final Map<String, Book> books = new HashMap<>();
@PostConstruct
public void initData() {
Book harryPotterI = new Book();
harryPotterI.setIsbn("9573317249");
harryPotterI.setName("哈利波特:神秘的魔法石");;
harryPotterI.setAuthor("J. K. 羅琳");
harryPotterI.setPublishing("1997");
harryPotterI.setEdition(1);
books.put(harryPotterI.getIsbn(), harryPotterI);
Book iceAndFireI = new Book();
iceAndFireI.setIsbn("9789861856216");
iceAndFireI.setName("冰與火之歌:權力遊戲");;
iceAndFireI.setAuthor("喬治馬汀");
iceAndFireI.setPublishing("2011");
iceAndFireI.setEdition(1);
books.put(iceAndFireI.getIsbn(), iceAndFireI);
Book lordOfRingsI = new Book();
lordOfRingsI.setIsbn("9789570841008");
lordOfRingsI.setName("魔戒首部曲:魔戒現身");;
lordOfRingsI.setAuthor("托爾金");
lordOfRingsI.setPublishing("2001");
lordOfRingsI.setEdition(1);
books.put(lordOfRingsI.getIsbn(), lordOfRingsI);
}
public Book findBookByIsbn(String isbn) {
Assert.notNull(isbn, "The isbn must not be null!");
return books.get(isbn);
}
}
5. 加入 SOAP Web Service Endpoint
Endpoint 負責提供業務邏輯的進入點,配置的細節如下:
@Endpoint :用 Spring WS 將類註冊為 Web 服務端點
@PayloadRoot :根據 namespace 和 localPart 屬性來分派處理方法。當消息中的元素符合名稱時,該方法就會被調用。
@ResponsePayload :指示此方法返回一個要映射的物件
@RequestPayload:指示此方法接受請求中要映射的物件
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;
import com.example.book.Book;
import com.example.book.GetBookRequest;
import com.example.book.GetBookResponse;
@Endpoint
public class BookEndpoint {
private static final String NAMESPACE_URI = "http://www.example.com/book";
@Autowired
BookRepository bookRepository;
@PayloadRoot(namespace = NAMESPACE_URI, localPart = "getBookRequest")
public @ResponsePayload GetBookResponse getBookByIsbn(@RequestPayload GetBookRequest request)
throws InterruptedException {
GetBookResponse response = new GetBookResponse();
Book book = bookRepository.findBookByIsbn(request.getIsbn());
response.setBook(book);
return response;
}
}
6.加入SOAP Web Service配置
@EnableWs:啟用 Spring WS 功能。
messageDispatcherServlet:
用來處理接受 SOAP 請求,裡面設定 ApplicationContext 使 Spring-WS 可以發現其他 Bean;setTransformWsdlLocations = true 用於轉換 WSDL 中<soap:address>的 location 屬性,反映此服務的地址。
defaultWsdl11Definition:
使用 xsd 檔創建 WSDL ,此方法上的 Bean name 即是 WSDL 的名稱。
package com.example.demo;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;
@EnableWs
@Configuration
public class WebServiceConfig {
private static final String NAMESPACE_URI = "http://www.example.com/book";
@Bean
public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean(servlet, "/bookService/*");
}
@Bean
public XsdSchema bookSchema() {
return new SimpleXsdSchema(new ClassPathResource("book.xsd"));
}
@Bean(name = "book")
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema bookSchema) {
DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
wsdl11Definition.setPortTypeName("BookPort");
wsdl11Definition.setLocationUri("/bookService");
wsdl11Definition.setTargetNamespace(NAMESPACE_URI);
wsdl11Definition.setSchema(bookSchema);
return wsdl11Definition;
}
}
測試
啟動後,輸入http://localhost:8080/bookService/book.wsdl 可查看該服務的描述。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:sch="http://www.example.com/book" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://www.example.com/book" targetNamespace="http://www.example.com/book">
<wsdl:types>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://www.example.com/book">
<xs:element name="getBookRequest">
<xs:complexType>
<xs:sequence>
<xs:element name="isbn" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="getBookResponse">
<xs:complexType>
<xs:sequence>
<xs:element name="book" type="tns:book"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="book">
<xs:sequence>
<xs:element name="isbn" type="xs:string"/>
<xs:element name="name" type="xs:string"/>
<xs:element name="author" type="xs:string"/>
<xs:element name="publishing" type="xs:string"/>
<xs:element name="edition" type="xs:int"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
</wsdl:types>
<wsdl:message name="getBookRequest">
<wsdl:part element="tns:getBookRequest" name="getBookRequest">
</wsdl:part>
</wsdl:message>
<wsdl:message name="getBookResponse">
<wsdl:part element="tns:getBookResponse" name="getBookResponse">
</wsdl:part>
</wsdl:message>
<wsdl:portType name="BookPort">
<wsdl:operation name="getBook">
<wsdl:input message="tns:getBookRequest" name="getBookRequest">
</wsdl:input>
<wsdl:output message="tns:getBookResponse" name="getBookResponse">
</wsdl:output>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="BookPortSoap11" type="tns:BookPort">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getBook">
<soap:operation soapAction=""/>
<wsdl:input name="getBookRequest">
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output name="getBookResponse">
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="BookPortService">
<wsdl:port binding="tns:BookPortSoap11" name="BookPortSoap11">
<soap:address location="http://localhost:8080/bookService"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
以 Postman 模擬發送 SOAP 請求訊息:
method:POST
Content-Type:text/xml
URL: http://localhost:8080/bookService
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:gs="http://www.example.com/book">
<soapenv:Header/>
<soapenv:Body>
<gs:getBookRequest>
<gs:isbn>9789861856216</gs:isbn>
</gs:getBookRequest>
</soapenv:Body>
</soapenv:Envelope>
服務返回的 SOAP 回應訊息:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<ns2:getBookResponse xmlns:ns2="http://www.example.com/book">
<ns2:book>
<ns2:isbn>9789861856216</ns2:isbn>
<ns2:name>冰與火之歌:權力遊戲</ns2:name>
<ns2:author>喬治馬汀</ns2:author>
<ns2:poblishing>2011</ns2:poblishing>
<ns2:edition>1</ns2:edition>
</ns2:book>
</ns2:getBookResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
結語
以上我們使用 Spring-WS 實作 Web Service 的 Server 端,從創建 xsd 檔生成 Java 物件,以及配置它的服務描述和收發 SOAP 訊息,這裡是以 Postman 模擬 Client 端來測試,接下來將以此篇 Server 端為基礎來實作 Client 端,請見 Spring Web Service ─ Client 端實作。
參考資料
Spring-WS Docs
Baeldung - Creating a SOAP Web Service with Spring
Wikipedia - Web Service
oKong - SpringBoot | 第三十三章:Spring web Servcies集成和使用