1. 概述

本教程介绍 Apache CXF 作为符合 JAX-RS 标准的框架,该标准定义了 Java 生态系统对 REpresentational State Transfer (REST) 架构模式的支持。

具体来说,我们将逐步讲解如何构建和发布 RESTful Web 服务,以及如何编写单元测试来验证服务。

这是 Apache CXF 系列教程的第三篇;第一篇 侧重于将 CXF 作为完全符合 JAX-WS 的实现使用。第二篇文章 提供了如何将 CXF 与 Spring 结合使用的指南。

2. Maven 依赖

第一个必需依赖是 org.apache.cxf:cxf-rt-frontend-jaxrs。该工件提供了 JAX-RS API 以及 CXF 实现:

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-frontend-jaxrs</artifactId>
    <version>3.1.7</version>
</dependency>

在本教程中,我们使用 CXF 创建一个 Server 接口来发布 Web 服务,而不是使用 servlet 容器。因此,需要在 Maven POM 文件中包含以下依赖:

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

最后,添加 HttpClient 库以简化单元测试:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.2</version>
</dependency>

这里 可以找到 cxf-rt-frontend-jaxrs 依赖的最新版本。org.apache.cxf:cxf-rt-transports-http-jetty 工件的最新版本可参考此链接。最后,httpclient 的最新版本可在这里找到。

3. 资源类与请求映射

让我们开始实现一个简单示例;我们将使用两个资源 CourseStudent 来设置 REST API。

我们从简单开始,逐步构建更复杂的示例。

3.1. 资源定义

以下是 Student 资源类的定义:

@XmlRootElement(name = "Student")
public class Student {
    private int id;
    private String name;

    // 标准的 getter 和 setter
    // 标准的 equals 和 hashCode 实现
}

注意我们使用 @XmlRootElement 注解告诉 JAXB 该类的实例应被序列化为 XML。

接下来是 Course 资源类的定义:

@XmlRootElement(name = "Course")
public class Course {
    private int id;
    private String name;
    private List<Student> students = new ArrayList<>();

    private Student findById(int id) {
        for (Student student : students) {
            if (student.getId() == id) {
                return student;
            }
        }
        return null;
    }
    // 标准的 getter 和 setter
    // 标准的 equals 和 hashCode 实现
}

最后,实现 CourseRepository——这是根资源,作为 Web 服务资源的入口点:

@Path("course")
@Produces("text/xml")
public class CourseRepository {
    private Map<Integer, Course> courses = new HashMap<>();

    // 请求处理方法

    private Course findById(int id) {
        for (Map.Entry<Integer, Course> course : courses.entrySet()) {
            if (course.getKey() == id) {
                return course.getValue();
            }
        }
        return null;
    }
}

注意 @Path 注解的映射。CourseRepository 是根资源,因此映射到处理所有以 course 开头的 URL。

@Produces 注解的值用于告诉服务器,在将此类中方法返回的对象发送给客户端之前,将其转换为 XML 文档。这里我们使用 JAXB 作为默认机制,因为没有指定其他绑定机制。

3.2. 简单数据初始化

由于这是一个简单的示例实现,我们使用内存数据而非完整的持久化解决方案。

基于此,让我们实现一些简单的初始化逻辑来填充系统数据:

{
    Student student1 = new Student();
    Student student2 = new Student();
    student1.setId(1);
    student1.setName("Student A");
    student2.setId(2);
    student2.setName("Student B");

    List<Student> course1Students = new ArrayList<>();
    course1Students.add(student1);
    course1Students.add(student2);

    Course course1 = new Course();
    Course course2 = new Course();
    course1.setId(1);
    course1.setName("REST with Spring");
    course1.setStudents(course1Students);
    course2.setId(2);
    course2.setName("Learn Spring Security");

    courses.put(1, course1);
    courses.put(2, course2);
}

处理 HTTP 请求的方法将在下一小节介绍。

3.3. API – 请求映射方法

现在,让我们实现实际的 REST API。

我们将开始在资源 POJO 中添加 API 操作——使用 @Path 注解。

需要理解的是,这与典型 Spring 项目中的方法有显著不同——在 Spring 项目中,API 操作会在控制器中定义,而不是在 POJO 本身上。

先从 Course 类中定义的映射方法开始:

@GET
@Path("{studentId}")
public Student getStudent(@PathParam("studentId")int studentId) {
    return findById(studentId);
}

简单来说,该方法在处理 GET 请求时被调用,由 @GET 注解表示。

注意从 HTTP 请求中映射 studentId 路径参数的简单语法。

然后我们简单地使用 findById 辅助方法返回对应的 Student 实例。

以下方法处理 POST 请求(由 @POST 注解指示),通过将接收到的 Student 对象添加到 students 列表:

@POST
@Path("")
public Response createStudent(Student student) {
    for (Student element : students) {
        if (element.getId() == student.getId()) {
            return Response.status(Response.Status.CONFLICT).build();
        }
    }
    students.add(student);
    return Response.ok(student).build();
}

如果创建操作成功,返回 200 OK 响应;如果已存在具有提交 id 的对象,则返回 409 Conflict

另请注意,我们可以省略 @Path 注解,因为其值为空字符串。

最后一个方法处理 DELETE 请求。它从 students 列表中移除 id 为接收到的路径参数的元素,并返回状态为 OK (200) 的响应。如果没有与指定 id 关联的元素(意味着没有可删除的内容),该方法返回状态为 Not Found (404) 的响应:

@DELETE
@Path("{studentId}")
public Response deleteStudent(@PathParam("studentId") int studentId) {
    Student student = findById(studentId);
    if (student == null) {
        return Response.status(Response.Status.NOT_FOUND).build();
    }
    students.remove(student);
    return Response.ok().build();
}

让我们继续 CourseRepository 类的请求映射方法。

以下 getCourse 方法返回一个 Course 对象,该对象是 courses 映射中条目的值,其键是接收到的 GET 请求的 courseId 路径参数。内部该方法将路径参数分派给 findById 辅助方法执行操作:

@GET
@Path("courses/{courseId}")
public Course getCourse(@PathParam("courseId") int courseId) {
    return findById(courseId);
}

以下方法更新 courses 映射中的现有条目,其中接收到的 PUT 请求的正文是条目值,courseId 参数是关联的键:

@PUT
@Path("courses/{courseId}")
public Response updateCourse(@PathParam("courseId") int courseId, Course course) {
    Course existingCourse = findById(courseId);        
    if (existingCourse == null) {
        return Response.status(Response.Status.NOT_FOUND).build();
    }
    if (existingCourse.equals(course)) {
        return Response.notModified().build();    
    }
    courses.put(courseId, course);
    return Response.ok().build();
}

updateCourse 方法在更新成功时返回状态为 OK (200) 的响应;如果现有对象和上传对象具有相同的字段值,则不做任何更改并返回 Not Modified (304) 响应。如果在 courses 映射中找不到给定 idCourse 实例,该方法返回状态为 Not Found (404) 的响应。

此根资源类的第三个方法不直接处理任何 HTTP 请求。相反,它将请求委托给 Course 类,在那里请求由匹配的方法处理:

@Path("courses/{courseId}/students")
public Course pathToStudent(@PathParam("courseId") int courseId) {
    return findById(courseId);
}

我们刚刚展示了 Course 类中处理委托请求的方法。

4. Server 接口

本节重点介绍 CXF 服务器的构建,用于发布上一节描述资源的 RESTful Web 服务。第一步是实例化 JAXRSServerFactoryBean 对象并设置根资源类:

JAXRSServerFactoryBean factoryBean = new JAXRSServerFactoryBean();
factoryBean.setResourceClasses(CourseRepository.class);

然后需要在工厂 bean 上设置资源提供者,以管理根资源类的生命周期。我们使用默认的单例资源提供者,它为每个请求返回相同的资源实例:

factoryBean.setResourceProvider(
  new SingletonResourceProvider(new CourseRepository()));

我们还设置一个地址来指示发布 Web 服务的 URL:

factoryBean.setAddress("http://localhost:8080/");

现在 factoryBean 可用于创建一个新的 server,它将开始监听传入的连接:

Server server = factoryBean.create();

本节中的所有上述代码应包装在 main 方法中:

public class RestfulServer {
    public static void main(String args[]) throws Exception {
        // 上面显示的代码片段
    }
}

main 方法的调用将在第 6 节中介绍。

5. 测试用例

本节描述用于验证我们之前创建的 Web 服务的测试用例。这些测试验证服务在响应四种最常用方法(即 GETPOSTPUTDELETE)的 HTTP 请求后的资源状态。

5.1. 准备工作

首先,在名为 RestfulTest 的测试类中声明两个静态字段:

private static String BASE_URL = "http://localhost:8080/baeldung/courses/";
private static CloseableHttpClient client;

在运行测试之前,我们创建一个 client 对象用于与服务器通信,并在之后销毁它:

@BeforeClass
public static void createClient() {
    client = HttpClients.createDefault();
}
    
@AfterClass
public static void closeClient() throws IOException {
    client.close();
}

client 实例现在已准备好供测试用例使用。

5.2. GET 请求

在测试类中,我们定义两个方法向运行 Web 服务的服务器发送 GET 请求。

第一个方法是根据资源中的 id 获取 Course 实例:

private Course getCourse(int courseOrder) throws IOException {
    URL url = new URL(BASE_URL + courseOrder);
    InputStream input = url.openStream();
    Course course
      = JAXB.unmarshal(new InputStreamReader(input), Course.class);
    return course;
}

第二个方法是根据资源和课程/学生的 id 获取 Student 实例:

private Student getStudent(int courseOrder, int studentOrder)
  throws IOException {
    URL url = new URL(BASE_URL + courseOrder + "/students/" + studentOrder);
    InputStream input = url.openStream();
    Student student
      = JAXB.unmarshal(new InputStreamReader(input), Student.class);
    return student;
}

这些方法向服务资源发送 HTTP GET 请求,然后将 XML 响应反序列化为相应类的实例。两者都用于在执行 POSTPUTDELETE 请求后验证服务资源状态。

5.3. POST 请求

本小节包含两个 POST 请求的测试用例,说明当上传的 Student 实例导致冲突以及成功创建时 Web 服务的操作。

在第一个测试中,我们使用从类路径中 conflict_student.xml 文件反序列化的 Student 对象,其内容如下:

<Student>
    <id>2</id>
    <name>Student B</name>
</Student>

以下代码将该内容转换为 POST 请求正文:

HttpPost httpPost = new HttpPost(BASE_URL + "1/students");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("conflict_student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));

设置 Content-Type 头以告知服务器请求的内容类型为 XML:

httpPost.setHeader("Content-Type", "text/xml");

由于上传的 Student 对象已存在于第一个 Course 实例中,我们预期创建失败并返回状态为 Conflict (409) 的响应。以下代码片段验证此预期:

HttpResponse response = client.execute(httpPost);
assertEquals(409, response.getStatusLine().getStatusCode());

在下一个测试中,我们从名为 created_student.xml 的文件中提取 HTTP 请求正文,该文件也在类路径中。以下是文件内容:

<Student>
    <id>3</id>
    <name>Student C</name>
</Student>

与前一个测试用例类似,我们构建并执行请求,然后验证新实例是否成功创建:

HttpPost httpPost = new HttpPost(BASE_URL + "2/students");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("created_student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));
httpPost.setHeader("Content-Type", "text/xml");
        
HttpResponse response = client.execute(httpPost);
assertEquals(200, response.getStatusLine().getStatusCode());

我们可以确认 Web 服务资源的新状态:

Student student = getStudent(2, 3);
assertEquals(3, student.getId());
assertEquals("Student C", student.getName());

这是对新 Student 对象请求的 XML 响应示例:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Student>
    <id>3</id>
    <name>Student C</name>
</Student>

5.4. PUT 请求

先从一个无效的更新请求开始,其中要更新的 Course 对象不存在。以下是用于替换 Web 服务资源中不存在的 Course 对象的实例内容:

<Course>
    <id>3</id>
    <name>Apache CXF Support for RESTful</name>
</Course>

该内容存储在类路径中名为 non_existent_course.xml 的文件中。它被提取并用于通过以下代码填充 PUT 请求的正文:

HttpPut httpPut = new HttpPut(BASE_URL + "3");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("non_existent_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));

设置 Content-Type 头以告知服务器请求的内容类型为 XML:

httpPut.setHeader("Content-Type", "text/xml");

由于我们故意发送了一个无效请求来更新不存在的对象,预期会收到状态为 Not Found (404) 的响应。验证响应:

HttpResponse response = client.execute(httpPut);
assertEquals(404, response.getStatusLine().getStatusCode());

PUT 请求的第二个测试用例中,我们提交一个具有相同字段值的 Course 对象。由于此情况下没有任何更改,我们预期返回状态为 Not Modified (304) 的响应。整个过程说明如下:

HttpPut httpPut = new HttpPut(BASE_URL + "1");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("unchanged_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
httpPut.setHeader("Content-Type", "text/xml");
        
HttpResponse response = client.execute(httpPut);
assertEquals(304, response.getStatusLine().getStatusCode());

其中 unchanged_course.xml 是类路径中保存更新信息的文件。其内容如下:

<Course>
    <id>1</id>
    <name>REST with Spring</name>
</Course>

PUT 请求的最后一个演示中,我们执行有效的更新。以下是 changed_course.xml 文件的内容,用于更新 Web 服务资源中的 Course 实例:

<Course>
    <id>2</id>
    <name>Apache CXF Support for RESTful</name>
</Course>

请求的构建和执行方式如下:

HttpPut httpPut = new HttpPut(BASE_URL + "2");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("changed_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
httpPut.setHeader("Content-Type", "text/xml");

验证向服务器发送的 PUT 请求并确认上传成功:

HttpResponse response = client.execute(httpPut);
assertEquals(200, response.getStatusLine().getStatusCode());

验证 Web 服务资源的新状态:

Course course = getCourse(2);
assertEquals(2, course.getId());
assertEquals("Apache CXF Support for RESTful", course.getName());

以下代码片段显示了对先前上传的 Course 对象发送 GET 请求时收到的 XML 响应内容:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Course>
    <id>2</id>
    <name>Apache CXF Support for RESTful</name>
</Course>

5.5. DELETE 请求

首先,尝试删除不存在的 Student 实例。操作应失败,预期返回状态为 Not Found (404) 的响应:

HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/3");
HttpResponse response = client.execute(httpDelete);
assertEquals(404, response.getStatusLine().getStatusCode());

DELETE 请求的第二个测试用例中,我们创建、执行并验证请求:

HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/1");
HttpResponse response = client.execute(httpDelete);
assertEquals(200, response.getStatusLine().getStatusCode());

使用以下代码片段验证 Web 服务资源的新状态:

Course course = getCourse(1);
assertEquals(1, course.getStudents().size());
assertEquals(2, course.getStudents().get(0).getId());
assertEquals("Student B", course.getStudents().get(0).getName());

接下来,我们列出对 Web 服务资源中第一个 Course 对象请求后收到的 XML 响应:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Course>
    <id>1</id>
    <name>REST with Spring</name>
    <students>
        <id>2</id>
        <name>Student B</name>
    </students>
</Course>

显然,第一个 Student 已被成功移除。

6. 测试执行

第 4 节描述了如何在 RestfulServer 类的 main 方法中创建和销毁 Server 实例。

使服务器启动并运行的最后一步是调用该 main 方法。为此,在 Maven POM 文件中包含并配置 Exec Maven 插件:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <mainClass>
          com.baeldung.cxf.jaxrs.implementation.RestfulServer
        </mainClass>
    </configuration>
</plugin>

此插件的最新版本可通过此链接找到。

在编译和打包本教程中描述的工件的过程中,Maven Surefire 插件会自动执行所有名称以 Test 开头或结尾的类中包含的测试。如果是这种情况,应配置插件排除这些测试:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version>
    <configuration>
    <excludes>
        <exclude>**/ServiceTest</exclude>
    </excludes>
    </configuration>
</plugin>

通过上述配置,ServiceTest 被排除,因为它是测试类的名称。您可以为该类选择任何名称,只要其中包含的测试在服务器准备好连接之前不被 Maven Surefire 插件运行即可。

Maven Surefire 插件的最新版本请查看这里

现在您可以执行 exec:java 目标来启动 RESTful Web 服务服务器,然后使用 IDE 运行上述测试。或者,您可以通过在终端中执行命令 mvn -Dtest=ServiceTest test 来启动测试。

7. 结论

本教程演示了将 Apache CXF 用作 JAX-RS 实现的方法。展示了如何使用该框架定义 RESTful Web 服务的资源,以及创建用于发布服务的服务器。

所有这些示例和代码片段的实现可以在 GitHub 项目 中找到。


原始标题:Apache CXF Support for RESTful Web Services