Apache CXF 對 RESTful Web 服務支持

REST
Remote
0
02:42 PM · Dec 01 ,2025

1. 概述

本教程介紹 Apache CXF 作為符合 JAX-RS 標準的框架,該框架為 Java 生態系統定義了用於 REST 架構模式的支持。

它詳細描述瞭如何逐步構建和發佈 RESTful Web 服務,以及如何編寫單元測試以驗證服務。

這是 Apache CXF 系列中的第三篇,前兩篇分別側重於將 CXF 用作符合 JAX-WS 的完全實現,以及如何使用 CXF 與 Spring。

2. Maven 依賴項

第一個必需的依賴項是 org.apache.cxf:cxf-rt-frontend-jaxrs。 此 Artifact 提供 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. Resource Classes and Request Mapping

Let’s start implementing a simple example; we’re going to set up our REST API with two resources Course and Student.

We’ll start simple and move towards a more complex example as we go.

3.1. The Resources

Here is the definition of the Student resource class:

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

    // standard getters and setters
    // standard equals and hashCode implementations

}

Notice we’re using the @XmlRootElement annotation to tell JAXB that instances of this class should be marshaled to XML.

Next, comes the definition of the Course resource class:

@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

Finally, let’s implement theCourseRepository – which is the root resource and serves as the entry point to web service resources:

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

Notice the mapping with the @Path annotation. The CourseRepository is the root resource here, so it’s mapped to handle all URLS starting with course.

The value of @Produces annotation is used to tell the server to convert objects returned from methods within this class to XML documents before sending them to clients. We’re using JAXB here as the default since no other binding mechanisms are specified.

3.2. Simple Data Setup

Because this is a simple example implementation, we’re using in-memory data instead of a full-fledged persistent solution.

With that in mind, let’s implement some simple setup logic to populate some data into the system:

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);

Methods within this class that take care of HTTP requests are covered in the next subsection.

3.3. The API – Request Mapping Methods

Now, let’s go to the implementation of the actual REST API.

We’re going to start adding API operations – using the @Path annotation – right in the resource POJOs.

It’s important to understand that is a significant difference from the approach in a typical Spring project – where the API operations would be defined in a controller, not on the POJO itself.

Let’s start with mapping methods defined inside the Course class:

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

Simply put, the method is invoked when handling GET requests, denoted by the @GET annotation.

Noticed the simple syntax of mapping the studentId path parameter from the HTTP request.

We’re then simply using the findById helper method to return the corresponding Student instance.

The following method handles POST requests, indicated by the @POST annotation, by adding the received Student object to the students list:

@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();
}

This returns a 200 OK response if the create operation was successful, or 409 Conflict if an object with the submitted id is already existent.

Also note that we can skip the @Path annotation since its value is an empty String.

The last method takes care of DELETE requests. It removes an element from the students list whose id is the received path parameter and returns a response with OK (200) status. In case there are no elements associated with the specified id, which implies there is nothing to be removed, this method returns a response with Not Found (404) status:

@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();
}

Let’s move on to request mapping methods of the CourseRepository class.

The following getCourse method returns a Course object that is the value of an entry in the courses map whose key is the received courseId path parameter of a GET request. Internally, the method dispatches path parameters to the findById helper method to do its job.

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

The following method updates an existing entry of the courses map, where the body of the received PUT request is the entry value and the courseId parameter is the associated key:

@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();
}

This updateCourse method returns a response with OK (200) status if the update is successful, does not change anything and returns a Not Modified (304) response if the existing and uploaded objects have the same field values. In case a Course instance with the given id is not found in the courses map, the method returns a response with Not Found (404) status.

The third method of this root resource class does not directly handle any HTTP request. Instead, it delegates requests to the Course class where requests are handled by matching methods.

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

We have shown methods within the Course class that process delegated requests right before.

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 server = factoryBean.create();

本節中的所有代碼都應包含在 main 方法中:

public class RestfulServer {
    public static void main(String args[]) throws Exception {
        // code snippets shown above
    }
}

main 方法的調用將在第 6 節中呈現。

5. Test Cases

This section describes test cases used to validate the web service we created before. Those tests validate resource states of the service after responding to HTTP requests of the four most commonly used methods, namely GET, POST, PUT, and DELETE.

5.1. Preparation

First, two static fields are declared within the test class, named RestfulTest:

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

Before running tests we create a client object, which is used to communicate with the server and destroy it afterward:

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

The client instance is now ready to be used by test cases.

5.2. GET Requests

In the test class, we define two methods to send GET requests to the server running the web service.

The first method is to get a Course instance given its id in the resource:

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

The second is to get a Student instance given the ids of the course and student in the resource:

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

These methods send HTTP GET requests to the service resource, then unmarshal XML responses to instances of the corresponding classes. Both are used to verify service resource states after executing POST, PUT, and DELETE requests.

5.3. POST Requests

This subsection features two test cases for POST requests, illustrating operations of the web service when the uploaded Student instance leads to a conflict and when it is successfully created.

In the first test, we use a Student object unmarshaled from the conflict_student.xml file, located on the classpath with the following content:

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

This is how that content is converted to a POST request body:

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

The Content-Type header is set to tell the server that the content type of the request is XML:

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

Since the uploaded Student object is already existent in the first Course instance, we expect that the creation fails and a response with Conflict (409) status is returned. The following code snippet verifies the expectation:

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

In the next test, we extract the body of an HTTP request from a file named created_student.xml, also on the classpath. Here is content of the file:

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

Similar to the previous test case, we build and execute a request, then verify that a new instance is successfully created:

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());

We may confirm new states of the web service resource:

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

The following code snippet shows the content of the XML response that is received after a GET request for the previously uploaded Student object is sent:

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

5.4. PUT Requests

Let’s start with an invalid update request, where the Course object being updated does not exist. Here is content of the instance used to replace a non-existent Course object in the web service resource:

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

That content is stored in a file called non_existent_course.xml on the classpath. It is extracted and then used to populate the body of a PUT request by the code below:

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

The Content-Type header is set to tell the server that the content type of the request is XML:

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

Since we intentionally sent an invalid request to update a non-existent object, a Not Found (404) response is expected to be received. The response is validated:

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

In the second test case for PUT requests, we submit a Course object with the same field values. Since nothing is changed in this case, we expect that a response with Not Modified (304) status is returned. The whole process is illustrated:

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());

Where unchanged_course.xml is the file on the classpath keeping information used to update. Here is its content:

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

In the last demonstration of PUT requests, we execute a valid update. The following is content of the changed_course.xml file whose content is used to update a Course instance in the web service resource:

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

This is how the request is built and executed:

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");
        
HttpResponse response = client.execute(httpPut);
assertEquals(200, response.getStatusLine().getStatusCode());

Let’s verify a successful upload.

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

The following code snippet shows the content of the XML response that is received after a GET request for the previously uploaded Student object is sent:

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

It is clear that the first Student has successfully been removed.

6. 測試執行

第 4 節描述瞭如何在 Server 實例的 main 方法中創建和銷燬 RestfulServer 類中的 Server 實例。

讓服務器啓動的最後一步是調用該 main 方法。要實現這一點,已包含並配置了 Exec Maven 插件在 Maven POM 文件中:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.1.0</version>
    </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 服務的資源以及創建發佈服務的服務器。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.