概述

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 接口并重写 marshalunmarshal 方法:

  • 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;
    }
}

StudentMapAdapterMap<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.getPortPORT_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 项目 中获取。