Поддръжка на Apache CXF за RESTful уеб услуги

1. Общ преглед

Този урок представя Apache CXF като рамка, съвместима със стандарта JAX-RS, който дефинира поддръжката на екосистемата Java за архитектурния модел REpresentational State Transfer (REST).

По-конкретно, той описва стъпка по стъпка как да създадете и публикувате RESTful уеб услуга и как да напишете модулни тестове за проверка на услуга.

Това е третият от поредицата на Apache CXF; първият се фокусира върху използването на CXF като JAX-WS напълно съвместима реализация. Втората статия предоставя ръководство за това как да използвате CXF с Spring.

2. Зависимости на Maven

Първата необходима зависимост е org.apache.cxf: cxf- rt -frontend- jaxrs . Този артефакт предоставя JAX-RS API, както и изпълнение на CXF:

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

В този урок използваме CXF, за да създадем крайна точка на сървъра за публикуване на уеб услуга, вместо да използваме контейнер за сървлети. Следователно, следващата зависимост трябва да бъде включена във файла на Maven POM:

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

И накрая, нека добавим библиотеката HttpClient, за да улесним модулни тестове:

 org.apache.httpcomponents httpclient 4.5.2 

Тук можете да намерите най-новата версия на зависимостта cxf-rt-frontend-jaxrs . Можете също така да се обърнете към тази връзка за най-новите версии на org.apache.cxf: cxf-rt-transports-http-jetty артефакти. И накрая, най-новата версия на httpclient можете да намерите тук.

3. Ресурсни класове и картографиране на заявки

Нека започнем да прилагаме прост пример; ще настроим нашия REST API с два ресурса Course и Student.

Ще започнем просто и ще преминем към по-сложен пример.

3.1. Ресурсите

Ето дефиницията на класа на студентските ресурси:

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

Забележете, че използваме анотацията @XmlRootElement, за да кажем на JAXB, че екземплярите от този клас трябва да бъдат маршалирани в XML.

След това идва дефиницията на класа на курса :

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

И накрая, нека внедрим CourseRepository - който е основният ресурс и служи като входна точка за ресурси на уеб услуги:

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

Забележете картографирането с анотацията @Path . В CourseRepository е основната ресурс тук, така че е назначено да се справят с всички адреси, които започват с курс .

Стойността на анотацията @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 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 на ресурса.

Важно е да се разбере, че това е съществена разлика от подхода в типичен пролетен проект - където операциите на API ще бъдат дефинирани в контролер, а не в самия POJO.

Нека започнем с методите за картографиране, дефинирани в класа на курса :

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

Най-просто казано, методът се извиква при обработка на GET заявки, обозначени с анотацията @GET .

Забеляза простия синтаксис на картографиране на параметъра на път studentId от HTTP заявката.

След това просто използваме помощния метод findById , за да върнем съответния студентски екземпляр.

Следният метод обработва POST заявки, посочени от анотацията @POST , чрез добавяне на получения обект Student в списъка на студентите :

@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, ако операцията за създаване е била успешна, или 409 конфликт, ако обект с изпратения идентификатор вече съществува.

Също така имайте предвид, че можем да пропуснем анотацията @Path, тъй като нейната стойност е празен низ.

Последният метод се грижи за ИЗТРИВАНЕ на заявки. Той премахва елемент от списъка със студенти, чийто идентификатор е полученият параметър на пътя и връща отговор със статус OK (200). В случай че няма елементи, свързани с посочения идентификатор , което означава, че няма нищо за премахване, този метод връща отговор със състояние Не е намерено (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(); }

Нека да преминем към заявка на методи за картографиране на класа 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

Този урок илюстрира използването на Apache CXF като реализация на JAX-RS. Той демонстрира как рамката може да се използва за дефиниране на ресурси за уеб услуга RESTful и за създаване на сървър за публикуване на услугата.

Внедряването на всички тези примери и кодови фрагменти може да се намери в проекта GitHub.