API REST com Play Framework em Java
1. Visão geral
O objetivo deste tutorial é explorar o Play Framework e aprender a construir serviços REST usando Java.
Vamos montar uma API REST para criar, recuperar, atualizar e excluir registros de alunos.
Nessas aplicações, normalmente teríamos um banco de dados para armazenar os registros dos alunos. O Play Framework possui um banco de dados H2 interno, juntamente com suporte para JPA com Hibernate e outras estruturas de persistência.
No entanto, para manter as coisas simples e focar nas coisas mais importantes, usaremos um mapa simples para armazenar objetos de alunos com IDs exclusivos.
2. Criar uma nova aplicação
Depois de instalar o Play Framework conforme descrito em nossoIntroduction to the Play Framework, estamos prontos para criar nosso aplicativo.
Vamos usar o comandosbt para criar um novo aplicativo chamadostudent-api usandoplay-java-seed:
sbt new playframework/play-java-seed.g8
3. Modelos
Com a estrutura de nosso aplicativo no lugar, vamos navegar parastudent-api/app/modelse criar um Java bean para lidar com as informações dos alunos:
public class Student {
private String firstName;
private String lastName;
private int age;
private int id;
// standard constructors, getters and setters
}
Agora vamos criar um armazenamento de dados simples - apoiado por umHashMap – para dados de alunos, com métodos auxiliares para realizar operações 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. Controladores
Vamos passar parastudent-api/app/controllerse criar um novo controlador chamadoStudentController.java. Percorreremos o código de forma incremental.
Primeiro, precisamosconfigure an *HttpExecutionContext*. Implementaremos nossas ações usando código assíncrono e não bloqueador. Isso significa que nossos métodos de ação retornarãoCompletionStage<Result> enão em vez de apenasResult. Isso tem o benefício de nos permitir escrever tarefas de longa execução sem bloquear.
Há apenas uma advertência ao lidar com programação assíncrona em um controlador Play Framework: temos que fornecer umHttpExecutionContext. Se não fornecermos o contexto de execução HTTP, obteremos o erro infame “Não há contexto HTTP disponível a partir daqui ”ao chamar o método de ação.
Vamos injetar:
private HttpExecutionContext ec;
private StudentStore studentStore;
@Inject
public StudentController(HttpExecutionContext ec, StudentStore studentStore) {
this.studentStore = studentStore;
this.ec = ec;
}
Observe que também adicionamos a areiaStudentStore injetada em ambos os campos no construtor do controlador usando a anotação@Inject . Feito isso, agora podemos prosseguir com a implementação dos métodos de ação.
Observe quePlay ships with Jackson to allow for data processing - então podemos importar quaisquer classes Jackson de que precisamos sem dependências externas.
Vamos definir uma classe de utilitário para realizar operações repetitivas. Nesse caso, construindo respostas HTTP.
Então, vamos criar o pacotestudent-api/app/utils e adicionarUtil.java nele:
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;
}
}
Com este método, criaremos respostas JSON padrão com uma chave booleanaisSuccessful e o corpo da resposta.
Agora podemos percorrer as ações da classe controller.
4.1. A açãocreate
Mapeado como uma açãoPOST , este método lida com a criação do objetoStudent:
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());
}
Usamos uma chamada do sclassHttp.Request injetado para colocar o corpo da solicitação na classeJsonNode de Jackson. Observe como usamos o método utilitário para criar uma resposta se o corpo fornull.
Também estamos retornando umCompletionStage<Result>, que nos permite escrever código sem bloqueio usando o métodoCompletedFuture.supplyAsync .
Podemos passar para ele qualquerString ouJsonNode, junto com um sinalizadorboolean para indicar o status.
Observe também como usamosJson.fromJson() para converter o objeto JSON de entrada em um objetoStudent e de volta para JSON para a resposta.
Finalmente, em vez deok() com o qual estamos acostumados, estamos usando o método auxiliarcreated do pacoteplay.mvc.results. A idéia é usar um método que forneça o status HTTP correto para a ação que está sendo executada em um contexto específico. Por exemplo,ok() para o status HTTP OK 200 ecreated() quando HTTP CRIADO 201 é o status de resultado usado acima. Esse conceito surgirá durante o restante das ações.
4.2. A açãoupdate
Uma solicitaçãoPUT parahttp://localhost:9000/ atinge o métodoStudentController.update, que atualiza as informações do aluno chamando o métodoupdateStudent deStudentStore:
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. A açãoretrieve
Para recuperar um aluno, passamos o id do aluno como um parâmetro de caminho em uma solicitaçãoGET parahttp://localhost:9000/:id. Isso atingirá a açãoretrieve :
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. A açãodelete
A açãodelete é mapeada parahttp://localhost:9000/:id. Fornecemosid para identificar qual registro excluir:
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. A açãolistStudents
Finalmente, a açãolistStudents retorna uma lista de todos os alunos que foram armazenados até agora. É mapeado parahttp://localhost:9000/ como uma solicitaçãoGET:
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. Mapeamentos
Tendo configurado nossas ações do controlador, podemos agora mapeá-las abrindo o arquivostudent-api/conf/routese adicionando estas rotas:
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)
O ponto de extremidade/assets deve estar sempre presente para fazer download de recursos estáticos.
Depois disso, concluímos a construção da APIStudent.
Para saber mais sobre como definir mapeamentos de rota, visite nosso tutorialRouting in Play Applications.
6. Teste
Agora podemos executar testes em nossa API enviando solicitações parahttp://localhost:9000/e adicionando o contexto apropriado. A execução do caminho base no navegador deve gerar:
{
"isSuccessful":true,
"body":[]
}
Como podemos ver, o corpo está vazio, pois ainda não adicionamos nenhum registro. Usandocurl, vamos executar alguns testes (como alternativa, podemos usar um cliente REST como o Postman).
Vamos abrir uma janela de terminal e executar o comando curl paraadd a student:
curl -X POST -H "Content-Type: application/json" \
-d '{"firstName":"John","lastName":"example","age": 18}' \
http://localhost:9000/
Isso retornará o aluno recém-criado:
{
"isSuccessful":true,
"body":{
"firstName":"John",
"lastName":"example",
"age":18,
"id":0
}
}
Depois de executar o teste acima, carregarhttp://localhost:9000 do navegador agora deve nos dar:
{
"isSuccessful":true,
"body":[
{
"firstName":"John",
"lastName":"example",
"age":18,
"id":0
}
]
}
O atributoid será incrementado para cada novo registro que adicionarmos.
Paradelete a record, enviamos uma solicitaçãoDELETE:
curl -X DELETE http://localhost:9000/0
{
"isSuccessful":true,
"body":"Student with id:0 deleted"
}
No teste acima, excluímos o registro criado no primeiro teste, agora vamoscreate 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
}
}
Vamos agoraupdate the record definindo o primeiro nome como “André” e a idade como 30:
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
}
}
O teste acima demonstra a mudança no valor dos camposfirstName andage após a atualização do registro.
Vamos criar alguns registros fictícios extras, vamos adicionar dois: John Doe e Sam exemplo:
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/
Agora, vamos obter todos os registros:
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
}
]
}
Com o teste acima, estamos verificando o funcionamento adequado da ação do controladorlistStudents.
7. Conclusão
Neste artigo, mostramos como construir uma API REST completa usando o Play Framework.
Como de costume, o código-fonte deste tutorial está disponívelover on GitHub.