API REST con Play Framework in Java

1. Panoramica

Lo scopo di questo tutorial è esplorare il Play Framework e imparare a creare servizi REST con esso utilizzando Java.

Metteremo insieme un'API REST per creare, recuperare, aggiornare ed eliminare i record degli studenti.

In tali applicazioni, normalmente avremmo un database per archiviare i record degli studenti. Play Framework ha un database H2 integrato, insieme al supporto per JPA con Hibernate e altri framework di persistenza.

Tuttavia, per mantenere le cose semplici e concentrarsi sulle cose più importanti, useremo una semplice mappa per memorizzare gli oggetti degli studenti con ID univoci.

2. Creare una nuova applicazione

Dopo aver installato Play Framework come descritto nella nostra Introduzione al Play Framework, siamo pronti per creare la nostra applicazione.

Usiamo il comando sbt per creare una nuova applicazione chiamata student-api usando play-java-seed :

sbt new playframework/play-java-seed.g8

3. Modelli

Con lo scaffolding dell'applicazione in atto, passiamo a student-api / app / models e creiamo un Java bean per la gestione delle informazioni sugli studenti:

public class Student { private String firstName; private String lastName; private int age; private int id; // standard constructors, getters and setters }

Creeremo ora un semplice archivio dati, supportato da una HashMap, per i dati degli studenti, con metodi di supporto per eseguire operazioni 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. Controller

Andiamo su student-api / app / controllers e creiamo un nuovo controller chiamato StudentController.java . Passeremo attraverso il codice in modo incrementale.

Prima di tutto, dobbiamo configurare un HttpExecutionContext . Implementeremo le nostre azioni utilizzando codice asincrono e non bloccante. Ciò significa che i nostri metodi di azione restituiranno CompletionStage anziché solo Result . Questo ha il vantaggio di permetterci di scrivere attività di lunga durata senza bloccarle.

C'è solo un avvertimento quando si ha a che fare con la programmazione asincrona in un controller Play Framework: dobbiamo fornire un HttpExecutionContext. Se non forniamo il contesto di esecuzione HTTP, riceveremo il famigerato errore "Non è disponibile alcun contesto HTTP da qui" quando chiamiamo il metodo di azione.

Iniettiamolo:

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

Si noti che abbiamo anche aggiunto StudentStore e inserito entrambi i campi nel costruttore del controller utilizzando l' annotazione @Inject . Fatto ciò, possiamo ora procedere con l'implementazione dei metodi di azione.

Tieni presente che Play viene fornito con Jackson per consentire l'elaborazione dei dati, quindi possiamo importare qualsiasi classe Jackson di cui abbiamo bisogno senza dipendenze esterne.

Definiamo una classe di utilità per eseguire operazioni ripetitive. In questo caso, creazione di risposte HTTP.

Quindi creiamo il pacchetto student-api / app / utils e aggiungiamo Util.java al suo interno:

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

Con questo metodo, creeremo risposte JSON standard con una chiave booleana isSuccessful e il corpo della risposta.

Ora possiamo passare attraverso le azioni della classe controller.

4.1. L' azione di creazione

Mappato come azione POST , questo metodo gestisce la creazione dell'oggetto Student :

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

Usiamo una chiamata dalla classe Http.Request iniettata per ottenere il corpo della richiesta nella classe JsonNode di Jackson . Nota come usiamo il metodo di utilità per creare una risposta se il corpo è nullo .

Stiamo anche restituendo un CompletionStage , che ci consente di scrivere codice non bloccante utilizzando il metodo CompletedFuture.supplyAsync .

Possiamo passare ad esso qualsiasi stringa o JsonNode , insieme a un flag booleano per indicare lo stato.

Si noti anche come utilizziamo Json.fromJson () per convertire l'oggetto JSON in arrivo in un oggetto Student e di nuovo in JSON per la risposta.

Infine, invece di ok () a cui siamo abituati, stiamo usando il metodo di supporto creato dal pacchetto play.mvc.results . L'idea è di utilizzare un metodo che fornisca lo stato HTTP corretto per l'azione eseguita in un contesto particolare. Ad esempio, ok () per lo stato HTTP OK 200 e created () quando HTTP CREATED 201 è lo stato del risultato come usato sopra. Questo concetto emergerà durante il resto delle azioni.

4.2. L' azione di aggiornamento

Una richiesta PUT a // localhost: 9000 / colpisce StudentController. metodo update , che aggiorna le informazioni sugli studenti chiamando il metodo updateStudent di StudentStore :

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' azione di recupero

Per recuperare uno studente, passiamo l'id dello studente come parametro del percorso in una richiesta GET a // localhost: 9000 /: id . Questo attiverà l' azione di recupero :

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' azione di eliminazione

L' azione di eliminazione è associata a // localhost: 9000 /: id . Forniamo l' id per identificare quale record eliminare:

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. The listStudents Action

Finally, the listStudents action returns a list of all the students that have been stored so far. It's mapped to //localhost:9000/ as a GET request:

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

Having set up our controller actions, we can now map them by opening the file student-api/conf/routes and adding these 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)

The /assets endpoint must always be present for downloading static resources.

After this, we're done with building the Student API.

To learn more about defining route mappings, visit our Routing in Play Applications tutorial.

6. Testing

We can now run tests on our API by sending requests to //localhost:9000/ and adding the appropriate context. Running the base path from the browser should output:

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

As we can see, the body is empty since we haven't added any records yet. Using curl, let's run some tests (alternatively, we can use a REST client like Postman).

Let's open up a terminal window and execute the curl command to add a student:

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

This will return the newly created student:

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

After running the above test, loading //localhost:9000 from the browser should now give us:

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

The id attribute will be incremented for every new record we add.

To delete a record we send a DELETE request:

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

In the above test, we delete the record created in the first test, now let's create it again so that we can test the update method:

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

Let's now update the record by setting the first name to “Andrew” and age to 30:

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

The above test demonstrates the change in the value of the firstName and age fields after updating the record.

Let's create some extra dummy records, we'll add two: John Doe and Sam Baeldung:

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

Now, let's get all the records:

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

With the above test, we are ascertaining the proper functioning of the listStudents controller action.

7. Conclusion

In questo articolo, abbiamo mostrato come creare un'API REST a tutti gli effetti utilizzando il Framework di gioco.

Come al solito, il codice sorgente di questo tutorial è disponibile su GitHub.