REST API с Play Framework в Java

REST API с Play Framework в Java

1. обзор

Цель этого руководства - изучить платформу Play Framework и узнать, как с ее помощью создавать службы REST с использованием Java.

Мы создадим REST API для создания, извлечения, обновления и удаления студенческих записей.

В таких приложениях у нас обычно была бы база данных для хранения студенческих записей. Play Framework имеет встроенную базу данных H2, а также поддержку JPA с Hibernate и другими средами персистентности.

Однако, чтобы упростить задачу и сосредоточиться на наиболее важных вещах, мы будем использовать простую карту для хранения объектов учащихся с уникальными идентификаторами.

2. Создать новое приложение

После того, как мы установили Play Framework, как описано в нашихIntroduction to the Play Framework, мы готовы создавать наше приложение.

Давайте воспользуемся командойsbt, чтобы создать новое приложение с именемstudent-api, используяplay-java-seed:

sbt new playframework/play-java-seed.g8

3. модели

Создав каркас нашего приложения, давайте перейдем кstudent-api/app/models и создадим Java-компонент для обработки информации о студентах:

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

    // standard constructors, getters and setters
}

Теперь мы создадим простое хранилище данных, поддерживаемоеHashMap – для данных учащихся, с вспомогательными методами для выполнения операций 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. Контроллеры

Давайте перейдем кstudent-api/app/controllers и создадим новый контроллер с именемStudentController.java. Мы будем выполнять код постепенно.

Во-первых, нам нужноconfigure an *HttpExecutionContext*.. Мы будем реализовывать наши действия, используя асинхронный неблокирующий код. Это означает, что наши методы действия вернутCompletionStage<Result>  вместо простоResult. Это имеет то преимущество, что позволяет нам писать долгосрочные задачи без блокировки.

При работе с асинхронным программированием в контроллере Play Framework есть только одно предостережение: мы должны предоставитьHttpExecutionContext.. Если мы не предоставим контекст выполнения HTTP, мы получим печально известную ошибку «Нет контекста HTTP. доступно отсюда »при вызове метода действия.

Давайте введем это:

private HttpExecutionContext ec;
private StudentStore studentStore;

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

Обратите внимание, что мы также добавили песокStudentStore , введенный в оба поля в конструкторе контроллера, используя саннотацию@Inject . Сделав это, мы можем приступить к реализации методов действия.

Обратите внимание, чтоPlay ships with Jackson to allow for data processing - поэтому мы можем импортировать любые классы Джексона, которые нам нужны, без внешних зависимостей.

Давайте определим служебный класс для выполнения повторяющихся операций. В этом случае строятся HTTP-ответы.

Итак, давайте создадим пакетstudent-api/app/utils и добавим в негоUtil.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;
    }
}

С помощью этого метода мы будем создавать стандартные ответы JSON с логическим ключомisSuccessful и телом ответа.

Теперь мы можем выполнить действия класса контроллера.

4.1. Действиеcreate

Отображенный как секцияPOST , этот метод обрабатывает создание объекта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());
}

Мы используем вызов из внедренного классаHttp.Request , чтобы передать тело запроса в класс ДжексонаJsonNode. Обратите внимание, как мы используем служебный метод для создания ответа, если телоnull.

Мы также возвращаемCompletionStage<Result>, что позволяет нам писать неблокирующий код с использованием методаCompletedFuture.supplyAsync .

Мы можем передать ему любойString илиJsonNode вместе с флагомboolean для обозначения статуса.

Также обратите внимание, как мы используемJson.fromJson() для преобразования входящего объекта JSON в объектStudent и обратно в JSON для ответа.

Наконец, вместоok(), к которому мы привыкли, мы используем вспомогательный методcreated из пакетаplay.mvc.results. Идея состоит в том, чтобы использовать метод, который дает правильный HTTP-статус для действия, выполняемого в определенном контексте. Например,ok() для статуса HTTP OK 200 иcreated(), когда HTTP CREATED 201 является статусом результата, как указано выше. Эта концепция возникнет во всех остальных действиях.

4.2. Действиеupdate

SrequestPUT кhttp://localhost:9000/ попадает в методStudentController.update, который обновляет информацию об учащемся, вызывая методupdateStudent  для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. Действиеretrieve

Чтобы получить студента, мы передаем идентификатор студента в качестве параметра пути в запросеGET кhttp://localhost:9000/:id. Это попадет в секциюretrieve :

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. Действиеdelete

Секцияdelete отображается вhttp://localhost:9000/:id. Мы предоставляемid, чтобы определить, какую запись удалить:

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. ДействиеlistStudents

Наконец, действиеlistStudents возвращает список всех студентов, которые были сохранены на данный момент. Он отображается наhttp://localhost:9000/ как запросGET:

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. Отображения

Настроив действия нашего контроллера, мы можем теперь сопоставить их, открыв файлstudent-api/conf/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)

Конечная точка/assets всегда должна присутствовать для загрузки статических ресурсов.

После этого мы закончили создание APIStudent.

Чтобы узнать больше об определении сопоставлений маршрутов, посетите наш учебникRouting in Play Applications.

6. тестирование

Теперь мы можем запускать тесты в нашем API, отправляя запросы кhttp://localhost:9000/ и добавляя соответствующий контекст. Запуск базового пути из браузера должен вывести:

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

Как видим, тело пусто, так как мы еще не добавили никаких записей. Используяcurl, давайте запустим несколько тестов (в качестве альтернативы мы можем использовать клиент REST, например Postman).

Давайте откроем окно терминала и выполним команду curl дляadd a student:

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

После выполнения вышеуказанного теста загрузкаhttp://localhost:9000 из браузера должна дать нам:

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

Атрибутid будет увеличиваться для каждой новой добавляемой записи.

Наdelete a record отправляем запросDELETE:

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

В приведенном выше тесте мы удаляем запись, созданную в первом тесте, теперь давайтеcreate 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
    }
}

Давайте теперьupdate the record, установив имя «Эндрю» и возраст 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
    }
}

Приведенный выше тест демонстрирует изменение значения полейfirstName andage после обновления записи.

Давайте создадим несколько дополнительных фиктивных записей, мы добавим две: пример Джона Доу и Сэма:

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/

А теперь давайте возьмем все записи:

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

С помощью вышеуказанного теста мы проверяем правильность функционирования действия контроллераlistStudents.

7. Заключение

В этой статье мы показали, как создать полноценный REST API с помощью Play Framework.

Как обычно, доступен исходный код этого руководстваover on GitHub.