Contents

Apache CXF支持RESTFUL WEB服务

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 创建服务器端点来发布 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;
    // standard getters and setters
    // standard equals and hashCode implementations
}

请注意,我们使用*@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;
    }
    // standard getters and setters
    // standard equals and hasCode implementations
    
}

最后,让我们实现CourseRepository——它是根资源并作为 Web 服务资源的入口点:

@Path("course")
@Produces("text/xml")
public class CourseRepository {
    private Map<Integer, Course> courses = new HashMap<>();
    // request handling methods
    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 项目中的方法有很大不同——API 操作将在控制器中定义,而不是在 POJO 本身上。

让我们从Course类中定义的映射方法开始:

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

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

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

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

以下方法通过将接收到的Student对象添加到Student列表来处理由*@POST注释指示的POST*请求:

@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请求。它从学生列表中删除一个元素,其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对象,该对象是Course映射中条目的值,其键是接收到的GET请求的courseId路径参数。在内部,该方法将路径参数分派给findById辅助方法以完成其工作。

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

以下方法更新课程地图的现有条目,其中接收到的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) 响应。如果在Course地图中找不到具有给定IDCourse实例,该方法将返回具有未找到(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 {
        // code snippets shown above
    }
}

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

5. 测试用例

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

5.1. 准备

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

private static String BASE_URL = "http://localhost:8080/blogdemo/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对象。由于在这种情况下没有任何更改,我们希望返回未修改(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());

其中未changed_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.0.0<version>
    <configuration>
        <mainClass>
          com.blogdemo.cxf.jaxrs.implementation.RestfulServer
        </mainClass>
    </configuration>
</plugin>

这个插件的最新版本可以通过这个链接 找到。

在编译和打包本教程中说明的工件的过程中,Maven Surefire 插件会自动执行包含在名称以 Test 开头或结尾的类中的所有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来开始测试。