概述
Apache CXF 是一个完全符合 JAX-WS 标准的框架。除了 JAX-WS 标准定义的特性外,它还提供了 WSDL 与 Java 类的转换能力、操作原始 XML 消息的 API、JAX-RS 支持、Spring 框架集成等功能。
本教程是 Apache CXF 系列的第一篇,介绍框架的基础特性。源代码中仅使用 JAX-WS 标准 API,但底层仍利用了 Apache CXF 的能力(如自动生成 WSDL 元数据和 CXF 默认配置)。
Maven 依赖
使用 Apache CXF 的核心依赖是 org.apache.cxf:cxf-rt-frontend-jaxws
,它替代了 JDK 内置的 JAX-WS 实现:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>3.1.6</version>
</dependency>
注意:该依赖在 META-INF/services
目录下包含 javax.xml.ws.spi.Provider
文件。JVM 通过该文件首行确定使用的 JAX-WS 实现(此处为 org.apache.cxf.jaxws.spi.ProviderImpl
)。
本教程不使用 Servlet 容器发布服务,需添加以下依赖提供必要的 Java 类型定义:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-jetty</artifactId>
<version>3.1.6</version>
</dependency>
最新版本号可查阅 Maven 中央仓库。
Web 服务接口
首先看服务接口的实现类:
@WebService(endpointInterface = "com.baeldung.cxf.introduction.Baeldung")
public class BaeldungImpl implements Baeldung {
private Map<Integer, Student> students
= new LinkedHashMap<Integer, Student>();
public String hello(String name) {
return "Hello " + name;
}
public String helloStudent(Student student) {
students.put(students.size() + 1, student);
return "Hello " + student.getName();
}
public Map<Integer, Student> getStudents() {
return students;
}
}
关键点:@WebService
注解的 endpointInterface
属性指向了定义服务抽象契约的接口。虽然不要求实现接口,但这里仍实现了 Baeldung
接口以明确表示所有方法均已实现:
@WebService
public interface Baeldung {
public String hello(String name);
public String helloStudent(Student student);
@XmlJavaTypeAdapter(StudentMapAdapter.class)
public Map<Integer, Student> getStudents();
}
默认情况下,Apache CXF 使用 JAXB 作为数据绑定架构。但 JAXB 不直接支持 getStudents
方法返回的 Map
,需使用适配器将 Map
转换为 JAXB 可处理的类型。
此外,Student
被定义为接口,而 JAXB 也不直接支持接口,因此需要额外适配器处理。实际开发中可直接将 Student
声明为类,此处使用接口仅为演示适配器的用法。
适配器实现见下节。
自定义适配器
本节展示如何通过适配器支持 JAXB 对接口和 Map
的绑定。
接口适配器
Student
接口定义如下:
@XmlJavaTypeAdapter(StudentAdapter.class)
public interface Student {
public String getName();
}
该接口仅声明一个返回 String
的方法,并通过 @XmlJavaTypeAdapter
指定适配器类 StudentAdapter
。
适配器类实现如下:
public class StudentAdapter extends XmlAdapter<StudentImpl, Student> {
public StudentImpl marshal(Student student) throws Exception {
if (student instanceof StudentImpl) {
return (StudentImpl) student;
}
return new StudentImpl(student.getName());
}
public Student unmarshal(StudentImpl student) throws Exception {
return student;
}
}
适配器需实现 XmlAdapter
接口并重写 marshal
和 unmarshal
方法:
marshal
:将 JAXB 无法直接处理的绑定类型(Student
接口)转换为值类型(StudentImpl
实体类)unmarshal
:执行反向转换
StudentImpl
类定义:
@XmlType(name = "Student")
public class StudentImpl implements Student {
private String name;
// 构造方法、getter/setter
}
Map 适配器
Baeldung
接口的 getStudents
方法返回 Map
,需适配器转换为 JAXB 可处理的类型。适配器实现如下:
public class StudentMapAdapter
extends XmlAdapter<StudentMap, Map<Integer, Student>> {
public StudentMap marshal(Map<Integer, Student> boundMap) throws Exception {
StudentMap valueMap = new StudentMap();
for (Map.Entry<Integer, Student> boundEntry : boundMap.entrySet()) {
StudentMap.StudentEntry valueEntry = new StudentMap.StudentEntry();
valueEntry.setStudent(boundEntry.getValue());
valueEntry.setId(boundEntry.getKey());
valueMap.getEntries().add(valueEntry);
}
return valueMap;
}
public Map<Integer, Student> unmarshal(StudentMap valueMap) throws Exception {
Map<Integer, Student> boundMap = new LinkedHashMap<Integer, Student>();
for (StudentMap.StudentEntry studentEntry : valueMap.getEntries()) {
boundMap.put(studentEntry.getId(), studentEntry.getStudent());
}
return boundMap;
}
}
StudentMapAdapter
将 Map<Integer, Student>
与 StudentMap
值类型相互转换。StudentMap
定义如下:
@XmlType(name = "StudentMap")
public class StudentMap {
private List<StudentEntry> entries = new ArrayList<StudentEntry>();
@XmlElement(nillable = false, name = "entry")
public List<StudentEntry> getEntries() {
return entries;
}
@XmlType(name = "StudentEntry")
public static class StudentEntry {
private Integer id;
private Student student;
// getter/setter
}
}
部署
服务器定义
使用标准 JAX-WS API 部署服务。底层 Apache CXF 会额外处理 WSDL 生成和发布:
public class Server {
public static void main(String args[]) throws InterruptedException {
BaeldungImpl implementor = new BaeldungImpl();
String address = "http://localhost:8080/baeldung";
Endpoint.publish(address, implementor);
Thread.sleep(60 * 1000);
System.exit(0);
}
}
服务运行一段时间后需关闭以释放资源。可通过修改 Thread.sleep
的参数调整运行时长。
服务器的部署
使用 exec-maven-plugin
插件启动服务并控制生命周期。在 POM 中添加:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>com.baeldung.cxf.introduction.Server</mainClass>
</configuration>
</plugin>
mainClass
指向发布服务接口的 Server
类。运行 exec:java
目标后,可通过 http://localhost:8080/baeldung?wsdl
访问 Apache CXF 自动生成的 WSDL。
测试用例
本节展示如何编写测试验证服务功能。注意:运行测试前需先执行 exec:java
启动服务。
准备工作
首先在测试类中声明字段:
public class StudentTest {
private static QName SERVICE_NAME
= new QName("http://introduction.cxf.baeldung.com/", "Baeldung");
private static QName PORT_NAME
= new QName("http://introduction.cxf.baeldung.com/", "BaeldungPort");
private Service service;
private Baeldung baeldungProxy;
private BaeldungImpl baeldungImpl;
// 其他声明
}
通过初始化块创建 Service
实例:
{
service = Service.create(SERVICE_NAME);
String endpointAddress = "http://localhost:8080/baeldung";
service.addPort(PORT_NAME, SOAPBinding.SOAP11HTTP_BINDING, endpointAddress);
}
使用 @Before
注解的方法在每次测试前重新初始化 Baeldung
实例:
@Before
public void reinstantiateBaeldungInstances() {
baeldungImpl = new BaeldungImpl();
baeldungProxy = service.getPort(PORT_NAME, Baeldung.class);
}
baeldungProxy
是服务接口的代理对象,baeldungImpl
是本地 Java 对象(用于对比远程调用与本地调用的结果)。
踩坑提示:
QName
由命名空间 URI 和本地部分组成。若省略Service.getPort
的PORT_NAME
参数,CXF 会自动生成(命名空间为接口包名的反转,本地部分为接口名+Port
)。本例中可省略该参数。
测试实现
第一个测试验证 hello
方法的远程调用:
@Test
public void whenUsingHelloMethod_thenCorrect() {
String endpointResponse = baeldungProxy.hello("Baeldung");
String localResponse = baeldungImpl.hello("Baeldung");
assertEquals(localResponse, endpointResponse);
}
远程调用与本地调用结果一致,说明服务正常。
第二个测试验证 helloStudent
方法:
@Test
public void whenUsingHelloStudentMethod_thenCorrect() {
Student student = new StudentImpl("John Doe");
String endpointResponse = baeldungProxy.helloStudent(student);
String localResponse = baeldungImpl.helloStudent(student);
assertEquals(localResponse, endpointResponse);
}
客户端提交 Student
对象后收到包含学生姓名的响应,远程与本地调用结果相同。
第三个测试验证 getStudents
方法(更复杂):
@Test
public void usingGetStudentsMethod_thenCorrect() {
Student student1 = new StudentImpl("Adam");
baeldungProxy.helloStudent(student1);
Student student2 = new StudentImpl("Eve");
baeldungProxy.helloStudent(student2);
Map<Integer, Student> students = baeldungProxy.getStudents();
assertEquals("Adam", students.get(1).getName());
assertEquals("Eve", students.get(2).getName());
}
每次调用 helloStudent
会将学生对象存入缓存。测试验证缓存内容与客户端提交数据一致。
总结
本教程介绍了 Apache CXF 框架的基础用法,重点展示了其作为标准 JAX-WS 实现的应用,同时利用了运行时框架特性(如自动生成 WSDL)。所有示例代码可在 GitHub 项目 中获取。