用Apache CXF和Spring消耗SOAP服务的详细指南

634 阅读3分钟

使用Apache CXF和Spring消费SOAP服务

阅读本指南,了解如何使用Apache CXF和Spring boot来消费SOAP webservice。我们还添加了一些关于CXF日志的额外信息。

Soap网络服务已经不流行了,但如果你正在处理旧的应用程序,你可能仍然需要处理soap网络服务。我将给你一个例子,说明如何用CXF消费一个soap服务,如何为它做一个配置,以及如何记录对它的请求和响应。

一个简单的Web服务

在我能够消费一个Web服务之前,我需要一个简单的Web服务来工作。对于这个项目,我将使用spring boot 2.5.0版本和java 11。如果你喜欢,你可以使用其他的版本,只要版本不是太老,这并不重要。你可以用_spring initializer_轻松创建一个spring boot项目。如果你使用maven,pom.xml应该是这样的。如果你使用Java 8,你不需要有 "jaxb-runtime "依赖。

XML

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cfxconsumer</groupId>
    <artifactId>soapcxfconsumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>soapcxfconsumer</name>
    <description>Demo spring project for soap consuming with apache cxf</description>
    <properties>
        <java.version>11</java.version>
        <cxf.version>3.3.3</cxf.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-spring-boot-starter-jaxws</artifactId>
            <version>${cxf.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-features-logging</artifactId>
            <version>${cxf.version}</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

首先,我将为我的简单网络服务创建一个接口,如下所示。正如你所看到的,它将获得一个单一的字符串参数并返回一个字符串响应。

Java

@WebService
public interface HelloWorldWS {
    @WebMethod
    String createMessage(@WebParam(name = "createMessageRequest", mode = WebParam.Mode.IN) String name);
}

现在,我需要一个这个接口的实现。它将只是在输入参数前面加上 "Hello",并返回它。

Java

@Component
public class HelloWorldWSImpl implements HelloWorldWS{
    @Override
    public String createMessage(String name){
        return "Hello "+name;
    }
}

这不是本文的主题,这就是为什么我不打算详细介绍CXF的配置。你只需要像下面这样配置CXF。

Java

import javax.xml.ws.Endpoint;
import org.apache.cxf.Bus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import com.cfxconsumer.soapcxfconsumer.ws.HelloWorldWS;

@Configuration
@ImportResource({ "classpath:META-INF/cxf/cxf.xml" })
public class CxfWebServiceConfig {
    @Autowired
    private Bus cxfBus;

    @Bean
    public ServletRegistrationBean cxfServlet() {
        org.apache.cxf.transport.servlet.CXFServlet cxfServlet = new org.apache.cxf.transport.servlet.CXFServlet();
        ServletRegistrationBean servletDef = new ServletRegistrationBean<>(cxfServlet, "/ws/*");
        servletDef.setLoadOnStartup(1);
        return servletDef;
    }

    @Bean
    public Endpoint helloWorldWebService(HelloWorldWS helloWorldWS) {
        EndpointImpl endpoint = new EndpointImpl(cxfBus, helloWorldWS);
        endpoint.setAddress("/helloWorldWS");
        endpoint.publish();
        return endpoint;
    }
}

现在,如果我启动我的spring boot应用程序,我可以像这样到达我的简单Web服务的WSDL。

我将写一个测试类来测试我的简单Web服务,如下所示:

Java

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class HelloWorldWSIT {

    private HelloWorldWS testClient;

    @BeforeEach
    public void init(){
        JaxWsProxyFactoryBean jaxWsProxyFactory = new JaxWsProxyFactoryBean();
        jaxWsProxyFactory.setServiceClass(HelloWorldWS.class);
        jaxWsProxyFactory.setServiceName(new QName(
                "http://ws.soapcxfconsumer.cfxconsumer.com/",
                "HelloWorldWSImplService"
        ));
        jaxWsProxyFactory.setAddress("http://localhost:8080/ws/helloWorldWS");
        testClient = jaxWsProxyFactory.create(HelloWorldWS.class);
    }

    @Test
    void name() {
        System.out.println("soap response: "+ testClient.createMessage("General Kenobi"));
    }
}

当我运行它时,我看到下面的输出。我的网络服务工作了,现在让我们进入正题。

网络服务客户端消费者配置

如果你愿意,你可以启动一个新的项目,就像之前的项目一样,来消费HelloWorld的网络服务。在消费网络服务之前,我需要一个客户端jar文件。我将使用wsimport工具来创建一个客户端jar文件,命令是这样的。

wsimport -clientjar helloWorldWSClient.jar "http://localhost:8080/ws/helloWorldWS?wsdl"

客户端jar文件被创建了,我需要把它作为一个依赖项添加到项目中。之后,我可以为它创建一个CXF配置,如下所示。

Java

import java.util.HashMap;
import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.ws.BindingProvider;
import org.apache.cxf.ext.logging.LoggingFeature;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HelloWorldWSClientConfig {

    @Bean
    public LoggingFeature loggingFeature() {
        LoggingFeature loggingFeature = new LoggingFeature();
        loggingFeature.setPrettyLogging(true);
        return loggingFeature;
    }

    @Bean(name = "helloWorldWSClient")
    public HelloWorldWS helloWorldWSService(@Autowired LoggingFeature loggingFeature){
        JaxWsProxyFactoryBean fb = new JaxWsProxyFactoryBean();
        fb.setServiceClass(HelloWorldWS.class);
        fb.setWsdlLocation("/META-INF/wsdl/HelloWorldWSService.wsdl");
        fb.setServiceName(new QName("http://ws.soapcxfconsumer.cfxconsumer.com/",
                "HelloWorldWSImplService"));

        Map<String, Object> properties = fb.getProperties();
        if(properties == null){
            properties = new HashMap<>();
        }

        properties.put("javax.xml.ws.client.connectionTimeout", 5000);
        properties.put("javax.xml.ws.client.receiveTimeout", 3000);

        fb.setProperties(properties);

        fb.getFeatures().add(loggingFeature);

        HelloWorldWS theService = (HelloWorldWS) fb.create();
        BindingProvider bindingProvider = (BindingProvider) theService;
        bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, "http://localhost:8080/ws/helloWorldWS");

        return theService;
    }
}

我创建了一个bean名称为 "helloWorldWSClient",如你所见。_HelloWorldWS.class_是包含在客户端jar文件中的服务类。_WSDL位置_是jar文件中WSDL文件的路径。_serviceName_包括命名空间和端口名。你可以把一些配置参数放到属性图中(比如连接和套接字超时值)。最后要做的是设置服务的端点。

_LoggingFeature_是用来记录传出的请求和传入的响应的。如果你想记录它们,你只需在_application.properties_文件中加入下面这一行:

logging.level.org.apache.cxf.services = INFO

这将导致所有的CXF客户端记录请求和响应。如果你想只记录一个特定的服务,那么你需要有像下面这样的日志配置。

logging.level.org.apache.cxf.services.HelloWorldWS.REQ_OUT = INFO
logging.level.org.apache.cxf.services.HelloWorldWS.RESP_IN = INFO

很明显,你可以使用服务类名称来记录一个特定的服务。

消耗网络服务

配置完成后,我准备调用和消费我们的Soap服务。首先,我将创建一个消费者服务,如下所示。

Java

@Service
public class HelloWorldWSConsumerService {
    private final HelloWorldWS helloWorldWSClient;

    public HelloWorldWSConsumerService(HelloWorldWS helloWorldWS){
        this.helloWorldWSClient = helloWorldWS;
    }

    public String callHelloWorld(String name){
        return helloWorldWSClient.createMessage(name);
    }
}

现在,我需要测试这个消费者服务。我将用一个简单的断言编写一个简单的测试类。

Java

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloWorldWSConsumerServiceIT {
    @Autowired
    private HelloWorldWSConsumerService helloWorldWSConsumerService;

    @Test
    void testCallHelloWorld() {
        Assertions.assertEquals("Hello General Kenobi",
                helloWorldWSConsumerService.callHelloWorld("General Kenobi"));
    }
}

运行这个测试后,让我们看一下控制台的输出。

就这样了。