SpringBoot 2 构建 SOAP Web 服务

开篇词

该指南将引导你使用 Spring 创建基于 SOAP 的 Web 服务服务器的过程。
 

你将创建的应用

我们将使用基于 WSDL 的 SOAP Web 服务来构建一个服务器,以暴露来自各个欧洲国家数据。

为了简化示例,我们将使用英国、西班牙及波兰等硬编码数据。

 

你将需要的工具

  • 大概 15 分钟左右;
  • 你最喜欢的文本编辑器或集成开发环境(IDE)
  • JDK 1.8 或更高版本;
  • Gradle 4+Maven 3.2+
  • 你还可以将代码直接导入到 IDE 中:
    • Spring Too Suite (STS)
    • IntelliJ IDEA
       

如何完成这个指南

像大多数的 Spring 入门指南一样,你可以从头开始并完成每个步骤,也可以绕过你已经熟悉的基本设置步骤。如论哪种方式,你最终都有可以工作的代码。

  • 要从头开始,移步至从 Spring Initializr 开始
  • 要跳过基础,执行以下操作:
    • 下载并解压缩该指南将用到的源代码,或借助 Git 来对其进行克隆操作:git clone https://github.com/spring-guides/gs-soap-service.git
    • 切换至 gs-soap-service/initial 目录;
    • 跳转至该指南的添加 Spring-WS 依赖

待一切就绪后,可以检查一下 gs-soap-service/complete 目录中的代码。
 

从 Spring Initializr 开始

对于所有的 Spring 应用来说,你应该从 Spring Initializr 开始。Initializr 提供了一种快速的方法来提取应用程序所需的依赖,并为你完成许多设置。该示例需要 Spring Web 和 Spring Web Services 依赖。下图显示了此示例项目的 Initializr 设置:

上图显示了选择 Maven 作为构建工具的 Initializr。你也可以使用 Gradle。它还将 com.exampleproducing-web-service 的值分别显示为 Group 和 Artifact。在本示例的其余部分,将用到这些值。

以下清单显示了选择 Maven 时创建的 pom.xml 文件:


<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0modelVersion>
	<parent>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-parentartifactId>
		<version>2.2.2.RELEASEversion>
		<relativePath/> 
	parent>
	<groupId>com.examplegroupId>
	<artifactId>producing-web-serviceartifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>producing-web-servicename>
	<description>Demo project for Spring Bootdescription>

	<properties>
		<java.version>1.8java.version>
	properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-web-servicesartifactId>
		dependency>

		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-testartifactId>
			<scope>testscope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintagegroupId>
					<artifactId>junit-vintage-engineartifactId>
				exclusion>
			exclusions>
		dependency>
	dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>

project>

以下清单显示了在选择 Gradle 时创建的 build.gradle 文件:

plugins {
	id 'org.springframework.boot' version '2.2.2.RELEASE'
	id 'io.spring.dependency-management' version '1.0.8.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-web-services'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

test {
	useJUnitPlatform()
}

pom.xmlbuild.gradle 文件都显示其他构建信息,我们将在下一步中添加它们。

 

添加 Spring-WS 依赖

该项目需要在构建文件中包含 spring-ws-corewsdl4j 作为依赖。

以下示例显示了使用 Maven 时需要对 pom.xml 文件进行的更改。

<dependency>
	<groupId>wsdl4jgroupId>
	<artifactId>wsdl4jartifactId>
dependency>

以下示例显示了使用 Gradle 时需要对 build.gradle 文件进行的更改:

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-web-services'
	implementation 'wsdl4j:wsdl4j:1.6.1'
	jaxb("org.glassfish.jaxb:jaxb-xjc:2.2.11")
	compile(files(genJaxb.classesDir).builtBy(genJaxb))
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

 

创建 XML Schema 以定义域

Web 服务域是在 XML 模式文件(XSD)中定义的,Spring-WS 会自动将其导出为 WSDL。

创建 XSD 文件及返回国家 namepopulationcapitalcurrency 的操作。以下清单(来自 src/main/resources/countries.xsd)显示了必要的 XSD 文件:

<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 Schema 来生成领域类

下一步是从 XSD 文件生成 Java 类。正确的方法是在构建期间使用 Maven 或 Gradle 插件自动执行该操作。

以下清单显示了 Maven 所需的插件配置:

<plugin>
	<groupId>org.codehaus.mojogroupId>
	<artifactId>jaxb2-maven-pluginartifactId>
	<version>2.5.0version>
	<executions>
		<execution>
			<id>xjcid>
			<goals>
				<goal>xjcgoal>
			goals>
		execution>
	executions>
	<configuration>
		<sources>
			<source>${project.basedir}/src/main/resources/countries.xsdsource>
		sources>
	configuration>
plugin>

生成的类放在 target/generated-sources/jaxb/ 目录中。

要对 Gradle 进行同样的操作,首先需要在构建文件中配置 JAXB,如下清单所示:

configurations {
    jaxb
}

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

构建文件具有 tagend 注释。这些标签可以更轻松地将其提取到该指南中,以进行更详细的说明。我们不需要在自己的构建文件中使用这些注释。

下一步是添加 genJaxb 任务,Gradle 用来生成 Java 类。以下清单显示了必要的添加物:

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", includeantruntime: false,
                    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 复杂。

在这两种情况下,JAXB 域对象的生成过程都已连接到构建工具的生命周期中,因此无需执行其他步骤。
 

创建 Country 存储库

为了向 Web 服务提供数据,请创建国家/地区存储库。在该指南中,我们将创建一个具有硬编码数据的虚拟国家/地区存储库实现。以下清单(来自 src/main/java/com/example/producingwebservice/CountryRepository.java)显示了如何执行该操作:

package com.example.producingwebservice;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

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 Map<String, Country> countries = new HashMap<>();

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

		countries.put(spain.getName(), spain);

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

		countries.put(poland.getName(), poland);

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

		countries.put(uk.getName(), uk);
	}

	public Country findCountry(String name) {
		Assert.notNull(name, "The country's name must not be null");
		return countries.get(name);
	}
}

 

创建 Country 服务端点

要创建服务端点,只需要一个带有一些 Spring WS 注解的 POJO 即可处理传入的 SOAP 请求。以下清单(来自 src/main/java/com/example/producingwebservice/CountryEndpoint.java)显示了该类:

package com.example.producingwebservice;

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 消息的潜在候选者。
然后,Spring WS 使用 @PayloadRoot 注解根据消息的名称空间和 localPart 选择处理方法。
@RequestPayload 注解表示传入的消息将映射到方法的 request 参数。
@ResponsePayload 注解使 Spring WS 将返回的值映射到响应有效负载。

在所有这些代码块中,io.spring.guides 类将在我们的 IDE 中报告编译时错误,除非我们已经运行任务以基于 WSDL 生成领域类。

 

配置 Web 服务 Bean

使用与 Spring WS 相关的 bean 配置创建一个新类,如下面的清单(来自 src/main/java/com/example/producingwebservice/WebServiceConfig.java)所示:

package com.example.producingwebservice;

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.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 messageDispatcherServlet(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。重要的是注入设置 ApplicationContextMessageDispatcherServlet。否则,Spring WS 将不会自动检测 Spring Bean。
  • 命名该 bean 为 messageDispatcherServlet 不会替代 Spring Boot 的默认 DispatcherServlet bean
  • DefaultMethodEndpointAdapter 配置注释驱动的 Spring WS 编程模型。这样就可以使用各种注释,例如 @Endpoint(前面提过);
  • DefaultWsdl11Definition 通过使用 XsdSchema 公开了标准的 WSDL 1.1。

我们需要为 MessageDispatcherServlet 和 DefaultWsdl11Definition。Bean 名称确定 URL,web 服务和生成的 WSDL 文件可在该 URL 下使用。在这种情况下,WSDL 将在 http://:/ws/countries.wsdl 下可用。

该配置还使用 WSDL 位置 servlet 转换:servlet.setTransformWsdlLocations(true)。如果我们访问 http://localhost:8080/ws/countries.wsdlsoap:address 将具有正确的地址。相反,如果我们从分配给计算机的面向公众的 IP 地址访问 WSDL,则会看到该地址。
 

使应用可执行

Spring Boot 为我们创建一个应用类。在这种情况下,无需进一步的修改。我们可以使用它来运行该应用。以下清单(来自 src/main/java/com/example/producingwebservice/ProducingWebServiceApplication.java)显示了应用类:

package com.example.producingwebservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProducingWebServiceApplication {

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

@SpringBootApplication 是一个便利的注解,它添加了以下所有内容:

  • @Configuration:将类标记为应用上下文 Bean 定义的源;
  • @EnableAutoConfiguration:告诉 Spring Boot 根据类路径配置、其他 bean 以及各种属性的配置来添加 bean。
  • @ComponentScan:告知 Spring 在 com/example 包中寻找他组件、配置以及服务。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法启动应用。

构建可执行 JAR

我们可以结合 Gradle 或 Maven 来从命令行运行该应用。我们还可以构建一个包含所有必须依赖项、类以及资源的可执行 JAR 文件,然后运行该文件。在整个开发生命周期中,跨环境等等情况下,构建可执行 JAR 可以轻松地将服务作为应用进行发布、版本化以及部署。

如果使用 Gradle,则可以借助 ./gradlew bootRun 来运行应用。或通过借助 ./gradlew build 来构建 JAR 文件,然后运行 JAR 文件,如下所示:

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

由官网提供的以上这条命令的执行结果与我本地的不一样,我需要这样才能运行:java -jar build/libs/gs-producing-web-service-0.0.1.jar

如果使用 Maven,则可以借助 ./mvnw spring-boot:run 来运行该用。或可以借助 ./mvnw clean package 来构建 JAR 文件,然后运行 JAR 文件,如下所示:

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

由官网提供的以上这条命令的执行结果与我本地的不一样,我需要这样才能运行:java -jar target/gs-producing-web-service-0.0.1.jar

我们还可以将 JAR 应用转换成 WAR 应用

显示日志记录输出。该服务应在几秒内启动并运行。
 

测试应用

现在该应用正在运行,我们可以对其进行测试。创建一个名为 request.xml 的文件,其中包含一下 SOAP 请求:

<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>Spaings:name>
      gs:getCountryRequest>
   soapenv:Body>
soapenv:Envelope>

在测试 SOAP 接口时,有几个选项。我们可以使用类似于 SoapUI 的工具,也可以在 *nix / Mac 系统上使用命令行工具。以下示例从命令行使用 curl:

# Use data from file
curl --header "content-type: text/xml" -d @request.xml http://localhost:8080/ws
# Use inline XML data
curl <<-EOF -fsSL -H "content-type: text/xml" -d @- http://localhost:8080/ws \
  > response.xml && xmllint --format response.xml

<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>

EOF

结果,我们应该看到以下响应:


<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>Spainns2:name>
        <ns2:population>46704314ns2:population>
        <ns2:capital>Madridns2:capital>
        <ns2:currency>EURns2:currency>
      ns2:country>
    ns2:getCountryResponse>
  SOAP-ENV:Body>
SOAP-ENV:Envelope>

奇怪的是,输出将是一个紧凑的 XML 文档,而不是上面显示的格式良好的文档。如果我们的系统上安装了 xmllib2,则可以使用 curl -fsSL --header "content-type: text/xml" -d @request.xml http://localhost:8080/ws > output.xml and xmllint --format output.xml 来查看格式良好的结果。

以上这条命令在 Mac 命令行不能执行,提示 curl 无法识别提供的参数。

 

概述

恭喜你!我们已经使用 Spring Web Services 开发了基于 SOAP 的服务。
 

参见

以下指南也可能会有所帮助:

  • 消费 SOAP Web 服务
  • 使用 Spring Boot 构建应用

想看指南的其他内容?请访问该指南的所属专栏:《Spring 官方指南

你可能感兴趣的:(Spring,入门指南)