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 .