API REST avec Play Framework en Java

API REST avec Play Framework en Java

1. Vue d'ensemble

Le but de ce didacticiel est d'explorer le Play Framework et d'apprendre à créer des services REST avec ce dernier à l'aide de Java.

Nous allons mettre en place une API REST pour créer, récupérer, mettre à jour et supprimer les dossiers des élèves.

Dans de telles applications, nous aurions normalement une base de données pour stocker les dossiers des étudiants. Play Framework dispose d'une base de données H2 intégrée, ainsi que de la prise en charge de JPA avec Hibernate et d'autres infrastructures de persistance.

Cependant, pour garder les choses simples et nous concentrer sur les éléments les plus importants, nous utiliserons une simple carte pour stocker les objets des étudiants avec des identifiants uniques.

2. Créer une nouvelle application

Une fois que nous avons installé Play Framework comme décrit dans nosIntroduction to the Play Framework, nous sommes prêts à créer notre application.

Utilisons la commandesbt pour créer une nouvelle application appeléestudent-api en utilisantplay-java-seed:

sbt new playframework/play-java-seed.g8

3. Des modèles

Avec la structure de notre application en place, accédons àstudent-api/app/models et créons un bean Java pour gérer les informations des étudiants:

public class Student {
    private String firstName;
    private String lastName;
    private int age;
    private int id;

    // standard constructors, getters and setters
}

Nous allons maintenant créer un magasin de données simple - soutenu par unHashMap – pour les données des élèves, avec des méthodes d'aide pour effectuer des opérations CRUD:

public class StudentStore {
    private Map students = new HashMap<>();

    public Optional addStudent(Student student) {
        int id = students.size();
        student.setId(id);
        students.put(id, student);
        return Optional.ofNullable(student);
    }

    public Optional getStudent(int id) {
        return Optional.ofNullable(students.get(id));
    }

    public Set getAllStudents() {
        return new HashSet<>(students.values());
    }

    public Optional updateStudent(Student student) {
        int id = student.getId();
        if (students.containsKey(id)) {
            students.put(id, student);
            return Optional.ofNullable(student);
        }
        return null;
    }

    public boolean deleteStudent(int id) {
        return students.remove(id) != null;
    }
}

4. Contrôleurs

Passons àstudent-api/app/controllers et créons un nouveau contrôleur appeléStudentController.java. Nous allons parcourir le code de manière incrémentielle.

Tout d'abord, nous devonsconfigure an *HttpExecutionContext*. Nous implémenterons nos actions en utilisant un code asynchrone et non bloquant. Cela signifie que nos méthodes d'action renverrontCompletionStage<Result> au lieu de seulementResult. Cela a l'avantage de nous permettre d'écrire des tâches de longue durée sans blocage.

Il y a juste une mise en garde concernant la programmation asynchrone dans un contrôleur Play Framework: nous devons fournir unHttpExecutionContext. Si nous ne fournissons pas le contexte d'exécution HTTP, nous obtiendrons la tristement célèbre erreur «Il n'y a pas de contexte HTTP disponible à partir d'ici »lors de l'appel de la méthode d'action.

Injectons-le:

private HttpExecutionContext ec;
private StudentStore studentStore;

@Inject
public StudentController(HttpExecutionContext ec, StudentStore studentStore) {
    this.studentStore = studentStore;
    this.ec = ec;
}

Notez que nous avons également ajouté le sableStudentStore injecté dans les deux champs dans le constructeur du contrôleur en utilisant la sannotation@Inject . Cela fait, nous pouvons maintenant procéder à la mise en œuvre des méthodes d'action.

Notez quePlay ships with Jackson to allow for data processing - nous pouvons donc importer toutes les classes Jackson dont nous avons besoin sans dépendances externes.

Définissons une classe utilitaire pour effectuer des opérations répétitives. Dans ce cas, création de réponses HTTP.

Créons donc le packagestudent-api/app/utils et ajoutons-yUtil.java:

public class Util {
    public static ObjectNode createResponse(Object response, boolean ok) {
        ObjectNode result = Json.newObject();
        result.put("isSuccessful", ok);
        if (response instanceof String) {
            result.put("body", (String) response);
        } else {
            result.putPOJO("body", response);
        }
        return result;
    }
}

Avec cette méthode, nous allons créer des réponses JSON standard avec une clé booléenneisSuccessful et le corps de la réponse.

Nous pouvons maintenant passer en revue les actions de la classe de contrôleur.

4.1. L'actioncreate

Mappée comme une actionPOST , cette méthode gère la création de l'objetStudent:

public CompletionStage create(Http.Request request) {
    JsonNode json = request.body().asJson();
    return supplyAsync(() -> {
        if (json == null) {
            return badRequest(Util.createResponse("Expecting Json data", false));
        }

        Optional studentOptional = studentStore.addStudent(Json.fromJson(json, Student.class));
        return studentOptional.map(student -> {
            JsonNode jsonObject = Json.toJson(student);
            return created(Util.createResponse(jsonObject, true));
        }).orElse(internalServerError(Util.createResponse("Could not create data.", false)));
    }, ec.current());
}

Nous utilisons un appel de la classeHttp.Request injectée pour obtenir le corps de la requête dans la classeJsonNode de Jackson. Remarquez comment nous utilisons la méthode utilitaire pour créer une réponse si le corps estnull.

Nous renvoyons également unCompletionStage<Result>, ce qui nous permet d'écrire du code non bloquant en utilisant la méthodeCompletedFuture.supplyAsync .

Nous pouvons lui passer n'importe quelString ou unJsonNode, avec un drapeauboolean pour indiquer l'état.

Notez également comment nous utilisonsJson.fromJson() pour convertir l'objet JSON entrant en un objetStudent et le retourner en JSON pour la réponse.

Enfin, au lieu deok() auquel nous sommes habitués, nous utilisons la méthode d'aidecreated du packageplay.mvc.results. L'idée est d'utiliser une méthode qui donne le statut HTTP correct pour l'action effectuée dans un contexte particulier. Par exemple,ok() pour l'état HTTP OK 200 etcreated() lorsque HTTP CREATED 201 est l'état du résultat utilisé ci-dessus. Ce concept sera abordé tout au long des actions.

4.2. L'actionupdate

Une requêtePUT àhttp://localhost:9000/ atteint la méthodeStudentController.update, qui met à jour les informations sur l'étudiant en appelant la méthodeupdateStudent desStudentStore:

public CompletionStage update(Http.Request request) {
    JsonNode json = request.body().asJson();
    return supplyAsync(() -> {
        if (json == null) {
            return badRequest(Util.createResponse("Expecting Json data", false));
        }
        Optional studentOptional = studentStore.updateStudent(Json.fromJson(json, Student.class));
        return studentOptional.map(student -> {
            if (student == null) {
                return notFound(Util.createResponse("Student not found", false));
            }
            JsonNode jsonObject = Json.toJson(student);
            return ok(Util.createResponse(jsonObject, true));
        }).orElse(internalServerError(Util.createResponse("Could not create data.", false)));
    }, ec.current());
}

4.3. L'actionretrieve

Pour récupérer un étudiant, nous transmettons l'id de l'étudiant comme paramètre de chemin dans une requêteGET àhttp://localhost:9000/:id. Cela atteindra l'actionretrieve :

public CompletionStage retrieve(int id) {
    return supplyAsync(() -> {
        final Optional studentOptional = studentStore.getStudent(id);
        return studentOptional.map(student -> {
            JsonNode jsonObjects = Json.toJson(student);
            return ok(Util.createResponse(jsonObjects, true));
        }).orElse(notFound(Util.createResponse("Student with id:" + id + " not found", false)));
    }, ec.current());
}

4.4. L'actiondelete

L'actiondelete est mappée surhttp://localhost:9000/:id. Nous fournissons lesid pour identifier quel enregistrement supprimer:

public CompletionStage delete(int id) {
    return supplyAsync(() -> {
        boolean status = studentStore.deleteStudent(id);
        if (!status) {
            return notFound(Util.createResponse("Student with id:" + id + " not found", false));
        }
        return ok(Util.createResponse("Student with id:" + id + " deleted", true));
    }, ec.current());
}

4.5. L'actionlistStudents

Enfin, l'actionlistStudents renvoie une liste de tous les étudiants qui ont été stockés jusqu'à présent. Il est mappé àhttp://localhost:9000/ en tant que requêteGET:

public CompletionStage listStudents() {
    return supplyAsync(() -> {
        Set result = studentStore.getAllStudents();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode jsonData = mapper.convertValue(result, JsonNode.class);
        return ok(Util.createResponse(jsonData, true));
    }, ec.current());
}

5. Mappages

Après avoir configuré nos actions de contrôleur, nous pouvons maintenant les mapper en ouvrant le fichierstudent-api/conf/routes et en ajoutant ces routes:

GET     /                           controllers.StudentController.listStudents()
GET     /:id                        controllers.StudentController.retrieve(id:Int)
POST    /                           controllers.StudentController.create(request: Request)
PUT     /                           controllers.StudentController.update(request: Request)
DELETE  /:id                        controllers.StudentController.delete(id:Int)
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

Le point de terminaison/assets doit toujours être présent pour télécharger des ressources statiques.

Après cela, nous en avons terminé avec la création de l'APIStudent.

Pour en savoir plus sur la définition des mappages d'itinéraire, consultez notre didacticielRouting in Play Applications.

6. Essai

Nous pouvons maintenant exécuter des tests sur notre API en envoyant des requêtes àhttp://localhost:9000/ et en ajoutant le contexte approprié. L'exécution du chemin de base à partir du navigateur doit générer:

{
     "isSuccessful":true,
     "body":[]
}

Comme nous pouvons le voir, le corps est vide car nous n’avons pas encore ajouté d’enregistrements. En utilisantcurl, exécutons quelques tests (nous pouvons également utiliser un client REST comme Postman).

Ouvrons une fenêtre de terminal et exécutons la commande curl versadd a student:

curl -X POST -H "Content-Type: application/json" \
 -d '{"firstName":"John","lastName":"example","age": 18}' \
 http://localhost:9000/

Cela retournera l'étudiant nouvellement créé:

{
    "isSuccessful":true,
    "body":{
        "firstName":"John",
        "lastName":"example",
        "age":18,
        "id":0
    }
}

Après avoir exécuté le test ci-dessus, le chargement dehttp://localhost:9000 depuis le navigateur devrait maintenant nous donner:

{
    "isSuccessful":true,
    "body":[
        {
            "firstName":"John",
            "lastName":"example",
            "age":18,
            "id":0
        }
    ]
}

L'attributid sera incrémenté pour chaque nouvel enregistrement que nous ajoutons.

Àdelete a record, nous envoyons une requêteDELETE:

curl -X DELETE http://localhost:9000/0
{
    "isSuccessful":true,
    "body":"Student with id:0 deleted"
}

Dans le test ci-dessus, nous supprimons l’enregistrement créé lors du premier test, maintenantcreate it again so that we can test the update method:

curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"example","age": 18}' \
http://localhost:9000/
{
    "isSuccessful":true,
    "body":{
        "firstName":"John",
        "lastName":"example",
        "age":18,
        "id":0
    }
}

Voyons maintenantupdate the record en définissant le prénom sur "Andrew" et l'âge de 30 ans:

curl -X PUT -H "Content-Type: application/json" \
-d '{"firstName":"Andrew","lastName":"example","age": 30,"id":0}' \
http://localhost:9000/
{
    "isSuccessful":true,
    "body":{
        "firstName":"Andrew",
        "lastName":"example",
        "age":30,
        "id":0
    }
}

Le test ci-dessus démontre la modification de la valeur des champsfirstName andage après la mise à jour de l'enregistrement.

Créons quelques enregistrements factices supplémentaires, nous en ajouterons deux: Exemple de John Doe et Sam:

curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"Doe","age": 18}' \
http://localhost:9000/
curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"Sam","lastName":"example","age": 25}' \
http://localhost:9000/

Maintenant, récupérons tous les enregistrements:

curl -X GET http://localhost:9000/
{
    "isSuccessful":true,
    "body":[
        {
            "firstName":"Andrew",
            "lastName":"example",
            "age":30,
            "id":0
        },
        {
            "firstName":"John",
            "lastName":"Doe",
            "age":18,
            "id":1
        },
        {
            "firstName":"Sam",
            "lastName":"example",
            "age":25,
            "id":2
        }
    ]
}

Avec le test ci-dessus, nous vérifions le bon fonctionnement de l'action du contrôleurlistStudents.

7. Conclusion

Dans cet article, nous avons montré comment créer une API REST complète à l'aide de Play Framework.

Comme d'habitude, le code source de ce tutoriel est disponibleover on GitHub.