Suport Apache CXF pentru servicii Web RESTful

1. Prezentare generală

Acest tutorial introduce Apache CXF ca un cadru conform cu standardul JAX-RS, care definește suportul ecosistemului Java pentru modelul arhitectural REpresentational State Transfer (REST).

Mai exact, descrie pas cu pas cum să construiți și să publicați un serviciu web RESTful și cum să scrieți teste unitare pentru a verifica un serviciu.

Acesta este al treilea dintr-o serie despre Apache CXF; primul se concentrează pe utilizarea CXF ca implementare JAX-WS complet compatibilă. Al doilea articol oferă un ghid despre modul de utilizare a CXF cu Spring.

2. Dependențele Maven

Prima dependență necesară este org.apache.cxf: cxf- rt -frontend- jaxrs . Acest artefact oferă API-uri JAX-RS, precum și o implementare CXF:

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

În acest tutorial, folosim CXF pentru a crea un punct final Server pentru a publica un serviciu web în loc să folosim un container servlet. Prin urmare, următoarea dependență trebuie inclusă în fișierul Maven POM:

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

În cele din urmă, să adăugăm biblioteca HttpClient pentru a facilita testele unitare:

 org.apache.httpcomponents httpclient 4.5.2 

Aici puteți găsi cea mai recentă versiune a dependenței cxf-rt-frontend-jaxrs . De asemenea, vă recomandăm să consultați acest link pentru cele mai recente versiuni ale artefactelor org.apache.cxf: cxf-rt-transports-http-jetty . În cele din urmă, cea mai recentă versiune a httpclient poate fi găsită aici.

3. Clasele de resurse și maparea cererilor

Să începem să implementăm un exemplu simplu; vom configura API-ul nostru REST cu două resurse Curs și Student.

Vom începe simplu și ne vom îndrepta spre un exemplu mai complex pe măsură ce mergem.

3.1. Resursele

Iată definiția clasei de resurse pentru elevi :

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

Observați că folosim adnotarea @XmlRootElement pentru a-i spune lui JAXB că instanțele din această clasă ar trebui transformate în XML.

Apoi, vine definiția cursului clasei de resurse:

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

În cele din urmă, să implementăm CourseRepository - care este resursa rădăcină și servește ca punct de intrare la resursele serviciului 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; } }

Observați maparea cu adnotarea @Path . CourseRepository este resursa rădăcină de aici, așa că este mapat să se ocupe de toate URL - urile începând cu curs .

Valoarea adnotării @Produces este utilizată pentru a spune serverului să convertească obiectele returnate din metodele din această clasă în documente XML înainte de a le trimite clienților. Folosim JAXB aici ca implicit, deoarece nu sunt specificate alte mecanisme de legare.

3.2. Configurare simplă a datelor

Deoarece acesta este un exemplu simplu de implementare, folosim date în memorie în loc de o soluție persistentă completă.

Având în vedere acest lucru, să implementăm o logică simplă de configurare pentru a completa unele date în 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); }

Metodele din această clasă care se ocupă de solicitările HTTP sunt acoperite în următoarea subsecțiune.

3.3. API - Metode de mapare a cererii

Acum, să trecem la implementarea API-ului REST real.

Vom începe să adăugăm operațiuni API - folosind adnotarea @Path - chiar în POJO-urile de resurse.

Este important să înțelegem că este o diferență semnificativă față de abordarea unui proiect tipic de primăvară - în care operațiunile API ar fi definite într-un controler, nu pe POJO în sine.

Să începem cu metode de cartografiere definite în cadrul cursului :

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

Pur și simplu, metoda este invocată atunci când se tratează solicitările GET , notate prin adnotarea @GET .

Am observat sintaxa simplă a mapării parametrului de cale studentId din cererea HTTP.

Atunci folosim pur și simplu metoda de ajutor findById pentru a returna instanța Student corespunzătoare .

Următoarea metodă gestionează solicitările POST , indicate prin adnotarea @POST , prin adăugarea obiectului Student primit în lista studenților :

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

Aceasta returnează un răspuns 200 OK dacă operațiunea de creare a reușit sau 409 Conflict dacă un obiect cu ID-ul trimis există deja.

De asemenea, rețineți că putem sări peste adnotarea @Path, deoarece valoarea sa este un șir gol.

Ultima metodă se ocupă de solicitările ȘTERGERE . Elimină un element din lista studenților al cărui id este parametrul căii primite și returnează un răspuns cu starea OK (200). În cazul în care nu există elemente asociate cu ID-ul specificat , ceea ce înseamnă că nu este nimic de eliminat, această metodă returnează un răspuns cu starea 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(); }

Să trecem mai departe pentru a solicita metode de mapare a clasei CourseRepository .

Următoarea metodă getCourse returnează un obiect Course care reprezintă valoarea unei intrări în harta cursurilor a cărei cheie este parametrul de cale courseId primit al unei cereri GET . Intern, metoda expediază parametrii căii către metoda de ajutor findById pentru a-și face treaba.

@GET @Path("courses/{courseId}") public Course getCourse(@PathParam("courseId") int courseId) { return findById(courseId); }

Următoarea metodă actualizează o intrare existentă a hărții cursurilor , în care corpul cererii PUT primite este valoarea de intrare și parametrul courseId este cheia asociată:

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

Această updateCourse metodă returnează un răspuns cu OK de stare (200) , în cazul în care actualizarea este de succes, nu schimba nimic și returnează un Nemodificată (304) , ca răspuns în cazul în care obiectele existente și încărcate au aceleași valori de câmp. În cazul în care o instanță de curs cu id-ul dat nu este găsită în harta cursurilor , metoda returnează un răspuns cu starea Not Found (404).

A treia metodă a acestei clase de resurse rădăcină nu gestionează direct nicio cerere HTTP. În schimb, delegă solicitările la clasa Curs , unde cererile sunt tratate prin metode de potrivire:

@Path("courses/{courseId}/students") public Course pathToStudent(@PathParam("courseId") int courseId) { return findById(courseId); }

Am arătat metode în cadrul cursului care procesează solicitările delegate chiar înainte.

4. Punct final al serverului

Această secțiune se concentrează pe construirea unui server CXF, care este utilizat pentru publicarea serviciului web RESTful ale cărui resurse sunt prezentate în secțiunea precedentă. Primul pas este instanțierea unui obiect JAXRSServerFactoryBean și setarea clasei de resurse rădăcină:

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

Apoi, un furnizor de resurse trebuie setat pe fabrica pentru a gestiona ciclul de viață al clasei de resurse rădăcină. Folosim furnizorul de resurse implicit, care returnează aceeași instanță de resursă la fiecare solicitare:

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

De asemenea, stabilim o adresă pentru a indica adresa URL în care este publicat serviciul web:

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

Acum factoryBean poate fi folosit pentru a crea un nou server care va începe să asculte conexiunile primite:

Server server = factoryBean.create();

Tot codul de mai sus din această secțiune ar trebui să fie înfășurat în metoda principală :

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

Invocarea acestei metode principale este prezentată în secțiunea 6.

5. Cazuri de testare

Această secțiune descrie cazurile de testare utilizate pentru validarea serviciului web pe care l-am creat anterior. Aceste teste validează stările resurselor serviciului după ce au răspuns la solicitările HTTP ale celor patru metode cele mai frecvent utilizate, și anume GET , POST , PUT și DELETE .

5.1. Pregătirea

În primul rând, două câmpuri statice sunt declarate în cadrul clasei de test, denumite 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

Acest tutorial a ilustrat utilizarea Apache CXF ca implementare JAX-RS. A demonstrat modul în care cadrul poate fi utilizat pentru a defini resursele pentru un serviciu web RESTful și pentru a crea un server pentru publicarea serviciului.

Implementarea tuturor acestor exemple și fragmente de cod poate fi găsită în proiectul GitHub.