知識庫 / REST RSS 訂閱

Apache CXF 對 RESTful Web 服務支持

REST
HongKong
4
03:55 AM · Dec 06 ,2025

1. 概述

本教程介紹 Apache CXF 作為符合 JAX-RS 標準的框架,該框架支持 Java 生態系統中的 REpresentational State Transfer (REST) 架構模式。

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

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

2. Maven 依賴

第一個必需的依賴是 <em >org.apache.cxf:cxf-rt-frontend-jaxrs</em>。 此 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. 資源類和請求映射

讓我們開始實現一個簡單的示例,我們將使用兩個資源 課程學生 來設置我們的 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 的實現。

我們將開始添加 API 操作 – 通過使用 @Path 註解 – 直接在資源 POJO 中。

重要的是要理解,這與典型 Spring 項目中的方法有顯著差異 – 在 API 操作在控制器中定義,而不是在 POJO 本身。

讓我們從在 Course 類中定義的映射方法開始:

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

簡單來説,該方法在處理 GET 請求時被調用,由 @GET 註解標識。

注意 HTTP 請求中從 studentId 路徑參數的簡單語法。

然後我們簡單地使用 findById 助手方法來返回相應的 Student 實例。

以下方法處理 POST 請求,通過將接收到的 Student 對象添加到 students 列表中來指示,由 @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 請求。它從 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();
}

讓我們繼續討論如何映射 <em lang="en">CourseRepository</em lang="en"> 類的映射方法。

以下 <em lang="en">getCourse</em lang="en"> 方法返回一個 <em lang="en">Course</em lang="en"> 對象,該對象是 <em lang="en">courses</em lang="en"> 映射中的一個條目的值,其鍵是 <em lang="en">GET</em lang="en"> 請求接收到的 <em lang="en">courseId</em lang="en"> 路徑參數。 內部上,該方法將路徑參數分派到 <em lang="en">findById</em lang="en"> 輔助方法來完成任務。

@GET
@Path("courses/{courseId}")
public Course getCourse(@PathParam("courseId") int courseId) {
    return findById(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 Endpoint)

本節介紹如何構建一個 CXF 服務器,該服務器用於發佈 RESTful Web 服務,其資源在上一節中進行了描述。第一步是實例化一個 <em JAXRSServerFactoryBean</em>> 對象並設置根資源類:

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 請求(即 GET, POST, PUT, 和 DELETE)後,資源的當前狀態。

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

客户端實例現在已準備好供測試用例使用。

5.2. GET 請求

在測試類中,我們定義了兩個方法用於向運行 Web 服務的服務器發送 GET 請求。

第一個方法是在資源中根據 Course 實例的 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 請求的 body,該文件也在 classpath 上。以下是該文件的內容:

<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 對象不存在。以下是用於替換不存在的 Course 對象的 Web 服務資源中使用的實例內容:

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

由於我們故意向系統發送一個更新不存在對象的無效請求,因此預計將收到 未找到 (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 是 classpath 上的一個文件,用於存儲更新所使用的信息。以下是其內容:

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

以下代碼片段展示了在向先前上傳的 課程 對象發送 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 Requests)

首先,嘗試刪除一個不存在的 Student 實例。操作應失敗,並期望返回包含 未找到 (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>

第一名學生已成功移除。

6. 測試執行

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

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

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

此插件的最新版本可通過 此鏈接 訪問。

在編譯和打包本教程中所示的 Artifact 時,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.