翻譯-使用Spring WebService生成SOAP Web Service

黃博文發表於2014-09-05

原文連結:http://spring.io/guides/gs/producing-web-service/

生成SOAP web service

該指南將帶領你使用Spring建立一個基於SOAP的web service的整個過程。

指南內容

你將建立一個服務,該服務通過一個基於WSDL的SOAP web service向外暴露歐洲國家的資料。

 注意:為了簡化該示例,你將使用硬編碼方式嵌入英國,西班牙及波蘭。

準備事項

  • 15分鐘

  • 喜愛的編輯器或IDE

  • JDK1.6或更高版本

  • Gradle 1.11+或Maven 3.0+

  • 你也可以直接通過該指南匯入程式碼,或通過Spring工具集(Spring Tool Suite,簡稱STS)通過網頁瀏覽程式碼,從而幫助你學習該章節內容。原始碼下載地址: https://github.com/spring-guides/gs-producing-web-service.git

如何完成該指南

如同大多數的示例教程一樣,你可以從頭開始並完成每個步驟,或者你也可以跳過已經熟悉的基礎章節。無論怎樣,最終你要得到可以工作的程式碼。

從頭開始,請移動到使用Gradle構建章節。

跳過基礎部分,請做以下事情:

  • 下載並解壓該向導的原始碼,或者使用Git複製一份: git clone https://github.com/spring-guides/gs-soap-service.git

  • 切換到gs-soap-service/initial

  • 跳到新增Spring-WS依賴章節。

當完成後,你可以使用gs-soap-service/complete目錄中的程式碼檢查你的結果。

使用Gradle構建

首先你要設定一個基本的build指令碼。當構建Spring應用程式時,你可以使用任何構建系統,但是這裡只包括了使用MavenGradle的程式碼。如果你兩者都不熟悉,請訪問使用Gradle構建Java專案使用Maven構建Java專案

建立目錄結構

在你選擇的存放專案的目錄中,建立如下的子目錄結構。例如,在*nix系統中使用mkdir -p src/main/java/hello

1
2
3
4
└── src
    └── main
        └── java
            └── hello

建立Gradle 構建檔案

下面是一個初始的Gradle build檔案

build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
buildscript {
    repositories {
        mavenLocal()
        mavenCentral()
        maven { url "http://repo.spring.io/libs-release" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.5.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

jar {
    baseName = 'gs-producing-web-service'
    version =  '0.1.0'
}

repositories {
    mavenLocal()
    mavenCentral()
    maven { url "http://repo.spring.io/libs-release" }
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
}

task wrapper(type: Wrapper) {
    gradleVersion = '1.11'
}

Spring Boot gradle外掛提供了很多便利的特性:

  • 將classpath中的所有jar包構建單個可執行的jar包,從而更容易執行和傳播服務。

  • 搜尋public static void main()方法並標記為可執行的類。

  • 提供了一個內建的依賴管理器,設定依賴版本以匹配Spring Boot依賴。你可以覆蓋為任何你希望的版本,但預設會使用Boot選擇的版本。

使用Maven構建

首先你需要設定一個基本的構建指令碼。你可以使用任何構建系統來構建Spring應用程式,但這裡包含了Maven的程式碼。如果你對Maven不熟,請訪問使用Maven構建Java專案

建立目錄結構

在你選擇的存放專案的目錄中,建立如下的子目錄結構。例如,在*nix系統中使用mkdir -p src/main/java/hello

1
2
3
4
└── src
    └── main
        └── java
            └── hello
pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-producting-web-service</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.5.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- use UTF-8 for everything -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

注意:你可能注意到我們指定了maven-complier-plugin的版本。通常並不推薦這樣做。這裡主要是為了解決我們的CI系統預設執行在該外掛的早期版本(java5之前)的一個問題。

Spring Boot Maven外掛提供了很多便利的特性:

  • 將classpath中的所有jar包構建單個可執行的jar包,從而更容易執行和傳播服務。

  • 搜尋public static void main()方法並標記為可執行的類。

  • 提供了一個內建的依賴管理器,設定依賴版本以匹配Spring Boot依賴。你可以覆蓋為任何你希望的版本,但預設會使用Boot選擇的版本。

使用Spring工具集構建

如果你擁有Spring工具集,只需簡單的直接匯入該指南

新增Spring-ws依賴

你建立的專案需要新增spring-ws-core和wsdl4j依賴到構建檔案中。

maven程式碼:

1
2
3
4
5
6
7
8
9
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-ws</artifactId>
</dependency>
<dependency>
    <groupId>wsdl4j</groupId>
    <artifactId>wsdl4j</artifactId>
    <version>1.6.1</version>
</dependency>

gradle程式碼:

1
2
3
4
5
6
dependencies {
    compile("org.springframework.boot:spring-boot-starter-ws")
    compile("wsdl4j:wsdl4j:1.6.1")
    jaxb("com.sun.xml.bind:jaxb-xjc:2.2.4-1")
    compile(files(genJaxb.classesDir).builtBy(genJaxb))
}

建立XML格式來定義領域物件

該web service領域物件被定義在一個XML格式檔案中(XSD),Spring-WS將自動匯出為一個WSDL檔案。

建立一個XSD檔案,包含一個操作來返回一個國家的名稱人口首都貨幣

src/main/resources/countries.xsd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://spring.io/guides/gs-producing-web-service"
           targetNamespace="http://spring.io/guides/gs-producing-web-service" elementFormDefault="qualified">

    <xs:element name="getCountryRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="name" type="xs:string"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="getCountryResponse">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="country" type="tns:country"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:complexType name="country">
        <xs:sequence>
            <xs:element name="name" type="xs:string"/>
            <xs:element name="population" type="xs:int"/>
            <xs:element name="capital" type="xs:string"/>
            <xs:element name="currency" type="tns:currency"/>
        </xs:sequence>
    </xs:complexType>

    <xs:simpleType name="currency">
        <xs:restriction base="xs:string">
            <xs:enumeration value="GBP"/>
            <xs:enumeration value="EUR"/>
            <xs:enumeration value="PLN"/>
        </xs:restriction>
    </xs:simpleType>
</xs:schema>

 基於XML格式建立領域類

接下來的步驟是根據XSD檔案來建立java類。正確的方式是使用maven或gradle外掛在構建時間自動建立。

maven外掛配置;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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>

生成的類放置在target/generated-sources/jaxb目錄。

gradle外掛配置如下,首先需要在構建檔案中配置JAXB:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
configurations {
    jaxb
}

jar {
    baseName = 'gs-producing-web-service'
    version =  '0.1.0'
    from genJaxb.classesDir
}

// tag::dependencies[]
dependencies {
    compile("org.springframework.boot:spring-boot-starter-ws")
    compile("wsdl4j:wsdl4j:1.6.1")
    jaxb("com.sun.xml.bind:jaxb-xjc:2.2.4-1")
    compile(files(genJaxb.classesDir).builtBy(genJaxb))
}
// end::dependencies[]

注意:上面的構建檔案擁有tag及end註釋。目的是為了能夠在本指南中更容易抽取出來並做進一步解釋。在你的構建檔案中無需這些註釋。

接下來的步驟是新增任務genJaxb,該任務會生成java類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
task genJaxb {
    ext.sourcesDir = "${buildDir}/generated-sources/jaxb"
    ext.classesDir = "${buildDir}/classes/jaxb"
    ext.schema = "src/main/resources/countries.xsd"

    outputs.dir classesDir

    doLast() {
        project.ant {
            taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask",
                    classpath: configurations.jaxb.asPath
            mkdir(dir: sourcesDir)
            mkdir(dir: classesDir)

            xjc(destdir: sourcesDir, schema: schema) {
                arg(value: "-wsdl")
                produces(dir: sourcesDir, includes: "**/*.java")
            }

            javac(destdir: classesDir, source: 1.6, target: 1.6, debug: true,
                    debugLevel: "lines,vars,source",
                    classpath: configurations.jaxb.asPath) {
                src(path: sourcesDir)
                include(name: "**/*.java")
                include(name: "*.java")
            }

            copy(todir: classesDir) {
                fileset(dir: sourcesDir, erroronmissingdir: false) {
                    exclude(name: "**/*.java")
                }
            }
        }
    }
}

由於gradle還沒有jaxb外掛,所以它呼叫了一個ant任務,程式碼看起來比maven稍顯複雜。

在maven和gradle兩個示例中,JAXB領域物件生成過程被包括在構建工具的生命週期中,所以無需額外步驟來執行。

建立國家倉庫

為了給web service提供資料,需要建立一個國家倉庫,在本指南中建立了一個硬編碼的偽造的國家倉庫實現。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package hello;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;

import io.spring.guides.gs_producing_web_service.Country;
import io.spring.guides.gs_producing_web_service.Currency;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

@Component
public class CountryRepository {
    private static final List<Country> countries = new ArrayList<Country>();

    @PostConstruct
    public void initData() {
        Country spain = new Country();
        spain.setName("Spain");
        spain.setCapital("Madrid");
        spain.setCurrency(Currency.EUR);
        spain.setPopulation(46704314);

        countries.add(spain);

        Country poland = new Country();
        poland.setName("Poland");
        poland.setCapital("Warsaw");
        poland.setCurrency(Currency.PLN);
        poland.setPopulation(38186860);

        countries.add(poland);

        Country uk = new Country();
        uk.setName("United Kingdom");
        uk.setCapital("London");
        uk.setCurrency(Currency.GBP);
        uk.setPopulation(63705000);

        countries.add(uk);
    }

    public Country findCountry(String name) {
        Assert.notNull(name);

        Country result = null;

        for (Country country : countries) {
            if (name.equals(country.getName())) {
                result = country;
            }
        }

        return result;
    }
}

建立國家服務終端

為了建立一個service endpoint,x需要一個pojo物件,以及一些Spring WS註解來處理SOAP請求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package hello;

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 io.spring.guides.gs_producing_web_service.GetCountryRequest;
import io.spring.guides.gs_producing_web_service.GetCountryResponse;

@Endpoint
public class CountryEndpoint {
    private static final String NAMESPACE_URI = "http://spring.io/guides/gs-producing-web-service";

    private CountryRepository countryRepository;

    @Autowired
    public CountryEndpoint(CountryRepository countryRepository) {
        this.countryRepository = countryRepository;
    }

    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getCountryRequest")
    @ResponsePayload
    public GetCountryResponse getCountry(@RequestPayload GetCountryRequest request) {
        GetCountryResponse response = new GetCountryResponse();
        response.setCountry(countryRepository.findCountry(request.getName()));

        return response;
    }
}

@Endpoint向Spring WS註冊了該類為一個處理來臨的SOAP訊息的潛在物件。

@PayloadRoot 被Spring WS用來根據訊息的名稱空間localPart來選擇處理該請求的方法。

@RequestPayload 指明來臨的訊息將被對映到該方法的request引數。

@ResponsePayload註解將使得Spring WS將返回值與響應負載對映起來。

注意:在以上程式碼中,如果你沒有執行任務來根據WSDL生成領域物件,那麼在你的IDE中io.spring.guides類將會報告編譯時錯誤。

配置web service bean

使用Spring WS相關的bean配置選項建立一個新的類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package hello;

import org.springframework.boot.context.embedded.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.config.annotation.WsConfigurerAdapter;
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 extends WsConfigurerAdapter {
    @Bean
    public ServletRegistrationBean dispatcherServlet(ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        servlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(servlet, "/ws/*");
    }

    @Bean(name = "countries")
    public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema countriesSchema) {
        DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
        wsdl11Definition.setPortTypeName("CountriesPort");
        wsdl11Definition.setLocationUri("/ws");
        wsdl11Definition.setTargetNamespace("http://spring.io/guides/gs-producing-web-service");
        wsdl11Definition.setSchema(countriesSchema);
        return wsdl11Definition;
    }

    @Bean
    public XsdSchema countriesSchema() {
        return new SimpleXsdSchema(new ClassPathResource("countries.xsd"));
    }
}
  • 這裡Spring WS使用了不同的servlet型別來處理SOAP訊息:MessageDispatcherServlet。注入及設定MessageDispatcherServletApplicationContext是非常重要的。如果不這樣做,Spring WS無法自動檢測到Spring bean。

  • 通過給dispatcherServlet bean命名,替代了Spring Boot中預設的DispatcherServlet bean

  • DefaultMethodEndpointAdapter配置了註解驅動的Spring WS程式設計模型。這使得使用前面提過的諸如@Endpoint等各種各樣的註解成為可能。

  • DefaultWsdl11Defination使用XsdSchema暴露了一個標準的WSDL 1.1。

請注意你需要為MessageDispatcherServletDefaultWsdl11Definition制定bean名稱,這是非常重要的。Bean名稱決定了生成的WSDL檔案在哪個web service是可用的。在本例中,WSDL可通過http://<host>:<port>/ws/countries.wsdl來訪問。

該配置也使用了WSDL位置servlet轉化servlet.setTransformWsdlLocations(true)。如果你訪問http://localhost:8080/ws/countries.wsdlsoap:address將擁有正確的值。如果你使用本機的公共IP來訪問該WSDL,你將看到的是IP。

建立該程式的可執行檔案

儘管我們可以將該程式打包成一個傳統的war包並部署到一個外部的應用程式伺服器中,但是最簡單的方式還是下面所演示的,建立一個能獨立執行的應用程式。你可以通過老但好用的java main()方法,將所有檔案打包到單個可執行的jar包中。同時,可以藉助於Spring的支援內建Tomcat servlet容器作為HTTP執行時,從而無需部署到外部的例項中。

src/main/java/hello/Application.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

main()方法使用了SpringApplication輔助方法,將Application.class作為引數傳遞給其自身的run()方法。這告訴Spring讀取Application中的註解後設資料,並將其作為Spring 應用程式上下文的元件。

@ComponentScan註解告訴Spring遞迴搜尋hello包及其子包中找到被直接或者間接使用了Spring的@Component註解的類。該指令確保了Spring發現並註冊CountryRepositoryCountriesEndpoint,因為他們被標記為@Component@Endpoint,這是一種@Component註解。

@EnableAutoConfiguration註解會基於classpath內容切換到預設的合理的行為。例如,由於應用程式依賴Tomcat的內建版本(tomcat-embed-core.jar),Spring會替你設定並配置一個預設的合理的Tomcat伺服器。並且該程式還依賴Spring MVC(spring-webmvc.jar),Spring會配置並註冊以惡搞Spring MVC DispatcherServlet,根本無需web.xml檔案!自動配置是強大的,彈性的機制。請檢視API文件獲取更多細節。

構建可執行的jar包

你可以建立一個包含所有必須的依賴,類,及資源的可執行的JAR檔案。這很方便傳輸,版本管理以及獨立於部署生命週期來部署服務,跨不同的環境,諸如此類。

1
./gradlew build

然後你可以執行WAR檔案:

1
java -jar build/libs/gs-soap-service-0.1.0.jar

如果你使用的是maven,你可以使用mvn spring-boot:run來執行程式,或者你可以使用mvn clean package構建JAR檔案,並使用下面命令來執行:

1
java -jar target/gs-soap-service-0.1.0.jar

注意:上面的產出物是以惡搞可執行JAR檔案。你也可以建立一個經典的WAR檔案

 執行服務

如果使用的是Gradle,可以使用以下命令來執行服務:

1
./gradlew clean build && java -jar build/libs/gs-soap-service-0.1.0.jar

注意:如果你使用的是Maven,可以使用以下命令來執行服務:mvn clean package && java -jar target/gs-soap-service-0.1.0.jar

你也可以通過Gradle直接執行該程式:

1
./gradlew bootRun

注意:使用mvn的話,命令是mvn spring-boot:run

可以看到日誌輸出。該服務應該在幾秒鐘內啟動並執行起來。

 測試該程式

現在該程式正在執行,你可以測試它。建立一個名為request.xml檔案,包含以下的SOAP請求;

1
2
3
4
5
6
7
8
9
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:gs="http://spring.io/guides/gs-producing-web-service">
   <soapenv:Header/>
   <soapenv:Body>
      <gs:getCountryRequest>
         <gs:name>Spain</gs:name>
      </gs:getCountryRequest>
   </soapenv:Body>
</soapenv:Envelope>

有很多方式來測試該SOAP介面。你可以使用SoapUI等工具,或者如果你使用的是*nix/Mac系統的話,直接可以使用命令列,如下所示:

1
$ curl --header "content-type: text/xml" -d @request.xml http://localhost:8080/ws

你將看到如下的響應結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <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>

注意:輸出可能是一個緊湊的XML文件,而不是上面顯示的格式友好的文件。如果系統中安裝了xmllib2,可以使用curl <args above> > output.xml | xmllint --format output.xml來檢視格式友好的結果。

 總結

恭喜你!你使用Spring Web Service開發完成了一個基於SOAP的服務。

相關文章