Sokongan Apache CXF untuk Perkhidmatan Web RESTful

1. Gambaran keseluruhan

Tutorial ini memperkenalkan Apache CXF sebagai kerangka kerja yang sesuai dengan standard JAX-RS, yang menentukan sokongan ekosistem Java untuk corak seni bina REpresentational State Transfer (REST).

Secara khusus, ini menerangkan langkah demi langkah bagaimana membina dan menerbitkan perkhidmatan web RESTful, dan bagaimana menulis ujian unit untuk mengesahkan perkhidmatan.

Ini adalah yang ketiga dalam siri di Apache CXF; yang pertama memberi tumpuan kepada penggunaan CXF sebagai pelaksanaan yang mematuhi sepenuhnya JAX-WS. Artikel kedua memberikan panduan bagaimana menggunakan CXF dengan Spring.

2. Pergantungan Maven

Ketergantungan pertama yang diperlukan ialah org.apache.cxf: cxf- rt -frontend- jaxrs . Artefak ini menyediakan API JAX-RS serta pelaksanaan CXF:

 org.apache.cxf cxf-rt-frontend-jaxrs 3.1.7 

Dalam tutorial ini, kami menggunakan CXF untuk membuat titik akhir Server untuk menerbitkan perkhidmatan web dan bukannya menggunakan wadah servlet. Oleh itu, kebergantungan berikut perlu dimasukkan ke dalam fail POM Maven:

 org.apache.cxf cxf-rt-transports-http-jetty 3.1.7 

Akhir sekali, mari tambah perpustakaan HttpClient untuk memudahkan ujian unit:

 org.apache.httpcomponents httpclient 4.5.2 

Di sini anda boleh mendapatkan versi terbaru dari pergantungan cxf-rt-frontend-jaxrs . Anda mungkin juga ingin merujuk pautan ini untuk versi terbaru dari artifak org.apache.cxf: cxf-rt-transports-http-jetty . Akhirnya, versi terbaru httpclient boleh didapati di sini.

3. Kelas Sumber dan Pemetaan Permintaan

Mari mulakan pelaksanaan contoh mudah; kami akan menyediakan API REST kami dengan Kursus dan Pelajar dua sumber .

Kita akan mula mudah dan menuju ke arah contoh yang lebih kompleks semasa kita pergi.

3.1. Sumbernya

Berikut adalah definisi kelas sumber Pelajar :

@XmlRootElement(name = "Student") public class Student { private int id; private String name; // standard getters and setters // standard equals and hashCode implementations }

Perhatikan bahawa kami menggunakan anotasi @XmlRootElement untuk memberitahu JAXB bahawa contoh kelas ini harus dihantarkan ke XML.

Seterusnya, disertakan definisi kelas sumber Kursus :

@XmlRootElement(name = "Course") public class Course { private int id; private String name; private List 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 }

Akhirnya, mari kita laksanakan CourseRepository - yang merupakan sumber root dan berfungsi sebagai titik masuk ke sumber perkhidmatan web:

@Path("course") @Produces("text/xml") public class CourseRepository { private Map courses = new HashMap(); // request handling methods private Course findById(int id) { for (Map.Entry course : courses.entrySet()) { if (course.getKey() == id) { return course.getValue(); } } return null; } }

Perhatikan pemetaan dengan anotasi @Path . The CourseRepository adalah sumber akar di sini, jadi ia dipetakan untuk mengendalikan semua URL bermula dengan kursus .

Nilai anotasi @Produces digunakan untuk memberitahu pelayan untuk menukar objek yang dikembalikan dari kaedah dalam kelas ini ke dokumen XML sebelum menghantarnya ke klien. Kami menggunakan JAXB di sini sebagai lalai kerana tidak ada mekanisme pengikat lain yang ditentukan.

3.2. Penyediaan Data Ringkas

Kerana ini adalah contoh pelaksanaan yang sederhana, kami menggunakan data dalam memori dan bukannya penyelesaian berterusan yang lengkap.

Dengan itu, mari kita laksanakan logik persediaan mudah untuk memasukkan beberapa data ke dalam sistem:

{ Student student1 = new Student(); Student student2 = new Student(); student1.setId(1); student1.setName("Student A"); student2.setId(2); student2.setName("Student B"); List 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); }

Kaedah dalam kelas ini yang mengurus permintaan HTTP akan dibahas dalam subseksyen seterusnya.

3.3. API - Kaedah Pemetaan Permintaan

Sekarang, mari kita pergi ke pelaksanaan REST API sebenar.

Kami akan mula menambahkan operasi API - menggunakan anotasi @Path - tepat di POJO sumber.

Penting untuk memahami bahawa adalah perbezaan yang signifikan dari pendekatan dalam projek Spring khas - di mana operasi API akan ditentukan dalam pengawal, bukan pada POJO itu sendiri.

Mari kita mulakan dengan kaedah pemetaan yang ditentukan dalam kelas Kursus :

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

Ringkasnya, kaedah ini digunakan ketika menangani permintaan GET , dilambangkan dengan anotasi @GET .

Memerhatikan sintaks mudah pemetaan parameter path studentId dari permintaan HTTP.

Kami kemudian hanya menggunakan kaedah penolong findById untuk mengembalikan contoh Pelajar yang sesuai .

Kaedah berikut menangani permintaan POST , ditunjukkan oleh anotasi @POST , dengan menambahkan objek Pelajar yang diterima ke senarai pelajar :

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

Ini mengembalikan respons 200 OK jika operasi membuat berjaya, atau 409 Konflik jika objek dengan id yang diserahkan sudah ada.

Perhatikan juga bahawa kita boleh melangkau anotasi @Path kerana nilainya adalah String kosong.

Kaedah terakhir menguruskan permintaan DELETE . Ia mengeluarkan elemen dari senarai pelajar yang idnya adalah parameter jalur yang diterima dan mengembalikan respons dengan status OK (200). Sekiranya tidak ada elemen yang terkait dengan id yang ditentukan , yang bermaksud tidak ada yang perlu dikeluarkan, kaedah ini mengembalikan respons dengan status 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(); }

Mari teruskan untuk meminta kaedah pemetaan kelas CourseRepository .

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 Endpoint

This section focuses on the construction of a CXF server, which is used for publishing the RESTful web service whose resources are depicted in the preceding section. The first step is to instantiate a JAXRSServerFactoryBean object and set the root resource class:

JAXRSServerFactoryBean factoryBean = new JAXRSServerFactoryBean(); factoryBean.setResourceClasses(CourseRepository.class);

A resource provider then needs to be set on the factory bean to manage the life cycle of the root resource class. We use the default singleton resource provider that returns the same resource instance to every request:

factoryBean.setResourceProvider( new SingletonResourceProvider(new CourseRepository()));

We also set an address to indicate the URL where the web service is published:

factoryBean.setAddress("//localhost:8080/");

Now the factoryBean can be used to create a new server that will start listening for incoming connections:

Server server = factoryBean.create();

All the code above in this section should be wrapped in the main method:

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

The invocation of this main method is presented in section 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 = "//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:

 2 Student B 

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:

 3 Student C 

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

This is what the XML response to a request for the new Student object looks like:

  3 Student C 

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:

 3 Apache CXF Support for RESTful 

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:

 1 REST with Spring 

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:

 2 Apache CXF Support for RESTful 

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

Let's validate a PUT request to the server and validate a successful upload:

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

Let's verify the new states of the web service resource:

Course course = getCourse(2); assertEquals(2, course.getId()); assertEquals("Apache CXF Support for RESTful", course.getName());

The following code snippet shows the content of the XML response when a GET request for the previously uploaded Course object is sent:

  2 Apache CXF Support for RESTful 

5.5. DELETE Requests

First, let's try to delete a non-existent Student instance. The operation should fail and a corresponding response with Not Found (404) status is expected:

HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/3"); HttpResponse response = client.execute(httpDelete); assertEquals(404, response.getStatusLine().getStatusCode());

In the second test case for DELETE requests, we create, execute and verify a request:

HttpDelete httpDelete = new HttpDelete(BASE_URL + "1/students/1"); HttpResponse response = client.execute(httpDelete); assertEquals(200, response.getStatusLine().getStatusCode());

We verify new states of the web service resource with the following code snippet:

Course course = getCourse(1); assertEquals(1, course.getStudents().size()); assertEquals(2, course.getStudents().get(0).getId()); assertEquals("Student B", course.getStudents().get(0).getName());

Next, we list the XML response that is received after a request for the first Course object in the web service resource:

  1 REST with Spring  2 Student B  

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

6. Test Execution

Section 4 described how to create and destroy a Server instance in the main method of the RestfulServer class.

The last step to make the server up and running is to invoke that main method. In order to achieve that, the Exec Maven plugin is included and configured in the Maven POM file:

 org.codehaus.mojo exec-maven-plugin 1.5.0   com.baeldung.cxf.jaxrs.implementation.RestfulServer   

The latest version of this plugin can be found via this link.

In the process of compiling and packaging the artifact illustrated in this tutorial, the Maven Surefire plugin automatically executes all tests enclosed in classes having names starting or ending with Test. If this is the case, the plugin should be configured to exclude those tests:

 maven-surefire-plugin 2.19.1   **/ServiceTest   

With the above configuration, ServiceTest is excluded since it is the name of the test class. You may choose any name for that class, provided tests contained therein are not run by the Maven Surefire plugin before the server is ready for connections.

For the latest version of Maven Surefire plugin, please check here.

Now you can execute the exec:java goal to start the RESTful web service server and then run the above tests using an IDE. Equivalently you may start the test by executing the command mvn -Dtest=ServiceTest test in a terminal.

7. Conclusion

Tutorial ini menggambarkan penggunaan Apache CXF sebagai pelaksanaan JAX-RS. Ini menunjukkan bagaimana kerangka kerja dapat digunakan untuk menentukan sumber untuk perkhidmatan web RESTful dan membuat pelayan untuk menerbitkan perkhidmatan tersebut.

Pelaksanaan semua contoh dan potongan kod ini boleh didapati di projek GitHub.