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. 资源类与请求映射
让我们开始实现一个简单示例;我们将使用两个资源 Course
和 Student
来设置 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
映射中找不到给定 id
的 Course
实例,该方法返回状态为 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 服务的测试用例。这些测试验证服务在响应四种最常用方法(即 GET
、POST
、PUT
和 DELETE
)的 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 响应反序列化为相应类的实例。两者都用于在执行 POST
、PUT
和 DELETE
请求后验证服务资源状态。
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 项目 中找到。