Следующая статья представляет собой руководство по написанию микросервиса Spring Boot на основе интеграции Spring Data REST и HATEOAS API. В качестве основы нашего проекта мы будем использовать образец, написанный Грегом Тернквистом, одним из авторов справочной документации Spring HATEOAS и справочника REST Spring Data, перечисленных в справочном разделе.
Если вы уже знакомы с примером, не стесняйтесь пропустить следующие абзацы и перейти непосредственно к разделу CustomOrderHateaosController , где мы описываем наш подход к интеграции Spring Data REST и HATEOAS, отличие от реализации, представленной в примере, и ее выгоды. В противном случае, мы рекомендуем вам прочитать следующие подробные описания проекта и его целей.
Завершите проект Maven с примерами кода
В следующем репозитории Github есть проект со всем кодом, который мы собираемся представить дальше.
Структура кода проекта
Проект имеет следующую структуру.
Все перечисленные классы, кроме CustomOrderHateoasContoller.java и KievSpringRestDataAndHateoasApplication.java, были написаны Грегом Тернквистом.
Вам также может понравиться:
REST API — Что такое HATEOAS?
Maven Project Зависимости
Проект имеет следующие зависимости Maven.
Ниже приведен соответствующий фрагмент из файла проекта POM.
XML
xxxxxxxxxx
1
<dependencies>
2
<dependency>
3
<groupId>org.springframework.boot</groupId>
4
<artifactId>spring-boot-starter-data-jpa</artifactId>
5
</dependency>
6
<dependency>
7
<groupId>org.springframework.boot</groupId>
8
<artifactId>spring-boot-starter-data-rest</artifactId>
9
</dependency>
10
<dependency>
11
<groupId>org.springframework.boot</groupId>
12
<artifactId>spring-boot-starter-hateoas</artifactId>
13
</dependency>
14
<dependency>
16
<groupId>com.h2database</groupId>
17
<artifactId>h2</artifactId>
18
<scope>runtime</scope>
19
</dependency>
20
<dependency>
21
<groupId>org.springframework.boot</groupId>
22
<artifactId>spring-boot-starter-test</artifactId>
23
<scope>test</scope>
24
<exclusions>
25
<exclusion>
26
<groupId>org.junit.vintage</groupId>
27
<artifactId>junit-vintage-engine</artifactId>
28
</exclusion>
29
</exclusions>
30
</dependency>
31
</dependencies>
KievSpringRestDataAndHateoasApplication.java
В этом классе нет ничего особенного. Это класс Spring Boot Application, и вы уже видели подобный код много раз.
Джава
xxxxxxxxxx
1
package com.example.demo;
2
import org.springframework.boot.SpringApplication;
4
import org.springframework.boot.autoconfigure.SpringBootApplication;
5
7
public class KievSpringRestDataAndHateoasApplication {
8
public static void main(String[] args) {
10
SpringApplication.run(KievSpringRestDataAndHateoasApplication.class, args);
11
}
12
}
Цели проекта
На этом этапе вы ожидаете, что мы поговорим о втором и наиболее важном классе класса CustomOrderHateoasController.java . В конце концов, это место, где происходит вся интеграция, и этот факт делает его центром внимания руководства. Не волнуйтесь, мы это сделаем, но сначала мы должны прояснить ситуацию и получить четкое представление о целях нашего проекта. Как вы помните, мы не пишем наш проект с нуля, а строим его на основе уже существующего образца . Это дает нам возможность для повторного использования. Ниже приведены выдержки из оригинального документа README, смешанные с нашими замечаниями.
Определение проблемы
ПРОБЛЕМА: Вы хотите реализовать концепцию заказов. Эти заказы имеют определенные коды состояния, которые определяют, какие переходы может предпринять система, например, заказ не может быть выполнен до тех пор, пока он не оплачен, и выполненный заказ не может быть отменен.
РЕШЕНИЕ . Необходимо кодировать набор кодов OrderStatus и применять их с помощью настраиваемого контроллера Spring Web MVC. Этот контроллер должен иметь маршруты, которые отображаются рядом с маршрутами, предоставленными Spring Data REST.
Опишем основы системы заказов. И это начинается с объекта домена:
Джава
xxxxxxxxxx
1
2
name = "ORDERS") // (1) (
3
class Order {
4
6
private Long id; // (2)
7
private OrderStatus orderStatus; // (3)
9
private String description; // (4)
11
private Order() {
13
this.id = null;
14
this.orderStatus = OrderStatus.BEING_CREATED;
15
this.description = "";
16
}
17
public Order(String description) {
19
this();
20
this.description = description;
21
}
22
...
24
}
Следующим шагом в определении вашего домена является определение OrderStatus
. Предполагая, что мы хотим создать общий поток создания заказа ⇒ оплатить заказ ⇒ выполнить заказ с возможностью отмены, только если вы еще не оплатили его, это будет хорошо выполнено:
Джава
xxxxxxxxxx
1
public enum OrderStatus {
2
BEING_CREATED, PAID_FOR, FULFILLED, CANCELLED;
4
/**
6
* Verify the transition between {@link OrderStatus} is valid.
7
*
8
* NOTE: This is where any/all rules for state transitions should be kept and enforced.
9
*/
10
static boolean valid(OrderStatus currentStatus, OrderStatus newStatus) {
11
if (currentStatus == BEING_CREATED) {
13
return newStatus == PAID_FOR || newStatus == CANCELLED;
14
} else if (currentStatus == PAID_FOR) {
15
return newStatus == FULFILLED;
16
} else if (currentStatus == FULFILLED) {
17
return false;
18
} else if (currentStatus == CANCELLED) {
19
return false;
20
} else {
21
throw new RuntimeException("Unrecognized situation.");
22
}
23
}
24
}
Вверху находятся актуальные состояния. Внизу статический метод проверки. Именно здесь определяются правила перехода состояний и где остальная часть системы должна искать, чтобы определить, является ли переход действительным.
Последний шаг, который нужно предпринять, - это определение репозитория Spring Data:
Джава
xxxxxxxxxx
1
public interface OrderRepository extends CrudRepository<Order, Long> {
2
}
Этот репозиторий расширяет CrudRepository Spring Data Commons, заполняя домен и типы ключей (Order и Long).
Следующий класс DatabaseLoader.java предварительно загружает некоторые данные тестирования.
Джава
xxxxxxxxxx
1
/**
2
* @author Greg Turnquist
3
*/
4
6
public class DatabaseLoader {
7
9
CommandLineRunner init(OrderRepository repository) {
10
return args -> {
12
repository.save(new Order("grande mocha"));
13
repository.save(new Order("venti hazelnut machiatto"));
14
};
15
}
16
}
Примечания: Авторы считают, что аннотацию @Configuration следует использовать вместо @Component, но мы старались максимально использовать код нашего фонда, и именно поэтому мы решили оставить аннотацию @Component на месте.
Следующий файл ресурсов показывает, как мы изменяем корневой путь микросервиса.
Пример 1 src/main/resources/application.yml
Джава
xxxxxxxxxx
1
spring:
2
data:
3
rest:
4
base-path: /api
Следует отметить, что прямо здесь вы можете запустить свое приложение. Spring Boot запустит веб-контейнер, предварительно загрузит данные, а затем переведет Spring Data REST в режим онлайн. Spring Data REST со всеми его предварительно созданными маршрутами на основе гипермедиа будет отвечать на вызовы для создания, замены, обновления и удаления Order
объектов.
Но Spring Data REST ничего не будет знать о допустимых и недопустимых переходах состояний. Его встроенные ссылки помогут вам перейти от нашего API к сводному корню для всех заказов, к отдельным записям и обратно. Но не будет концепции оплаты, выполнения или отмены заказов - по крайней мере, не встроенной в гипермедиа. Единственные подсказки, которые могут иметь конечные пользователи, - это полезные данные существующих заказов. И это не эффективно. Лучше создать несколько дополнительных операций и затем обслуживать их ссылки, когда это необходимо.
Замечания: На этом мы завершаем описание наших предварительных шагов. На данный момент вы должны знать цели проекта. Вы знаете, что нужно сделать, поэтому давайте посмотрим, как это можно сделать. Давайте наконец поговорим об интеграции Spring Data REST и HATEOAS. Давайте поговорим о пользовательском контроллере CustomOrderHateoasController!
CustomOrderHateoasController
Вот и все! Большие времена! Итак, вот наш пользовательский контроллер, где все происходит и все готово!
Джава
xxxxxxxxxx
1
2
public class CustomOrderHateoasController implements RepresentationModelProcessor<EntityModel<Order>>{...}
Давайте рассмотрим контроллер.
@BasePathAwareController
аннотация объявляет контроллер, в котором сопоставления запросов дополняются базовым URI в конфигурации REST Spring Data. Другими словами, наш пользовательский контроллер зарегистрирован под базовым путем / api .
Контроллер имеет конечную частную переменную экземпляра, OrderPepository
которая инициализируется с помощью инжектора конструктора.
Контейнер имеет три открытых методов, pay
, cancel
, и fulfill
. Эти три метода являются основной причиной существования контроллера, поскольку они отвечают за оплату, отмену и выполнение заказа. Реализация этих методов была взята из CustomOrderController.java, написанного Грегом Тернквистом. В этих методах нет ничего особенного, и они не связаны с интеграцией HATEOAS. Ниже приведен код, который мы обсуждали до сих пор:
Джава
xxxxxxxxxx
1
private final OrderRepository repository;
2
public CustomOrderHateoasController(OrderRepository repository) {
4
this.repository = repository;
5
}
6
("/orders/{id}/pay")
8
ResponseEntity<?> pay( Long id) {
9
Order order = this.repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id));
11
if (valid(order.getOrderStatus(), OrderStatus.PAID_FOR)) {
13
order.setOrderStatus(OrderStatus.PAID_FOR);
15
return ResponseEntity.ok(repository.save(order));
16
}
17
return ResponseEntity.badRequest()
19
.body("Transitioning from " + order.getOrderStatus() + " to " + OrderStatus.PAID_FOR + " is not valid.");
20
}
21
("/orders/{id}/cancel")
23
ResponseEntity<?> cancel( Long id) {
24
Order order = this.repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id));
26
if (valid(order.getOrderStatus(), OrderStatus.CANCELLED)) {
28
order.setOrderStatus(OrderStatus.CANCELLED);
30
return ResponseEntity.ok(repository.save(order));
31
}
32
return ResponseEntity.badRequest()
34
.body("Transitioning from " + order.getOrderStatus() + " to " + OrderStatus.CANCELLED + " is not valid.");
35
}
36
("/orders/{id}/fulfill")
38
ResponseEntity<?> fulfill( Long id) {
39
Order order = this.repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id));
41
if (valid(order.getOrderStatus(), OrderStatus.FULFILLED)) {
43
order.setOrderStatus(OrderStatus.FULFILLED);
45
return ResponseEntity.ok(repository.save(order));
46
}
47
return ResponseEntity.badRequest()
49
.body("Transitioning from " + order.getOrderStatus() + " to " + OrderStatus.FULFILLED + " is not valid.");
50
}
Теперь, когда мы знаем основное назначение контроллера, давайте поговорим о том, как происходит интеграция с HATEOAS. Но сначала давайте остановимся и обсудим, чего мы пытаемся достичь. Проще говоря, наша цель состоит в том, чтобы добавить дополнительные ссылки к существующим ссылкам, созданным нашим Spring Data REST API.
Наш пользовательский контроллер реализует этот интерфейс и имеет реализацию метода
process
. Прежде чем разбить этот код, давайте на минуту остановимся еще раз и поговорим о преимуществах ответственности контроллера за реализацию интерфейса по сравнению с созданием отдельного процессора. Впримере проекта был выбран последний подход, в котором использовался отдельный класс
OrderProcessor.java . Основным преимуществом превращения контроллера в процессор является объединение всего связанного кода в одном месте.
Итак, если вы думаете о поддержке вашего кода после того, как вы покинете проект, мы будем настаивать на нашем подходе. В противном случае, каждый раз, когда новый разработчик меняет / реорганизует контроллер, он или она должны помнить, что нужно перейти к отдельному процессору и изменить его соответствующим образом. И правда в том, что эта бедная душа может не знать о существовании процессора. Кстати, если вы заранее думаете о поддержке своего кода, вы хороший человек. Шутки в сторону!
И теперь пришло время поговорить о предложенной нами process
реализации метода .
Джава
xxxxxxxxxx
1
2
public EntityModel<Order> process(EntityModel<Order> model) {
3
Links links = model.getLinks();
4
List<Link> listLink = links.toList();
5
if (listLink==null || listLink.isEmpty()) {
6
//just to be sure that there is at least one link in the list
7
return model;
8
}
9
10
Link self = listLink.get(0);
11
String href = self.getHref();
12
13
// If PAID_FOR is valid, add a link to the `pay()` method
14
if (valid(model.getContent().getOrderStatus(), OrderStatus.PAID_FOR)) {
15
Link paidLink = new Link(String.format("%s/%s", href,"pay"),"payment");
16
model.add(paidLink);
17
}
18
// If CANCELLED is valid, add a link to the `cancel()` method
20
if (valid(model.getContent().getOrderStatus(), OrderStatus.CANCELLED)) {
21
Link cancelLink = new Link(String.format("%s/%s", href,"cancel"),"cancel");
22
model.add(cancelLink);
23
}
24
// If FULFILLED is valid, add a link to the `fulfill()` method
26
if (valid(model.getContent().getOrderStatus(), OrderStatus.FULFILLED)) {
27
Link fulfilLink = new Link(String.format("%s/%s", href,"fulfill"),"fulfill");
28
model.add(fulfilLink);
29
}
30
return model;
32
}
Помните, что основной целью этого метода является создание дополнительных ссылок при определенных условиях. Допустим, нам нужно создать дополнительную ссылку payment
(строка 16).
Простой текст
xxxxxxxxxx
1
"self" : {
2
"href" : "http://localhost:8080/api/orders/1"
3
},
4
...,
5
"payment" : {
6
"href" : "http://localhost:8080/api/orders/1/pay"
7
},
8
…
Как видим, метод, process
, вызывается с аргументом , EntityModel<Order> model
который является представлением , чтобы быть изменена , если это необходимо , и возвращен из метода путем дальнейшей обработки в рамках Spring (а именно сериализации). Это значит, что мы должны сконструировать Link
объект и добавить его в модель (строка 16).
Дополнение тривиально; мы используем метод add
, представленный EntityModel
классом. Чтобы построить Link
, мы можем использовать следующий конструктор public Link(String href, String rel)
, где href
- это требуемый URI, а это rel
- отношение. Другими словами, наш href
должен быть " HTTP: // локальный: 8080 / API / заказы / 1 / оплаты , " и rel
есть payment
. Итак, теперь вопрос заключается в том, как href
легко создать URI. Ответ заключается в model
объекте, предоставленном нам в качестве аргумента средой Spring.
Снимок экрана отладчика четко указывает на то, что модель (наше Order
представление ресурса ) уже была изменена платформой, и ее коллекция ссылок не пуста. Кроме того, первый элемент - это ссылка на себя. Мы почти в конце нашей презентации здесь. Все, что нам нужно сделать, это объединить это href
с « / pay » (строка 15), создать Link
экземпляр, добавить его в модель и вернуть измененное представление в платформу для дальнейшей сериализации. И это все, ребята! Мы можем запустить приложение Spring Boot и выполнить шаги, описанные в документе README .
Простой текст
xxxxxxxxxx
1
$ curl localhost:8080/api/orders/1 {"orderStatus" : "BEING_CREATED","description" : "grande mocha","_links" : {
2
"self" : {
3
"href" : "http://localhost:8080/api/orders/1"
4
},
5
"order" : {
6
"href" : "http://localhost:8080/api/orders/1"
7
},
8
"payment" : {
9
"href" : "http://localhost:8080/api/orders/1/pay"
10
},
11
"cancel" : {
12
"href" : "http://localhost:8080/api/orders/1/cancel"
13
}
14
}
16
====
19
Apply the payment link:
21
====
23
$ curl -X POST localhost:8080/api/orders/1/pay {"id" : 1,"orderStatus" : "PAID_FOR","description" : "grande mocha" }
26
$ curl localhost:8080/api/orders/1 {"orderStatus" : "PAID_FOR","description" : "grande mocha","_links" : {
28
"self" : {
29
"href" : "http://localhost:8080/api/orders/1"
30
},
31
"order" : {
32
"href" : "http://localhost:8080/api/orders/1"
33
},
34
"fulfill" : {
35
"href" : "http://localhost:8080/api/orders/1/fulfill"
36
}
37
}
39
====
42
The `pay` and `cancel` links have disappeared, replaced with a `fulfill` link.
44
Fulfill the order and see the final state:
45
====
47
$ curl -X POST localhost:8080/api/orders/1/fulfill {"id" : 1,"orderStatus" : "FULFILLED","description" : "grande mocha" }
50
$ curl localhost:8080/api/orders/1 {"orderStatus" : "FULFILLED","description" : "grande mocha","_links" : {
52
"self" : {
53
"href" : "http://localhost:8080/api/orders/1"
54
},
55
"order" : {
56
"href" : "http://localhost:8080/api/orders/1"
57
}
58
}
60
====
63
The first drink order has been fulfilled.
65
If you cancel the second order, you can see what _it's_ links tell you:
66
====
68
$ curl localhost:8080/api/orders/2 {"orderStatus" : "BEING_CREATED","description" : "venti hazelnut machiatto","_links" : {
71
"self" : {
72
"href" : "http://localhost:8080/api/orders/2"
73
},
74
"order" : {
75
"href" : "http://localhost:8080/api/orders/2"
76
},
77
"payment" : {
78
"href" : "http://localhost:8080/api/orders/2/pay"
79
},
80
"cancel" : {
81
"href" : "http://localhost:8080/api/orders/2/cancel"
82
}
83
}
85
$ curl -X POST localhost:8080/api/orders/2/cancel {"id" : 2,"orderStatus" : "CANCELLED","description" : "venti hazelnut machiatto" }
87
$ curl localhost:8080/api/orders/2 {"orderStatus" : "CANCELLED","description" : "venti hazelnut machiatto","_links" : {
89
"self" : {
90
"href" : "http://localhost:8080/api/orders/2"
91
},
92
"order" : {
93
"href" : "http://localhost:8080/api/orders/2"
94
}
95
C АКЛЮЧЕНИЕ
Мы достигли цели нашего проекта. На этом мы завершаем наше руководство о том, как написать микросервис с помощью Spring Boot, используя Spring Data REST и HATEOAS API.
Стоит также отметить, что мы повторно использовали тестовый пример OrderIntegrationTest.java, написанный Грегом Тернквистом, и за это мы ему благодарны !.
Авторы надеются, что вы найдете предложенный подход разумным и, что самое важное, полезным в ваших собственных усилиях по разработке. Так что дайте нам знать, если вы нашли чтение информативным, и дайте нам знать ваши мысли по этому вопросу в разделе комментариев.