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 地圖中找到具有給定 id 的 Course 實例,則該方法返回包含 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 響應反序列化為相應的類實例。兩者均用於在執行 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 請求的 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 服務的資源,以及創建用於發佈服務的服務器。