Contents

Apache CXF简介

1. 概述

Apache CXF 是一个完全兼容 JAX-WS 的框架。

在 JAX-WS 标准定义的特性之上,Apache CXF 提供了 WSDL 和 Java 类之间的转换能力、用于操作原始 XML 消息的 API、对 JAX-RS 的支持、与 Spring 框架的集成等。

本教程是 Apache CXF 系列的第一篇,介绍了该框架的基本特征。它仅在源代码中使用 JAX-WS 标准 API,同时仍然在幕后利用 Apache CXF,例如自动生成的 WSDL 元数据和 CXF 默认配置。

2. Maven依赖

使用 Apache CXF 所需的关键依赖项是org.apache.cxf:cxf–rt–frontend–jaxws。这提供了一个 JAX-WS 实现来替换内置的 JDK:

<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的文件。Java VM 查看此文件的第一行以确定要使用的 JAX-WS 实现。在这种情况下,该行的内容是org.apache.cxf.jaxws.spi.ProviderImpl,指的是 Apache CXF 提供的实现。

在本教程中,我们不使用 servlet 容器来发布服务,因此需要另一个依赖项来提供必要的 Java 类型定义:

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-transports-http-jetty</artifactId>
    <version>3.1.6</version>
</dependency>

有关这些依赖项的最新版本,请查看Maven 中央存储库中的cxf-rt-frontend-jaxwscxf-rt-transports-http-jetty

3. Web服务端点

让我们从用于配置服务端点的实现类开始:

@WebService(endpointInterface = "com.blogdemo.cxf.introduction.Blogdemo")
public class BlogdemoImpl implements Blogdemo {
    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*属性。此属性指向定义 Web 服务的抽象契约的接口。

端点接口中声明的所有方法签名都需要实现,但不需要实现接口。

这里BlogdemoImpl实现类仍然实现了如下端点接口,以明确接口声明的所有方法都已实现,但这样做是可选的:

@WebService
public interface Blogdemo {
    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 可以使用的 Java 类

此外,为了将契约元素与其实现分离,我们将Student定义为一个接口,而 JAXB 也不直接支持接口,因此我们需要多一个适配器来处理这个问题。事实上,为了方便,我们可以将Student声明为一个类。使用这种类型作为接口只是对使用适配类的又一次演示。

适配器在下面的部分中演示。

4. 自定义适配器

本节说明了使用适配类来支持使用 JAXB绑定 Java 接口和Map的方法。

4.1. 接口适配器

Student接口是这样定义的:

@XmlJavaTypeAdapter(StudentAdapter.class)
public interface Student {
    public String getName();
}

此接口仅声明一个返回String的方法,并将StudentAdapter指定为适配类,以将自身映射到可以应用 JAXB 绑定的类型或从该类型映射。

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方法将绑定类型(Student,JAXB 无法直接处理的接口)转换为值类型(StudentImpl,可以由 JAXB 处理的具体类)。unmarshal方法以相反的方式做事。

这是StudentImpl类定义:

@XmlType(name = "Student")
public class StudentImpl implements Student {
    private String name;
    // constructors, getter and setter
}

4.2. Map适配器

Blogdemo端点接口的getStudents方法返回一个Map并指示一个适配类,用于将Map转换为 JAXB 可以处理的类型。与StudentAdapter类类似,这个适配类必须实现XmlAdapter接口的marshalunmarshal方法:

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;
        // getters and setters
    }
}

5. 部署

5.1. Server定义

为了部署上面讨论的 Web 服务,我们将使用标准的 JAX-WS API。因为我们使用的是 Apache CXF,所以框架会做一些额外的工作,例如生成和发布 WSDL 模式。以下是服务服务器的定义方式:

public class Server {
    public static void main(String args[]) throws InterruptedException {
        BlogdemoImpl implementor = new BlogdemoImpl();
        String address = "http://localhost:8080/blogdemo";
        Endpoint.publish(address, implementor);
        Thread.sleep(60 * 1000);        
        System.exit(0);
    }
}

在服务器处于活动状态一段时间后,为了方便测试,应该将其关闭以释放系统资源。您可以通过将长参数传递给Thread.sleep方法,根据需要为服务器指定任何工作持续时间。

5.2. Server的部署

在本教程中,我们使用org.codehaus.mojo: exec -maven-plugin插件来实例化上述服务器并控制其生命周期。这在 Maven POM 文件中声明如下:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <configuration>
        <mainClass>com.blogdemo.cxf.introduction.Server</mainClass>
    </configuration>
</plugin>

mainClass配置是指发布 Web 服务端点的Server类。运行此插件的java目标后,我们可以通过访问 URL http://localhost:8080/blogdemo?wsdl 查看 Apache CXF 自动生成的 WSDL 模式。

6. 测试用例

本节将引导您完成编写用于验证我们之前创建的 Web 服务的测试用例的步骤。

请注意,在运行任何测试之前,我们需要执行exec:java 目标来启动 Web 服务服务器。

6.1. 准备

第一步是为测试类声明几个字段:

public class StudentTest {
    private static QName SERVICE_NAME 
      = new QName("http://introduction.cxf.blogdemo.com/", "Blogdemo");
    private static QName PORT_NAME 
      = new QName("http://introduction.cxf.blogdemo.com/", "BlogdemoPort");
    private Service service;
    private Blogdemo blogdemoProxy;
    private BlogdemoImpl blogdemoImpl;
    // other declarations
}

以下初始化程序块用于在运行任何测试之前启动javax.xml.ws.Service类型的Service字段:

{
    service = Service.create(SERVICE_NAME);
    String endpointAddress = "http://localhost:8080/blogdemo";
    service.addPort(PORT_NAME, SOAPBinding.SOAP11HTTP_BINDING, endpointAddress);
}

将 JUnit 依赖项添加到 POM 文件后,我们可以使用*@Before注解,如下面的代码片段所示。此方法在每次测试之前运行以重新实例化Blogdemo*字段:

@Before
public void reinstantiateBlogdemoInstances() {
    blogdemoImpl = new BlogdemoImpl();
    blogdemoProxy = service.getPort(PORT_NAME, Blogdemo.class);
}

blogdemoProxy变量是 Web 服务端点的代理,而blogdemoImpl只是一个简单的 Java 对象。该对象用于比较通过代理调用远程端点方法的结果与调用本地方法的结果。

请注意,QName实例由两部分标识:命名空间 URI 和本地部分。如果省略Service.getPort方法的QName类型的PORT_NAME参数,则 Apache CXF 将假定参数的 Namespace URI 是端点接口的包名,顺序相反,其本地部分是端口附加的接口名,与PORT_NAME的值完全相同。因此,在本教程中,我们可能会忽略这个论点。

6.2. 测试实施

我们在本小节中说明的第一个测试用例是验证从服务端点上的hello方法的远程调用返回的响应:

@Test
public void whenUsingHelloMethod_thenCorrect() {
    String endpointResponse = blogdemoProxy.hello("Blogdemo");
    String localResponse = blogdemoImpl.hello("Blogdemo");
    assertEquals(localResponse, endpointResponse);
}

很明显,远程端点方法返回与本地方法相同的响应,这意味着 Web 服务按预期工作。

下一个测试用例演示了helloStudent方法的使用:

@Test
public void whenUsingHelloStudentMethod_thenCorrect() {
    Student student = new StudentImpl("John Doe");
    String endpointResponse = blogdemoProxy.helloStudent(student);
    String localResponse = blogdemoImpl.helloStudent(student);
    assertEquals(localResponse, endpointResponse);
}

在这种情况下,客户端向端点提交一个Student对象,并收到一条包含学生姓名的消息作为回报。与前面的测试用例一样,来自远程和本地调用的响应是相同的。

我们在这里展示的最后一个测试用例更复杂。正如服务端点实现类所定义的,每次客户端调用端点上的helloStudent方法时,提交的Student对象都会存储在缓存中。可以通过调用端点上的getStudents方法来检索此缓存。以下测试用例确认Student缓存的内容代表客户端发送到 Web 服务的内容:

@Test
public void usingGetStudentsMethod_thenCorrect() {
    Student student1 = new StudentImpl("Adam");
    blogdemoProxy.helloStudent(student1);
    Student student2 = new StudentImpl("Eve");
    blogdemoProxy.helloStudent(student2);

    Map<Integer, Student> students = blogdemoProxy.getStudents();
    assertEquals("Adam", students.get(1).getName());
    assertEquals("Eve", students.get(2).getName());
}