API REST com Play Framework em Java

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.