Перейти к основному содержимому

REST API с Play Framework на Java

· 8 мин. чтения

1. Обзор

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

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

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

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

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

После того, как мы установили Play Framework, как описано в нашем Введении в 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<Integer, Student> students = new HashMap<>();

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

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

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

public Optional<Student> 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 . Мы будем шаг за шагом выполнять код.

Во-первых, нам нужно настроить 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 поставляется с Джексоном для обработки данных, поэтому мы можем импортировать любые классы Джексона, которые нам нужны, без внешних зависимостей.

Давайте определим служебный класс для выполнения повторяющихся операций. В этом случае построение 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. Действие создания _

Сопоставленный как действие POST , этот метод обрабатывает создание объекта Student :

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

Optional<Student> 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 вместе с логическим флагом для указания статуса.

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

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

4.2. Действие обновления _

Запрос PUT на http://localhost:9000/ попадает в StudentController. update , который обновляет информацию о студентах, вызывая метод updateStudent для StudentStore :

public CompletionStage<Result> update(Http.Request request) {
JsonNode json = request.body().asJson();
return supplyAsync(() -> {
if (json == null) {
return badRequest(Util.createResponse("Expecting Json data", false));
}
Optional<Student> 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. Действие извлечения _

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

public CompletionStage<Result> retrieve(int id) {
return supplyAsync(() -> {
final Optional<Student> 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. Действие удаления _

Действие удаления сопоставляется с http://localhost:9000/:id . Мы предоставляем идентификатор , чтобы определить, какую запись удалить:

public CompletionStage<Result> 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<Result> listStudents() {
return supplyAsync(() -> {
Set<Student> 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 всегда должна присутствовать для загрузки статических ресурсов.

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

Чтобы узнать больше об определении сопоставлений маршрутов, посетите наш учебник «Маршрутизация в приложениях Play» .

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

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

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

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

Давайте откроем окно терминала и выполним команду curl, чтобы добавить студента :

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

Это вернет вновь созданного ученика:

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

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

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

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

Для удаления записи отправляем запрос DELETE :

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

В приведенном выше тесте мы удаляем запись, созданную в первом тесте, теперь давайте создадим ее снова, чтобы мы могли протестировать метод обновления :

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

Давайте теперь обновим запись , установив имя на «Эндрю» и возраст на 30:

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

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

Давайте создадим несколько дополнительных фиктивных записей, добавим две: John Doe и Sam ForEach:

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":"ForEach","age": 25}' \
http://localhost:9000/

Теперь давайте получим все записи:

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

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

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

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

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