Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您使用 Spring 创建一个基于 SOAP 的 Web 服务服务器。

你将构建什么

您将构建一个服务器,通过使用基于 WSDL 的 SOAP Web 服务来公开来自各个欧洲国家的数据。

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

所需内容

如何完成本指南 {#_how_to_complete_this_guide}

与大多数 Spring 入门指南一样,您可以从头开始并完成每个步骤,也可以跳过您已经熟悉的基本设置步骤。无论哪种方式,您最终都会获得可运行的代码。

从头开始,请继续阅读从 Spring Initializr 开始

跳过基础部分,请执行以下操作:

完成后,您可以将您的结果与 gs-soap-service/complete 中的代码进行比对。

从 Spring Initializr 开始

您可以使用这个预初始化项目,点击生成以下载一个ZIP文件。该项目已配置为适合本教程中的示例。

要手动初始化项目:

  1. 访问 https://start.spring.io。该服务会拉取应用程序所需的所有依赖项,并为您完成大部分设置工作。

  2. 选择 Gradle 或 Maven 以及您想要使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies,然后选择 Spring WebSpring Web Services

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,这是一个根据您的选择配置好的 Web 应用程序的存档。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成这个过程。

pom.xmlbuild.gradle 文件都需要额外的构建信息,您将在下一步中添加这些信息。

您也可以从 Github 上 fork 该项目,并在您的 IDE 或其他编辑器中打开它。

添加 Spring-WS 依赖

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

以下示例展示了如果您使用 Maven,需要对 pom.xml 文件进行的更改:

<dependency>
    <groupId>wsdl4j</groupId>
    <artifactId>wsdl4j</artifactId>
</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'
    jaxb("org.glassfish.jaxb:jaxb-xjc")
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}

创建 XML 架构以定义域

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

创建一个包含返回国家 namepopulationcapitalcurrency 操作的 XSD 文件。以下代码清单(来自 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 模式生成域类 {#_generate_domain_classes_based_on_an_xml_schema}

下一步是从 XSD 文件生成 Java 类。正确的方法是在构建时使用 Maven 或 Gradle 插件自动完成。

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

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxb2-maven-plugin</artifactId>
    <version>3.1.0</version>
    <executions>
        <execution>
            <id>xjc</id>
            <goals>
                <goal>xjc</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <sources>
            <source>${project.basedir}/src/main/resources/countries.xsd</source>
        </sources>
    </configuration>
</plugin>

生成的类会被放置在 target/generated-sources/jaxb/ 目录下。

要在 Gradle 中实现同样的效果,首先需要在构建文件中配置 JAXB,如下面的代码清单所示:

configurations {
    jaxb
}

bootJar {
    archiveBaseName = 'gs-producing-web-service'
    archiveVersion =  '0.1.0'
}

构建文件中有 tagend 注释。这些标签使得将其中的部分内容提取到本指南中以便更详细地解释变得更加容易。您在自己的构建文件中不需要这些注释。

下一步是添加genJaxb任务,Gradle 使用它来生成 Java 类。我们需要配置 Gradle 在build/generated-sources/jaxb目录下找到这些生成的 Java 类,并将genJaxb作为compileJava任务的依赖项。以下代码片段展示了必要的配置:

sourceSets {
    main {
        java {
            srcDir 'src/main/java'
            srcDir 'build/generated-sources/jaxb'
        }
    }
}

task genJaxb {
    ext.sourcesDir = "${buildDir}/generated-sources/jaxb"
    ext.schema = "src/main/resources/countries.xsd"

    outputs.dir sourcesDir

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

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

compileJava.dependsOn genJaxb

由于 Gradle 还没有 JAXB 插件,因此需要使用 Ant 任务,这使得它比在 Maven 中稍微复杂一些。

在这两种情况下,JAXB 领域对象的生成过程已经集成到构建工具的生命周期中,因此不需要额外的步骤来运行。

创建国家信息库

为了向 Web 服务提供数据,创建一个国家信息仓库。在本指南中,您将创建一个带有硬编码数据的虚拟国家信息仓库实现。以下代码清单(来自 src/main/java/com/example/producingwebservice/CountryRepository.java)展示了如何实现这一点:

package com.example.producingwebservice;

import jakarta.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);
    }
}

创建国家服务端点

要创建一个服务端点,您只需要一个带有几个 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 注解根据消息的 namespacelocalPart 来选择处理方法。

@RequestPayload 注解表示传入的消息将被映射到方法的 request 参数。

@ResponsePayload 注解使 Spring WS 将返回值映射到响应负载中。

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

配置 Web 服务 Beans

创建一个包含 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> 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。重要的是要将 ApplicationContext 注入并设置到 MessageDispatcherServlet 中。如果没有这样做,Spring WS 将无法自动检测 Spring bean。

  • 将此 bean 命名为 messageDispatcherServlet 并不会替换 Spring Boot 的 默认 DispatcherServlet bean

  • DefaultMethodEndpointAdapter 配置了注解驱动的 Spring WS 编程模型。这使得可以使用各种注解,例如之前提到的 @Endpoint

  • DefaultWsdl11Definition 通过使用 XsdSchema 来暴露标准的 WSDL 1.1。

您需要为MessageDispatcherServletDefaultWsdl11Definition指定Bean名称。Bean名称决定了Web服务和生成的WSDL文件可用的URL。在这种情况下,WSDL将在http://下可用。

此配置还使用了 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。例如,如果 spring-webmvc 在类路径上,该注解会将应用程序标记为 Web 应用程序并激活关键行为,例如设置 DispatcherServlet

  • @ComponentScan: 告诉 Spring 在 com/example 包中查找其他组件、配置和服务,使其能够找到控制器。

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法来启动应用程序。您是否注意到没有一行 XML 代码?也没有 web.xml 文件。这个 Web 应用程序是 100% 纯 Java 的,您无需处理任何配置或基础设施的设置。

构建可执行的 JAR 文件

您可以使用 Gradle 或 Maven 从命令行运行应用程序。您还可以构建一个包含所有必要依赖项、类和资源的可执行 JAR 文件并运行它。构建可执行 jar 文件使得在开发生命周期中、跨不同环境等场景下,将服务作为应用程序进行交付、版本控制和部署变得非常容易。

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

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

如果您使用 Maven,可以通过 ./mvnw spring-boot:run 来运行应用程序。或者,您可以使用 ./mvnw clean package 构建 JAR 文件,然后像下面这样运行该 JAR 文件:

java -jar target/gs-soap-service-0.1.0.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>Spain</gs: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 \
  > target/response.xml && xmllint --format target/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

因此,您应该会看到以下响应:

<?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 -fsSL --header "content-type: text/xml" -d @request.xml http://localhost:8080/ws > output.xml 然后使用 xmllint --format output.xml 来查看格式良好的结果。

总结

恭喜!您已经使用 Spring Web Services 开发了一个基于 SOAP 的服务。

另请参阅

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

本页目录