Support Apache CXF pour les services Web RESTful

Prise en charge d'Apache CXF pour les services Web RESTful

1. Vue d'ensemble

Ce didacticiel présenteApache CXF en tant que framework conforme à la norme JAX-RS, qui définit la prise en charge de l'écosystème Java pour le modèle architectural REpresentational State Transfer (REST).

Plus précisément, il décrit étape par étape comment construire et publier un service Web RESTful et comment écrire des tests unitaires pour vérifier un service.

Ceci est le troisième d'une série sur Apache CXF; the first one se concentre sur l'utilisation de CXF en tant qu'implémentation entièrement compatible JAX-WS. Lesecond article fournit un guide sur l'utilisation de CXF avec Spring.

2. Dépendances Maven

La première dépendance requise estorg.apache.cxf:cxf-rt-frontend-jaxrs. This artifact provides JAX-RS APIs as well as a CXF implementation:


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

Dans ce didacticiel, nous utilisons CXF pour créer un point de terminaisonServer afin de publier un service Web au lieu d'utiliser un conteneur de servlet. Par conséquent, la dépendance suivante doit être incluse dans le fichier Maven POM:


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

Enfin, ajoutons la bibliothèque HttpClient pour faciliter les tests unitaires:


    org.apache.httpcomponents
    httpclient
    4.5.2

Here vous pouvez trouver la dernière version de la dépendancecxf-rt-frontend-jaxrs. Vous pouvez également vous référer àthis link pour les dernières versions des artefactsorg.apache.cxf:cxf-rt-transports-http-jetty. Enfin, la dernière version dehttpclient peut être trouvéehere.

3. Classes de ressources et mappage des demandes

Commençons par mettre en œuvre un exemple simple; nous allons configurer notre API REST avec deux ressourcesCourse etStudent.

Nous commencerons simplement et passerons à un exemple plus complexe au fur et à mesure.

3.1. Les ressources

Voici la définition de la classe de ressourcesStudent:

@XmlRootElement(name = "Student")
public class Student {
    private int id;
    private String name;

    // standard getters and setters
    // standard equals and hashCode implementations

}

Notez que nous utilisons l'annotation@XmlRootElement pour indiquer à JAXB que les instances de cette classe doivent être marshalées en XML.

Ensuite, vient la définition de la classe de ressourcesCourse:

@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

}

Enfin, implémentons leCourseRepository - qui est la ressource racine et sert de point d'entrée aux ressources du service 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;
    }
}

Notez le mappage avec l'annotation@Path. LeCourseRepository est la ressource racine ici, il est donc mappé pour gérer toutes les URL commençant parcourse.

La valeur de l'annotation@Produces est utilisée pour indiquer au serveur de convertir les objets renvoyés par les méthodes de cette classe en documents XML avant de les envoyer aux clients. Nous utilisons ici JAXB par défaut car aucun autre mécanisme de liaison n'est spécifié.

3.2. Configuration simple des données

Comme il s'agit d'un exemple d'implémentation simple, nous utilisons des données en mémoire au lieu d'une solution permanente à part entière.

Dans cet esprit, implémentons une logique de configuration simple pour alimenter certaines données dans le système:

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

Les méthodes de cette classe prenant en charge les requêtes HTTP sont traitées dans la sous-section suivante.

3.3. L'API - Méthodes de mappage des requêtes

Passons maintenant à la mise en œuvre de l'API REST réelle.

Nous allons commencer à ajouter des opérations API - en utilisant l'annotation@Path - directement dans les POJO de ressources.

Il est important de comprendre que c'est une différence significative par rapport à l'approche d'un projet Spring typique - où les opérations de l'API seraient définies dans un contrôleur, et non sur le POJO lui-même.

Commençons par les méthodes de mappage définies dans la classeCourse:

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

En termes simples, la méthode est invoquée lors du traitement des requêtesGET, désignées par l'annotation@GET.

Remarqué la syntaxe simple du mappage du paramètre de cheminstudentId à partir de la requête HTTP.

Nous utilisons alors simplement la méthode d'assistancefindById pour renvoyer l'instanceStudent correspondante.

La méthode suivante gère les requêtesPOST, indiquées par l'annotation@POST, en ajoutant l'objetStudent reçu à la listestudents:

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

Cela renvoie une réponse200 OK si l'opération de création a réussi, ou409 Conflict si un objet avec lesid soumis existe déjà.

Notez également que nous pouvons ignorer l'annotation@Path car sa valeur est une chaîne vide.

La dernière méthode prend en charge les requêtesDELETE. Il supprime un élément de la listestudents dontid est le paramètre de chemin reçu et renvoie une réponse avec l'étatOK (200). Dans le cas où aucun élément n'est associé auxid spécifiés, ce qui implique qu'il n'y a rien à supprimer, cette méthode renvoie une réponse avec le statutNot 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();
}

Passons à la requête des méthodes de mappage de la classeCourseRepository.

La méthodegetCourse suivante renvoie un objetCourse qui est la valeur d'une entrée dans la mappecourses dont la clé est le paramètre de chemincourseId reçu d'unGET demande. En interne, la méthode distribue les paramètres de chemin à la méthode d'assistancefindById pour faire son travail.

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

La méthode suivante met à jour une entrée existante de la mappecourses, où le corps de la requêtePUT reçue est la valeur d'entrée et le paramètrecourseId est la clé associée:

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

Cette méthodeupdateCourse renvoie une réponse avec le statutOK (200) si la mise à jour réussit, ne change rien et renvoie une réponseNot Modified (304) si les objets existants et téléchargés ont le mêmes valeurs de champ. Dans le cas où une instanceCourse avec leid donné n'est pas trouvée dans la cartecourses, la méthode renvoie une réponse avec le statutNot Found (404).

La troisième méthode de cette classe de ressources racine ne gère directement aucune requête HTTP. Au lieu de cela, il délègue les demandes à la classeCourse où les demandes sont traitées par des méthodes correspondantes:

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

Nous avons montré des méthodes dans la classeCourse qui traitent les demandes déléguées juste avant.

4. Point finalServer

Cette section porte sur la construction d'un serveur CXF, utilisé pour la publication du service Web RESTful dont les ressources sont décrites dans la section précédente. La première étape consiste à instancier un objetJAXRSServerFactoryBean et à définir la classe de ressources racine:

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

Un fournisseur de ressources doit ensuite être défini sur le bean factory pour gérer le cycle de vie de la classe de ressources racine. Nous utilisons le fournisseur de ressources singleton par défaut qui renvoie la même instance de ressource à chaque demande:

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

Nous avons également défini une adresse pour indiquer l'URL où le service Web est publié:

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

Maintenant, lesfactoryBean peuvent être utilisés pour créer un nouveauserver qui commencera à écouter les connexions entrantes:

Server server = factoryBean.create();

Tout le code ci-dessus dans cette section doit être enveloppé dans la méthodemain:

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

L'appel de cette méthodemain est présenté dans la section 6.

5. Cas de test

Cette section décrit les scénarios de test utilisés pour valider le service Web que nous avons créé auparavant. Ces tests valident les états des ressources du service après avoir répondu aux requêtes HTTP des quatre méthodes les plus couramment utilisées, à savoirGET,POST,PUT etDELETE.

5.1. Préparation

Tout d'abord, deux champs statiques sont déclarés dans la classe de test, nommésRestfulTest:

private static String BASE_URL = "http://localhost:8080/example/courses/";
private static CloseableHttpClient client;

Avant d'exécuter les tests, nous créons un objetclient, qui est utilisé pour communiquer avec le serveur et le détruire par la suite:

@BeforeClass
public static void createClient() {
    client = HttpClients.createDefault();
}

@AfterClass
public static void closeClient() throws IOException {
    client.close();
}

L'instanceclient est maintenant prête à être utilisée par les cas de test.

5.2. DemandesGET

Dans la classe de test, nous définissons deux méthodes pour envoyer les requêtesGET au serveur exécutant le service Web.

La première méthode consiste à obtenir une instanceCourse en fonction de sesid dans la ressource:

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

La seconde consiste à obtenir une instanceStudent étant donné lesids du cours et de l'étudiant dans la ressource:

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

Ces méthodes envoient des requêtes HTTPGET à la ressource de service, puis annulent les réponses XML aux instances des classes correspondantes. Les deux sont utilisés pour vérifier les états des ressources de service après l'exécution des requêtesPOST,PUT etDELETE.

5.3. DemandesPOST

Cette sous-section présente deux cas de test pour les requêtesPOST, illustrant les opérations du service Web lorsque l'instanceStudent téléchargée conduit à un conflit et lorsqu'elle est créée avec succès.

Dans le premier test, nous utilisons un objetStudent démarshalé du fichierconflict_student.xml, situé sur le chemin de classe avec le contenu suivant:


    2
    Student B

Voici comment ce contenu est converti en un corps de requêtePOST:

HttpPost httpPost = new HttpPost(BASE_URL + "1/students");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("conflict_student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));

L'en-têteContent-Type est défini pour indiquer au serveur que le type de contenu de la requête est XML:

httpPost.setHeader("Content-Type", "text/xml");

Étant donné que l'objetStudent téléchargé existe déjà dans la première instance deCourse, nous nous attendons à ce que la création échoue et qu'une réponse avec le statutConflict (409) soit renvoyée. L'extrait de code suivant vérifie les attentes:

HttpResponse response = client.execute(httpPost);
assertEquals(409, response.getStatusLine().getStatusCode());

Dans le test suivant, nous extrayons le corps d'une requête HTTP à partir d'un fichier nommécreated_student.xml, également sur le chemin de classe. Voici le contenu du fichier:


    3
    Student C

Semblable au cas de test précédent, nous construisons et exécutons une demande, puis vérifions qu’une nouvelle instance est créée avec succès:

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

Nous pouvons confirmer les nouveaux états de la ressource de service Web:

Student student = getStudent(2, 3);
assertEquals(3, student.getId());
assertEquals("Student C", student.getName());

Voici à quoi ressemble la réponse XML à une requête pour le nouvel objetStudent:



    3
    Student C

5.4. DemandesPUT

Commençons par une demande de mise à jour non valide, où l'objetCourse en cours de mise à jour n'existe pas. Voici le contenu de l'instance utilisée pour remplacer un objetCourse inexistant dans la ressource de service Web:


    3
    Apache CXF Support for RESTful

Ce contenu est stocké dans un fichier appelénon_existent_course.xml sur le chemin de classe. Il est extrait puis utilisé pour remplir le corps d'une requêtePUT par le code ci-dessous:

HttpPut httpPut = new HttpPut(BASE_URL + "3");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("non_existent_course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));

L'en-têteContent-Type est défini pour indiquer au serveur que le type de contenu de la requête est XML:

httpPut.setHeader("Content-Type", "text/xml");

Puisque nous avons intentionnellement envoyé une demande invalide pour mettre à jour un objet inexistant, une réponseNot Found (404) devrait être reçue. La réponse est validée:

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

Dans le deuxième cas de test pour les requêtesPUT, nous soumettons un objetCourse avec les mêmes valeurs de champ. Puisque rien n'est changé dans ce cas, nous nous attendons à ce qu'une réponse avec le statutNot Modified (304) soit renvoyée. L'ensemble du processus est illustré:

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 est le fichier sur le chemin de classe conservant les informations utilisées pour la mise à jour. Voici son contenu:


    1
    REST with Spring

Dans la dernière démonstration des requêtesPUT, nous exécutons une mise à jour valide. Voici le contenu du fichierchanged_course.xml dont le contenu est utilisé pour mettre à jour une instanceCourse dans la ressource de service Web:


    2
    Apache CXF Support for RESTful

Voici comment la requête est construite et exécutée:

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

Validons une requêtePUT vers le serveur et validons un téléchargement réussi:

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

Vérifions les nouveaux états de la ressource de service Web:

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

L'extrait de code suivant montre le contenu de la réponse XML lorsqu'une requête GET pour l'objetCourse précédemment téléchargé est envoyée:



    2
    Apache CXF Support for RESTful

5.5. DemandesDELETE

Tout d'abord, essayons de supprimer une instanceStudent inexistante. L'opération doit échouer et une réponse correspondante avec l'étatNot Found (404) est attendue:

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

Dans le deuxième cas de test pour les requêtesDELETE, nous créons, exécutons et vérifions une requête:

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

Nous vérifions les nouveaux états de la ressource de service Web avec l'extrait de code suivant:

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

Ensuite, nous listons la réponse XML qui est reçue après une requête pour le premier objetCourse dans la ressource de service Web:



    1
    REST with Spring
    
        2
        Student B
    

Il est clair que le premierStudent a été supprimé avec succès.

6. Exécution du test

La section 4 a décrit comment créer et détruire une instanceServer dans la méthodemain de la classeRestfulServer.

La dernière étape pour rendre le serveur opérationnel est d'appeler cette méthodemain. Afin d’y parvenir, le plugin Exec Maven est inclus et configuré dans le fichier Maven POM:


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

La dernière version de ce plugin peut être trouvée viathis link.

Dans le processus de compilation et d'empaquetage de l'artefact illustré dans ce didacticiel, le plugin Maven Surefire exécute automatiquement tous les tests inclus dans les classes dont les noms commencent ou se terminent parTest. Si tel est le cas, le plug-in doit être configuré pour exclure ces tests:


    maven-surefire-plugin
    2.19.1
    
    
        **/ServiceTest
    
    

Avec la configuration ci-dessus,ServiceTest est exclu car il s'agit du nom de la classe de test. Vous pouvez choisir n’importe quel nom pour cette classe, à condition que les tests qu’il contient ne soient pas exécutés par le plug-in Maven Surefire avant que le serveur ne soit prêt pour les connexions.

Pour la dernière version du plugin Maven Surefire, veuillez vérifierhere.

Vous pouvez maintenant exécuter l'objectifexec:java pour démarrer le serveur de service Web RESTful, puis exécuter les tests ci-dessus à l'aide d'un IDE. De manière équivalente, vous pouvez démarrer le test en exécutant la commandemvn -Dtest=ServiceTest test in a terminal.

7. Conclusion

Ce tutoriel illustre l'utilisation d'Apache CXF en tant qu'implémentation JAX-RS. Il a montré comment utiliser la structure pour définir des ressources pour un service Web RESTful et créer un serveur pour la publication du service.

L'implémentation de tous ces exemples et extraits de code se trouve dansthe GitHub project.