Apache CXF-Unterstützung für RESTful Web Services

1. Übersicht

In diesem Lernprogramm wird Apache CXF als Framework vorgestellt, das dem JAX-RS-Standard entspricht, der die Unterstützung des Java-Ökosystems für das REST-Architekturmuster (REpresentational State Transfer) definiert.

Insbesondere wird Schritt für Schritt beschrieben, wie ein RESTful-Webdienst erstellt und veröffentlicht wird und wie Komponententests geschrieben werden, um einen Dienst zu überprüfen.

Dies ist der dritte Teil einer Reihe zu Apache CXF. Die erste konzentriert sich auf die Verwendung von CXF als JAX-WS-kompatible Implementierung. Der zweite Artikel enthält eine Anleitung zur Verwendung von CXF mit Spring.

2. Maven-Abhängigkeiten

Die erste erforderliche Abhängigkeit ist org.apache.cxf: cxfrt-frontend- jaxrs . Dieses Artefakt bietet JAX-RS-APIs sowie eine CXF-Implementierung:

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

In diesem Lernprogramm verwenden wir CXF, um einen Server- Endpunkt zum Veröffentlichen eines Webdienstes zu erstellen, anstatt einen Servlet-Container zu verwenden. Daher muss die folgende Abhängigkeit in die Maven POM-Datei aufgenommen werden:

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

Fügen wir abschließend die HttpClient-Bibliothek hinzu, um Unit-Tests zu vereinfachen:

 org.apache.httpcomponents httpclient 4.5.2 

Hier finden Sie die neueste Version der Abhängigkeit cxf-rt-frontend-jaxrs . Unter diesem Link finden Sie möglicherweise auch die neuesten Versionen der Artefakte org.apache.cxf: cxf-rt-transports-http-jetty . Die neueste Version von httpclient finden Sie hier.

3. Ressourcenklassen und Anforderungszuordnung

Beginnen wir mit der Implementierung eines einfachen Beispiels. Wir werden unsere REST-API mit zwei Ressourcen einrichten: Kurs und Student.

Wir werden einfach anfangen und uns im Laufe der Zeit einem komplexeren Beispiel zuwenden.

3.1. Die Ressourcen

Hier ist die Definition der Schülerressourcenklasse :

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

Beachten Sie, dass wir die Annotation @XmlRootElement verwenden , um JAXB mitzuteilen, dass Instanzen dieser Klasse in XML gemarshallt werden sollen.

Als nächstes folgt die Definition der Kursressourcenklasse :

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

Lassen Sie uns abschließend das CourseRepository implementieren, das die Stammressource ist und als Einstiegspunkt für Webdienstressourcen dient:

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

Beachten Sie die Zuordnung mit der Annotation @Path . Das CourseRepository ist hier die Stammressource , daher ist es so zugeordnet, dass alle URLs ab Kurs behandelt werden .

Der Wert der Annotation @Produces wird verwendet, um den Server anzuweisen , von Methoden innerhalb dieser Klasse zurückgegebene Objekte in XML-Dokumente zu konvertieren, bevor sie an Clients gesendet werden. Wir verwenden hier JAXB als Standard, da keine anderen Bindungsmechanismen angegeben sind.

3.2. Einfache Dateneinrichtung

Da dies eine einfache Beispielimplementierung ist, verwenden wir In-Memory-Daten anstelle einer vollwertigen persistenten Lösung.

In diesem Sinne implementieren wir eine einfache Setup-Logik, um einige Daten in das System einzufügen:

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

Methoden innerhalb dieser Klasse, die sich um HTTP-Anforderungen kümmern, werden im nächsten Unterabschnitt behandelt.

3.3. Die API - Request Mapping-Methoden

Kommen wir nun zur Implementierung der eigentlichen REST-API.

Wir werden anfangen, API-Operationen - unter Verwendung der @ Path- Annotation - direkt in den Ressourcen-POJOs hinzuzufügen .

Es ist wichtig zu verstehen, dass dies ein wesentlicher Unterschied zum Ansatz in einem typischen Spring-Projekt ist - bei dem die API-Operationen in einem Controller und nicht auf dem POJO selbst definiert werden.

Beginnen wir mit dem Mapping - Methoden innerhalb der definierten Kurs Klasse:

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

Einfach ausgedrückt, wird die Methode bei der Verarbeitung von GET- Anforderungen aufgerufen , die durch die @ GET- Annotation gekennzeichnet sind.

Beachten Sie die einfache Syntax zum Zuordnen des Pfadparameters studentId aus der HTTP-Anforderung.

Wir verwenden dann einfach die FindById-Hilfsmethode , um die entsprechende Student- Instanz zurückzugeben.

Die folgende Methode verarbeitet POST- Anforderungen, die in der @ POST- Annotation angegeben sind, indem das empfangene Student- Objekt zur Studentenliste hinzugefügt wird :

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

Dies gibt eine 200 OK- Antwort zurück, wenn der Erstellungsvorgang erfolgreich war, oder 409 Conflict, wenn ein Objekt mit der übermittelten ID bereits vorhanden ist.

Beachten Sie auch, dass wir die Annotation @Path überspringen können, da ihr Wert eine leere Zeichenfolge ist.

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

Dieses Tutorial veranschaulicht die Verwendung von Apache CXF als JAX-RS-Implementierung. Es wurde gezeigt, wie das Framework verwendet werden kann, um Ressourcen für einen RESTful-Webdienst zu definieren und einen Server zum Veröffentlichen des Dienstes zu erstellen.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie im GitHub-Projekt.