SpringBoot2.x系列教程80--SpringBoot整合CXF构建Web Service服务端与客户端
1. 环境准备与项目初始化在开始整合CXF之前我们需要先准备好开发环境。我推荐使用IntelliJ IDEA作为开发工具它能够很好地支持Spring Boot项目。首先创建一个新的Spring Boot项目选择Maven作为构建工具Java版本建议使用8或11。在pom.xml中添加必要的依赖。CXF的核心依赖包括cxf-rt-frontend-jaxws和cxf-rt-transports-http。这里有个坑需要注意网上很多教程会推荐使用cxf-spring-boot-starter-jaxws但在Spring Boot 2.2.5及以上版本中这个依赖会导致ClassNotFoundException异常。我亲自踩过这个坑所以建议大家直接使用原生CXF依赖。dependency groupIdorg.apache.cxf/groupId artifactIdcxf-rt-frontend-jaxws/artifactId version3.4.5/version /dependency dependency groupIdorg.apache.cxf/groupId artifactIdcxf-rt-transports-http/artifactId version3.4.5/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web-services/artifactId /dependency2. 定义Web Service接口我们先创建一个简单的用户服务接口作为示例。这个接口将提供两个方法一个简单的问候方法和一个获取用户列表的方法。在定义接口时需要使用WebService注解来标识这是一个Web Service接口。package com.example.service; import com.example.domain.User; import javax.jws.WebService; import java.util.List; WebService(name UserService, targetNamespace http://service.example.com) public interface UserService { String sayHello(String name); ListUser getUsers(); }这里有几个关键点需要注意name属性指定了服务名称这个会出现在WSDL中targetNamespace定义了命名空间通常使用倒置的包名接口中的方法默认都会暴露为Web Service操作3. 实现Web Service接口接下来我们实现这个接口。实现类同样需要使用WebService注解并且需要指定endpointInterface属性指向我们定义的接口。package com.example.service.impl; import com.example.domain.User; import com.example.service.UserService; import org.springframework.stereotype.Component; import javax.jws.WebService; import java.util.ArrayList; import java.util.List; WebService(serviceName UserService, targetNamespace http://service.example.com, endpointInterface com.example.service.UserService) Component public class UserServiceImpl implements UserService { Override public String sayHello(String name) { return Hello, name !; } Override public ListUser getUsers() { ListUser users new ArrayList(); users.add(new User(1L, 张三, 北京)); users.add(new User(2L, 李四, 上海)); return users; } }在实际项目中这里通常会注入DAO或其他服务来完成业务逻辑。我建议将业务逻辑和Web Service层分离这样代码更加清晰。4. 配置CXF服务端现在我们需要配置CXF来发布我们的Web Service。创建一个配置类主要做三件事注册CXF Servlet配置SpringBusCXF的核心总线发布Web Service端点package com.example.config; import com.example.service.UserService; import org.apache.cxf.Bus; import org.apache.cxf.bus.spring.SpringBus; import org.apache.cxf.jaxws.EndpointImpl; import org.apache.cxf.transport.servlet.CXFServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.xml.ws.Endpoint; Configuration public class CxfConfig { Bean public ServletRegistrationBeanCXFServlet cxfServlet() { return new ServletRegistrationBean(new CXFServlet(), /ws/*); } Bean(name Bus.DEFAULT_BUS_ID) public SpringBus springBus() { return new SpringBus(); } Bean public Endpoint endpoint(UserService userService) { EndpointImpl endpoint new EndpointImpl(springBus(), userService); endpoint.publish(/userService); return endpoint; } }这里有个特别需要注意的地方ServletRegistrationBean的方法名不能是dispatcherServlet()否则会与Spring Boot的自动配置冲突导致ErrorMvcAutoConfiguration错误。这是我踩过的另一个坑网上很多教程都没提到这点。5. 配置应用属性在application.properties或application.yml中添加基本配置server: port: 8080 spring: application: name: cxf-server6. 测试Web Service服务端启动应用后我们可以通过浏览器访问WSDL来验证服务是否发布成功。访问地址为http://localhost:8080/ws/userService?wsdl如果一切正常你会看到生成的WSDL文档。这个XML文档描述了我们的Web Service接口、操作和数据类型。对于客户端开发来说这个WSDL非常重要因为它是客户端了解服务契约的主要途径。7. 创建Web Service客户端现在我们来创建一个独立的客户端项目来调用这个Web Service。同样创建一个Spring Boot项目添加相同的CXF依赖。客户端有两种调用方式动态客户端和静态客户端。我们先介绍更简单的静态客户端方式它通过生成的存根类来调用服务。7.1 使用wsdl2java生成客户端代码CXF提供了wsdl2java工具可以根据WSDL生成客户端代码。在项目根目录下执行wsdl2java -d src/main/java -p com.example.client http://localhost:8080/ws/userService?wsdl这会在指定包下生成客户端代码。如果你没有安装wsdl2java可以使用Maven插件plugin groupIdorg.apache.cxf/groupId artifactIdcxf-codegen-plugin/artifactId version3.4.5/version executions execution idgenerate-sources/id phasegenerate-sources/phase configuration sourceRootsrc/main/java/sourceRoot wsdlOptions wsdlOption wsdlhttp://localhost:8080/ws/userService?wsdl/wsdl /wsdlOption /wsdlOptions /configuration goals goalwsdl2java/goal /goals /execution /executions /plugin7.2 配置客户端Bean创建一个配置类来配置Web Service客户端package com.example.config; import com.example.client.UserService; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class ClientConfig { Bean public UserService userService() { JaxWsProxyFactoryBean factory new JaxWsProxyFactoryBean(); factory.setServiceClass(UserService.class); factory.setAddress(http://localhost:8080/ws/userService); return (UserService) factory.create(); } }7.3 创建测试Controllerpackage com.example.controller; import com.example.client.UserService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController public class TestController { private final UserService userService; public TestController(UserService userService) { this.userService userService; } GetMapping(/hello) public String sayHello(RequestParam String name) { return userService.sayHello(name); } GetMapping(/users) public Object getUsers() { return userService.getUsers(); } }8. 动态客户端调用方式如果你不想生成客户端代码也可以使用动态客户端方式。这种方式更灵活但代码可读性稍差。package com.example.service; import org.apache.cxf.endpoint.Client; import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory; import org.springframework.stereotype.Service; Service public class DynamicClientService { public Object[] invoke(String operation, Object... params) throws Exception { JaxWsDynamicClientFactory dcf JaxWsDynamicClientFactory.newInstance(); Client client dcf.createClient(http://localhost:8080/ws/userService?wsdl); return client.invoke(operation, params); } }然后在Controller中注入这个服务即可调用远程方法。9. CXF与JAX-WS原生端点的对比在Java中除了CXF我们还可以使用JDK自带的JAX-WS实现来发布Web Service。两者主要区别如下配置方式CXF提供了更Spring友好的配置方式而JAX-WS需要更多手动配置功能丰富度CXF支持更多WS-*标准如WS-Security、WS-ReliableMessaging等性能CXF通常性能更好特别是在处理大量消息时拦截器机制CXF提供了强大的拦截器机制可以方便地扩展功能在实际项目中我推荐使用CXF因为它与Spring Boot集成更好功能也更强大。只有在非常简单的场景下才考虑使用JAX-WS原生实现。10. 常见问题与解决方案在整合CXF的过程中我遇到过不少问题这里分享几个常见的ClassNotFoundException: EmbeddedServletContainerCustomizer这是因为使用了不兼容的starter依赖解决方案是使用原生CXF依赖而非starter。WSDL访问404检查Servlet注册的路径和Endpoint发布的路径是否匹配确保完整URL正确。参数或返回值序列化问题确保你的DTO类实现了Serializable接口并且有无参构造函数。性能问题对于高并发场景可以考虑配置CXF的异步处理特性或者使用MTOM优化大文件传输。11. 安全配置在实际项目中Web Service通常需要安全保护。CXF提供了多种安全机制WS-Security可以通过配置拦截器来实现基本认证在客户端添加认证信息SSL/TLS配置HTTPS传输这里给出一个基本认证的配置示例Bean public UserService userService() { JaxWsProxyFactoryBean factory new JaxWsProxyFactoryBean(); factory.setServiceClass(UserService.class); factory.setAddress(http://localhost:8080/ws/userService); // 添加认证拦截器 MapString, Object props new HashMap(); props.put(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN); props.put(WSHandlerConstants.PASSWORD_TYPE, WSConstants.PW_TEXT); props.put(WSHandlerConstants.USER, admin); props.put(WSHandlerConstants.PW_CALLBACK_CLASS, ClientPasswordCallback.class.getName()); factory.setProperties(props); return (UserService) factory.create(); }需要实现一个CallbackHandler来处理密码验证public class ClientPasswordCallback implements CallbackHandler { public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback pc (WSPasswordCallback) callbacks[0]; pc.setPassword(password123); } }12. 日志与监控为了方便调试和监控我们可以配置CXF的日志拦截器Bean public LoggingFeature loggingFeature() { LoggingFeature feature new LoggingFeature(); feature.setPrettyLogging(true); return feature; } Bean public Endpoint endpoint(UserService userService, LoggingFeature loggingFeature) { EndpointImpl endpoint new EndpointImpl(springBus(), userService); endpoint.getFeatures().add(loggingFeature); endpoint.publish(/userService); return endpoint; }这样可以在控制台看到详细的SOAP消息日志对于调试非常有用。在生产环境中记得适当调整日志级别。