使用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"));
}
}
运行这个测试后,让我们看一下控制台的输出。
就这样了。