Spring Boot 2.0 недавно вышел на GA, поэтому я решил написать свой первый пост о Spring довольно долго. С момента выпуска я все больше и больше упоминал о Spring WebFlux вместе с учебными пособиями по его использованию. Но, прочитав их и попробовав, чтобы все заработало самостоятельно, я обнаружил, что сделать переход от кода, включенного в публикации и учебные пособия, которые я прочитал, к написанию кода, который на самом деле делает что-то чуть более интересное, чем возвращение строки, оказалось довольно сложно. от конца Теперь я надеюсь, что не буду стрелять себе в ногу, сказав, что, как вы, вероятно, можете подвергнуть той же критике код, который я использую в этом посте, но вот моя попытка дать учебник по Spring WebFlux, который на самом деле напоминает то, что вы могли бы использовать в дикой природе.
Прежде чем я продолжу, и после всего этого упоминания о WebFlux, что это на самом деле? Spring WebFlux — полностью неблокирующая реактивная альтернатива Spring MVC. Это позволяет лучше вертикальное масштабирование без увеличения ваших аппаратных ресурсов. Будучи реактивным, теперь он использует Reactive Streams, чтобы разрешить асинхронную обработку данных, возвращаемых при обращениях к серверу. Это означает, что мы увидим намного меньше List
s, Collection
s или даже отдельных объектов и вместо них их реактивные эквиваленты, такие как Flux
и Mono
(из Reactor). Я не собираюсь вдаваться в подробности о том, что такое Reactive Streams, так как, честно говоря, мне нужно самому изучить его еще больше, прежде чем пытаться объяснить это кому-либо. Вместо этого давайте вернемся к сосредоточению на WebFlux.
Я использовал Spring Boot для написания кода в этом уроке, как обычно.
Ниже приведены зависимости, которые я использовал в этом посте.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
< dependencies > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-webflux</ artifactId > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-data-cassandra-reactive</ artifactId > < version >2.0.0.RELEASE</ version > </ dependency > </ dependencies > |
Хотя я не включил его в приведенный выше фрагмент зависимости, используется spring-boot-starter-parent
, который в конечном итоге может быть обновлен до версии 2.0.0.RELEASE
. Этот урок посвящен WebFlux, в том числе Spring spring-boot-starter-webflux
, очевидно, хорошая идея. spring-boot-starter-data-cassandra-reactive
также был включен, так как мы будем использовать его в качестве базы данных для примера приложения, поскольку это одна из немногих баз данных, которые имеют реактивную поддержку (на момент написания). Используя эти зависимости вместе, наше приложение может быть полностью реактивным от начала до конца.
WebFlux представляет другой способ обработки запросов вместо использования модели программирования @Controller
или @RestController
которая используется в Spring MVC. Но это не заменяет его. Вместо этого он был обновлен, чтобы разрешить использование реактивных типов. Это позволяет вам сохранить тот же формат, который вы использовали при написании в Spring, но с некоторыми изменениями в типах возвращаемых значений, поэтому вместо них возвращаются Flux
или Mono
. Ниже приведен очень надуманный пример.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
@RestController public class PersonController { private final PersonRepository personRepository; public PersonController(PersonRepository personRepository) { this .personRepository = personRepository; } @GetMapping ( "/people" ) public Flux<Person> all() { return personRepository.findAll(); } @GetMapping ( "/people/{id}" ) Mono<Person> findById( @PathVariable String id) { return personRepository.findOne(id); } } |
Для меня это выглядит очень знакомо и, на первый взгляд, на самом деле не выглядит так же, как ваш стандартный контроллер Spring MVC, но после прочтения методов мы можем увидеть различные типы возвращаемых данных, которые мы обычно ожидаем. В этом примере PersonRepository
должен быть реактивным репозиторием, поскольку мы были в состоянии напрямую возвращать результаты их поисковых запросов, для справки, реактивные репозитории возвращают Flux
для коллекций и Mono
для отдельных объектов.
Метод аннотации не является тем, на чем я хочу сосредоточиться в этом посте. Это не круто и достаточно модно для нас. Недостаточно использования лямбд, чтобы удовлетворить нашу тягу к написанию Java более функциональным способом. Но у Spring WebFlux есть свои спины. Он предоставляет альтернативный метод для маршрутизации и обработки запросов к нашим серверам, который слегка использует лямбды для написания функций маршрутизатора. Давайте посмотрим на пример.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
@Configuration public class PersonRouter { @Bean public RouterFunction<ServerResponse> route(PersonHandler personHandler) { return RouterFunctions.route(GET( "/people/{id}" ).and(accept(APPLICATION_JSON)), personHandler::get) .andRoute(GET( "/people" ).and(accept(APPLICATION_JSON)), personHandler::all) .andRoute(POST( "/people" ).and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post) .andRoute(PUT( "/people/{id}" ).and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put) .andRoute(DELETE( "/people/{id}" ), personHandler::delete) .andRoute(GET( "/people/country/{country}" ).and(accept(APPLICATION_JSON)), personHandler::getByCountry); } } |
Это все пути к методам в PersonHandler
которые мы рассмотрим позже. Мы создали компонент, который будет обрабатывать нашу маршрутизацию. Для настройки функций маршрутизации мы используем хорошо названный класс RouterFunctions
предоставляющий нам множество статических методов, но сейчас нас интересует только его метод route
. Ниже подпись метода route
.
1
2
3
4
|
public static <T extends ServerResponse> RouterFunction<T> route( RequestPredicate predicate, HandlerFunction<T> handlerFunction) { // stuff } |
Метод показывает, что он принимает RequestPredicate
вместе с HandlerFunction
и выводит RouterFunction
.
RequestPredicate
— это то, что мы используем для определения поведения маршрута, такого как путь к нашей функции-обработчику, тип запроса и тип ввода, который он может принять. Из-за того, что я использовал статический импорт, чтобы все читалось чище, некоторая важная информация была скрыта от вас. Чтобы создать RequestPredicate
мы должны использовать RequestPredicates
(множественное число), статический вспомогательный класс, предоставляющий нам все необходимые нам методы. Лично я рекомендую статически импортировать RequestPredicates
иначе ваш код будет беспорядочным из-за того, что вам может понадобиться использовать статические методы RequestPredicates
. В приведенном выше примере GET
, POST
, PUT
, DELETE
, accept
и contentType
являются статическими методами RequestPredicates
.
Следующим параметром является HandlerFunction
, который является функциональным интерфейсом. Здесь есть три важных элемента информации, у них есть универсальный тип <T extends ServerResponse>
, его метод handle
возвращает Mono<T>
и принимает ServerRequest
. Используя их, мы можем определить, что нам нужно передать функцию, которая возвращает Mono<ServerResponse>
(или один из его подтипов). Это, очевидно, накладывает жесткие ограничения на то, что возвращается из наших функций-обработчиков, поскольку они должны соответствовать этому требованию, иначе они не будут подходить для использования в этом формате.
Наконец, результатом является RouterFunction
. Затем его можно вернуть и использовать для маршрутизации к любой функции, которую мы указали. Но обычно мы хотим направлять множество разных запросов одновременно к различным обработчикам, которые обслуживает WebFlux. Из-за route
возвращающей RouterFunction
и того факта, что RouterFunction
также имеет свой собственный доступный метод маршрутизации andRoute
, мы можем andRoute
вызовы вместе и продолжать добавлять все дополнительные маршруты, которые нам нужны.
Если мы еще раз PersonRouter
приведенный выше пример PersonRouter
, то увидим, что методы названы в честь глаголов REST, таких как GET
и POST
которые определяют путь и тип запросов, которые будет принимать обработчик. Если мы возьмем, например, первый GET
, это маршрутизация к /people
с id
имени пути path (переменная пути обозначается {id}
), а тип возвращаемого содержимого, в частности APPLICATION_JSON
(статическое поле из MediaType
), определяется с помощью метод accept
. Если используется другой путь, он не будет обработан. Если путь указан правильно, но заголовок Accept не относится к числу допустимых типов, запрос не будет выполнен.
Прежде чем продолжить, я хочу перейти к методам accept
и contentType
. Оба из этих установленных заголовков запроса accept
совпадения с заголовком Accept и contentType
для Content-Type. Заголовок Accept определяет, какие медиа-типы являются приемлемыми для ответа, так как мы возвращали JSON-представления объекта Person
устанавливая его в APPLICATION_JSON
( application/json
в фактическом заголовке). Content-Type имеет ту же идею, но вместо этого описывает, какой Media Type находится внутри тела отправленного запроса. Вот почему только глаголы POST
и PUT
включают contentType
как остальные не содержат ничего в своих телах. DELETE
не включает accept
и contentType
поэтому мы можем заключить, что он не ожидает, что что-либо будет возвращено, и не включит ничего в тело запроса.
Теперь, когда мы знаем, как настроить маршруты, давайте рассмотрим написание методов-обработчиков, которые обрабатывают входящие запросы. Ниже приведен код, который обрабатывает все запросы от маршрутов, которые были определены в предыдущем примере.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
@Component public class PersonHandler { private final PersonManager personManager; public PersonHandler(PersonManager personManager) { this .personManager = personManager; } public Mono<ServerResponse> get(ServerRequest request) { final UUID id = UUID.fromString(request.pathVariable( "id" )); final Mono<Person> person = personManager.findById(id); return person .flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person. class ))) .switchIfEmpty(notFound().build()); } public Mono<ServerResponse> all(ServerRequest request) { return ok().contentType(APPLICATION_JSON) .body(fromPublisher(personManager.findAll(), Person. class )); } public Mono<ServerResponse> put(ServerRequest request) { final UUID id = UUID.fromString(request.pathVariable( "id" )); final Mono<Person> person = request.bodyToMono(Person. class ); return personManager .findById(id) .flatMap( old -> ok().contentType(APPLICATION_JSON) .body( fromPublisher( person .map(p -> new Person(p, id)) .flatMap(p -> personManager.update(old, p)), Person. class ))) .switchIfEmpty(notFound().build()); } public Mono<ServerResponse> post(ServerRequest request) { final Mono<Person> person = request.bodyToMono(Person. class ); final UUID id = UUID.randomUUID(); return created(UriComponentsBuilder.fromPath( "people/" + id).build().toUri()) .contentType(APPLICATION_JSON) .body( fromPublisher( person.map(p -> new Person(p, id)).flatMap(personManager::save), Person. class )); } public Mono<ServerResponse> delete(ServerRequest request) { final UUID id = UUID.fromString(request.pathVariable( "id" )); return personManager .findById(id) .flatMap(p -> noContent().build(personManager.delete(p))) .switchIfEmpty(notFound().build()); } public Mono<ServerResponse> getByCountry(ServerRequest serverRequest) { final String country = serverRequest.pathVariable( "country" ); return ok().contentType(APPLICATION_JSON) .body(fromPublisher(personManager.findAllByCountry(country), Person. class )); } } |
Одна вещь, которая весьма заметна, это отсутствие аннотаций. За @Component
аннотации @Component
для автоматического создания бина PersonHandler
, других аннотаций Spring нет.
Я пытался сохранить большую часть логики хранилища вне этого класса и скрыл любые ссылки на объекты сущности, пройдя через PersonManager
который делегирует PersonRepository
он содержит. Если вас интересует код внутри PersonManager
его можно увидеть здесь, на моем GitHub , дальнейшие пояснения к нему будут исключены для этого поста, поэтому мы можем сосредоточиться на самом WebFlux.
Хорошо, вернемся к коду под рукой. Давайте подробнее рассмотрим методы get
и post
чтобы выяснить, что происходит.
1
2
3
4
5
6
7
|
public Mono<ServerResponse> get(ServerRequest request) { final UUID id = UUID.fromString(request.pathVariable( "id" )); final Mono<Person> person = personManager.findById(id); return person .flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person. class ))) .switchIfEmpty(notFound().build()); } |
Этот метод предназначен для извлечения одной записи из базы данных, которая поддерживает этот пример приложения. Поскольку Cassandra является предпочтительной базой данных, я решил использовать UUID
для первичного ключа каждой записи, что приводит к тому, что тестирование примера становится более раздражающим, но ничего такого, что некоторые копирование и вставка не могут решить.
Помните, что переменная пути была включена в путь для этого запроса GET
. Используя метод pathVariable
в ServerRequest
переданный в метод, мы можем извлечь его значение, указав имя переменной, в данном случае id
. Затем идентификатор преобразуется в UUID
, который выдает исключение, если строка не в правильном формате, я решил игнорировать эту проблему, чтобы пример кода не стал более беспорядочным.
Когда у нас есть идентификатор, мы можем запросить базу данных на наличие соответствующей записи. Возвращается Mono<Person>
который либо содержит существующую запись, сопоставленную с Person
либо оставляется как пустой Mono
.
Используя возвращенный Mono
мы можем выводить разные ответы в зависимости от его существования. Это означает, что мы можем возвращать полезные коды состояния клиенту, чтобы они соответствовали содержимому тела. Если запись существует, то flatMap
возвращает ServerResponse
со статусом OK
. Наряду с этим статусом мы хотим вывести запись, для этого мы указываем тип содержимого тела, в данном случае APPLICATION_JSON
, и добавляем в него запись. fromPublisher
принимает наш Mono<Person>
(который является Publisher
) вместе с классом Person
чтобы он знал, что он отображает в теле. fromPublisher
— это статический метод из класса BodyInserters
.
Если запись не существует, то поток переместится в блок switchIfEmpty
и вернет состояние NOT FOUND
. Так как ничего не найдено, тело можно оставить пустым, поэтому мы просто создадим ServerResponse
.
Теперь перейдем к post
обработчику.
1
2
3
4
5
6
7
8
9
|
public Mono<ServerResponse> post(ServerRequest request) { final Mono<Person> person = request.bodyToMono(Person. class ); final UUID id = UUID.randomUUID(); return created(UriComponentsBuilder.fromPath( "people/" + id).build().toUri()) .contentType(APPLICATION_JSON) .body( fromPublisher( person.map(p -> new Person(p, id)).flatMap(personManager::save), Person. class )); } |
Даже из первой строки мы видим, что это уже отличается от того, как работал метод get
. Так как это запрос POST
он должен принять объект, который мы хотим сохранить из тела запроса. Поскольку мы пытаемся вставить одну запись, мы будем использовать метод bodyToMono
для извлечения Person
из тела. Если вы имеете дело с несколькими записями, вы, вероятно, захотите использовать вместо него bodyToFlux
.
Мы вернем статус CREATED
используя метод CREATED
который принимает URI
для определения пути к вставленной записи. Затем он следует настройке, аналогичной методу get
, используя метод fromPublisher
для добавления новой записи в тело ответа. Код, который формирует Publisher
, немного отличается, но вывод по-прежнему Mono<Person>
что имеет значение. Просто для дальнейшего объяснения того, как выполняется вставка, Person
переданный из запроса, сопоставляется с новым Person
с помощью сгенерированного нами UUID
а затем передается для save
путем вызова flatMap
. Создавая нового Person
мы вставляем в Cassandra только те значения, которые нам разрешены, в этом случае мы не хотим, чтобы UUID
передавался из тела запроса.
Вот и все, что касается обработчиков. Очевидно, есть другие методы, которые мы не прошли. Все они работают по-разному, но все следуют одной и той же концепции возврата ServerResponse
который содержит подходящий код состояния и записи в теле, если это необходимо.
Теперь мы написали весь код, который нам нужен для запуска базового бэк-энда Spring WebFlux. Осталось только связать всю конфигурацию, что легко сделать с помощью Spring Boot.
1
2
3
4
5
6
|
@SpringBootApplication public class Application { public static void main(String args[]) { SpringApplication.run(Application. class ); } } |
Вместо того, чтобы заканчивать пост здесь, мы, вероятно, должны посмотреть, как на самом деле использовать код.
Spring предоставляет класс WebClient
для обработки запросов без блокировки. Мы можем использовать это сейчас как способ тестирования приложения, хотя есть также WebTestClient
который мы могли бы использовать здесь вместо этого. WebClient
— это то, что вы бы использовали вместо блокирующего RestTemplate
при создании реактивного приложения.
Ниже приведен код, вызывающий обработчики, определенные в PersonHandler
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public class Client { public void doStuff() { // POST final Person record = new Person(UUID.randomUUID(), "John" , "Doe" , "UK" , 50 ); final Mono<ClientResponse> postResponse = client .post() .uri( "/people" ) .body(Mono.just(record), Person. class ) .accept(APPLICATION_JSON) .exchange(); postResponse .map(ClientResponse::statusCode) .subscribe(status -> System.out.println( "POST: " + status.getReasonPhrase())); // GET client .get() .uri( "/people/{id}" , "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4" ) .accept(APPLICATION_JSON) .exchange() .flatMap(response -> response.bodyToMono(Person. class )) .subscribe(person -> System.out.println( "GET: " + person)); // ALL client .get() .uri( "/people" ) .accept(APPLICATION_JSON) .exchange() .flatMapMany(response -> response.bodyToFlux(Person. class )) .subscribe(person -> System.out.println( "ALL: " + person)); // PUT final Person updated = new Person(UUID.randomUUID(), "Peter" , "Parker" , "US" , 18 ); client .put() .uri( "/people/{id}" , "ec2212fc-669e-42ff-9c51-69782679c9fc" ) .body(Mono.just(updated), Person. class ) .accept(APPLICATION_JSON) .exchange() .map(ClientResponse::statusCode) .subscribe(response -> System.out.println( "PUT: " + response.getReasonPhrase())); // DELETE client .delete() .uri( "/people/{id}" , "ec2212fc-669e-42ff-9c51-69782679c9fc" ) .exchange() .map(ClientResponse::statusCode) .subscribe(status -> System.out.println( "DELETE: " + status)); } } |
Не забудьте создать экземпляр Client
где-нибудь, ниже приведен хороший ленивый способ сделать это!
1
2
3
4
5
6
7
8
|
@SpringBootApplication public class Application { public static void main(String args[]) { SpringApplication.run(Application. class ); Client client = new Client(); client.doStuff(); } } |
Сначала мы создаем WebClient
.
1
|
|
После создания мы можем начать делать что-то с ним, отсюда метод doStuff
.
Давайте разберем POST
запрос, который отправляется на сервер.
01
02
03
04
05
06
07
08
09
10
|
final Mono<ClientResponse> postResponse = client .post() .uri( "/people" ) .body(Mono.just(record), Person. class ) .accept(APPLICATION_JSON) .exchange(); postResponse .map(ClientResponse::statusCode) .subscribe(status -> System.out.println( "POST: " + status.getReasonPhrase())); |
Я написал это немного по-другому, так что вы можете видеть, что Mono<ClientResponse>
возвращается после отправки запроса. Метод exchange
запускает HTTP-запрос к серверу. Ответ будет обработан всякий раз, когда ответ прибудет, если он когда-либо поступит.
Используя WebClient
мы указываем, что мы хотим отправить POST
запрос, конечно же, используя метод post
. Затем URI
добавляется с помощью метода uri
(перегруженный метод, этот принимает String
а другой принимает URI
). Я устал говорить, что этот метод делает то, что называется методом, поэтому содержимое тела добавляется вместе с заголовком Accept. Наконец мы отправляем запрос по телефону exchange
.
Обратите внимание, что тип носителя APPLICATION_JSON
совпадает с типом, определенным в функции маршрутизатора POST
. Если бы мы отправили другой тип, скажем, TEXT_PLAIN
мы получили бы ошибку 404
поскольку не существует обработчика, который бы соответствовал ожидаемому запросу.
Используя Mono<ClientResponse>
возвращаемый вызовом exchange
мы можем отобразить его содержимое на желаемый результат. В приведенном выше примере код состояния выводится на консоль. Если мы вспомним метод post
в PersonHandler
, помните, что он может только возвращать статус «Создан», но если отправленный запрос не совпадает правильно, то «Not Found» будет распечатан.
Давайте посмотрим на один из других запросов.
1
2
3
4
5
6
7
|
client .get() .uri( "/people/{id}" , "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4" ) .accept(APPLICATION_JSON) .exchange() .flatMap(response -> response.bodyToMono(Person. class )) .subscribe(person -> System.out.println( "GET: " + person)); |
Это наш типичный GET
. Это выглядит очень похоже на запрос POST
мы только что прошли. Основное отличие состоит в том, что uri
принимает как путь запроса, так и UUID
(в данном случае как String
) в качестве параметра, который заменит переменную пути {id}
и что тело остается пустым. Как обрабатывается ответ, также отличается. В этом примере он извлекает тело ответа, отображает его в Mono<Person>
и распечатывает его. Это можно было сделать с помощью предыдущего примера POST
но код состояния ответа был более полезен для его сценария.
Для немного другой перспективы, мы могли бы использовать cURL для отправки запросов и посмотреть, как выглядит ответ.
1
|
CURL -H "Accept:application/json" -i localhost: 8080 /people |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: application/json [ { "id" : "13c403a2-6770-4174-8b76-7ba7b75ef73d" , "firstName" : "John" , "lastName" : "Doe" , "country" : "UK" , "age" : 50 }, { "id" : "fbd53e55-7313-4759-ad74-6fc1c5df0986" , "firstName" : "Peter" , "lastName" : "Parker" , "country" : "US" , "age" : 50 } ] |
Ответ будет выглядеть примерно так, очевидно, он будет отличаться в зависимости от данных, которые вы сохранили.
Обратите внимание на заголовки ответа.
1
2
|
transfer-encoding: chunked Content-Type: application /json |
transfer-encoding
здесь представляет данные, которые передаются порциями, которые могут использоваться для потоковой передачи данных. Это то, что нам нужно, чтобы клиент мог реагировать на данные, которые ему возвращаются.
Я думаю, что это должно быть хорошим местом для остановки. Мы рассмотрели довольно много материала, который, надеюсь, помог вам лучше понять Spring WebFlux. Есть несколько других тем, о которых я хочу рассказать о WebFlux, но я остановлюсь на них в отдельных постах, так как считаю, что эта достаточно длинная.
В заключение, в этом посте мы очень кратко обсудили, почему вы хотели бы использовать Spring WebFlux поверх типичного Spring MVC-сервера. Затем мы посмотрели, как настроить маршруты и обработчики для обработки входящих запросов. Обработчики реализовали методы, которые могли работать с большинством глаголов REST, и возвращали правильные данные и коды состояния в своих ответах. Наконец, мы рассмотрели два способа отправки запросов к WebClient
один с использованием WebClient
для обработки вывода непосредственно на стороне клиента, а другой с помощью cURL, чтобы увидеть, как выглядит возвращаемый JSON.
Если вы заинтересованы в просмотре остального кода, который я использовал для создания примера приложения для этого поста, его можно найти на моем GitHub .
Как всегда, если вы нашли этот пост полезным, поделитесь им, и если вы хотите быть в курсе моих последних постов, вы можете подписаться на меня в Twitter по адресу @LankyDanDev .
Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Работа с Spring WebFlux
Мнения, высказанные участниками Java Code Geeks, являются их собственными. |