}

//end::validated[]

Хотя вы могли бы так же легко применить аннотации @Validated, @Min и @Max к OrderController (и любым другим bean-компонентам, которые могут быть внедрены с помощью OrderProps), это просто намного больше загромождает OrderController. С помощью компонента-хранилища свойства конфигурации вы собрали спецификацию свойств конфигурации в одном месте, оставив классы, которым эти свойства нужны, относительно чистыми.

5.2.2 Объявление метаданных свойства конфигурации

В зависимости от вашей среды IDE вы, возможно, заметили, что запись taco.orders.pageSize в application.yml (или application.properties) имеет предупреждение о чем-то вроде неизвестного свойства «taco». Это предупреждение появляется из-за отсутствия метаданных, касающихся только что созданного свойства конфигурации. На рисунке 5.2 показано, как это выглядит, когда я наведу курсор мыши на тако-часть свойства в Spring Tool Suite.

Рисунок 5.2 Предупреждение о отсутствующих метаданных свойства конфигурации

Метаданные свойств конфигурации не являются обязательными и не мешают работе свойств конфигурации. Но метаданные могут быть полезны для предоставления некоторой минимальной документации по свойствам конфигурации, особенно в IDE.

Например, при наведении курсора на свойство security.user.password я вижу то, что показано на рисунке 5.3. Хотя помощь при наведении мыши минимальна, ее может быть достаточно, чтобы понять, для чего используется свойство и как его использовать.

Рисунок 5.3. Документация при наведению указателя на свойства конфигурации в Spring Tool Suite

Чтобы помочь тем, кто может использовать свойства конфигурации, которые вы определяете - возможно, даже вы - обычно рекомендуется создать некоторые метаданные к этим свойствам. По крайней мере, это избавляет от этих надоедливых желтых предупреждений в IDE.

Чтобы создать метаданные для пользовательских свойств конфигурации, вам нужно создать файл в META-INF (например, в проекте в каталоге src/main/resources/META-INF) с именем Additional-spring-configuration-metadata.json.

БЫСТРОЕ ИСПРАВЛЕНИЕ ОТСУТСТВУЮЩИХ МЕТАДАННЫХ.

Если вы используете Spring Tool Suite, у вас есть возможность быстрого исправления для создания отсутствующих метаданных свойств. Поместите курсор на строку с предупреждением об отсутствующих метаданных и откройте всплывающее окно быстрого исправления с CMD-1 на Mac или Ctrl-1 в Windows и Linux (см. Рисунок 5.4).

Рисунок 5.4 Создание метаданных свойства конфигурации с помощью всплывающего быстрого исправления в Spring Tool Suite

Затем выберите параметр «Create Metadata for ...», чтобы добавить метаданные для свойства (в Additional-spring-configuration-metadata.json, как показано на этом рисунке), и создайте этот файл, если он еще не существует.

Для свойства taco.orders.pageSize вы можете настроить метаданные с помощью следующего JSON:

{

"properties": [

{

"name": "taco.orders.page-size",

"type": "java.lang.String",

"description": "Sets the maximum number of orders to display in a list."

}

]

}

Обратите внимание, что имя свойства, указанное в метаданных, имеет вид taco.orders.page-size. Гибкое именование свойств в Spring Boot допускает некоторые изменения в именах свойств, так что taco.orders.page-size эквивалентен taco.orders.pageSize.

После задания метаданных предупреждения должны исчезнуть. Более того, если вы наведете указатель мыши на свойство taco.orders.pageSize, вы увидите описание, показанное на рисунке 5.5.

Рисунок 5.5 Справка при наведении курсора на свойства пользовательской конфигурации

Кроме того, вы получаете справку по автозаполнению из IDE, так же как по стандартным предоставляемым Spring-ом свойствам конфигурации (как показано на рисунке 5.6).

Рисунок 5.6. Автодополнение при заполнении свойств на основе метаданных.

Как вы уже видели, свойства конфигурации полезны для настройки как автоматически настраиваемых компонентов, так и деталей, внедряемых в компоненты вашего приложения. Но что, если вам нужно настроить разные свойства для разных сред развертывания? Давайте рассмотрим, как использовать профили Spring для настройки конкретной среды.

5.3 Конфигурация профилей

Когда приложения развертываются в разных средах выполнения, обычно некоторые детали конфигурации различаются. Например, подробности соединения с базой данных, вероятно, не одинаковы в среде разработки, в среде обеспечения качества,и в производственной среде (production). Одним из способов настройки уникальной свойств в каждой среде является использование переменных среды для указания свойств конфигурации вместо их определения в application.properties и application.yml.

Например, во время разработки вы можете опираться на автоматически сконфигурированную встроенную базу данных H2. Но в производственной среде вы можете установить свойства конфигурации базы данных как переменные среды:

% export SPRING_DATASOURCE_URL=jdbc:mysql://localhost/tacocloud

% export SPRING_DATASOURCE_USERNAME=tacouser

% export SPRING_DATASOURCE_PASSWORD=tacopassword

Хотя это сработает, но как-то, несколько громоздко указывать более одного или двух свойств конфигурации в качестве переменных среды. Более того, нет хорошего способа отследить изменения переменных среды или откатить изменения в случае ошибки.

Вместо этого я предпочитаю использовать профили Spring. Профили - это тип условной конфигурации, в которой различные компоненты, классы конфигурации и свойства конфигурации применяются или игнорируются в зависимости от того, какие профили активны во время выполнения.

Например, допустим, что для целей разработки и отладки вы хотите использовать встроенную базу данных H2 и хотите, чтобы уровни ведения журнала для кода Taco Cloud были установлены на DEBUG. Но в продакшине вы хотите использовать внешнюю базу данных MySQL и установить уровень логирования WARN. В ситуации разработки достаточно просто не задавать какие-либо свойства источника данных и получить автоматически сконфигурированную базу данных H2. А что касается уровня логирования в отладке, вы можете установить для свойства logging.level.tacos для базового пакета tacos значение DEBUG в application.yml:

logging:

level:

tacos: DEBUG

Это именно то, что вам нужно для режима разработки. Но если бы вы развернули это приложение в режиме продакшен без каких-либо дальнейших изменений в application.yml, у вас все равно было бы логирование для пакета tacos и встроенная базы данных H2. Вам нужно определить профиль со свойствами, подходящими для продакшена.

5.3.1 Определение свойств профиля

Один из способов определения специфичных для профиля свойств - создать еще один YAML или файл свойств, содержащий только свойства для продакшена. Имя файла должно соответствовать следующему соглашению: application-{имя профиля}.yml или application-{имя профиля}.properties. Затем вы можете указать свойства конфигурации, соответствующие этому профилю. Например, вы можете создать новый файл с именем application-prod.yml, который содержит следующие свойства:

spring:

datasource: url: jdbc:mysql://localhost/tacocloud

username: tacouser

password: tacopassword

logging:

level:

tacos: WARN

Другой способ указать специфичные для профиля свойства работает только с конфигурацией YAML. Он включает размещение специфичных для профиля свойств вместе с непрофилированными свойствами в application.yml, разделенных тремя дефисами и свойством spring.profiles для присвоения имени профилю. При записи свойств для продакшена в application.yml файл будет выглядеть так:

logging:

level:

tacos: DEBUG

---

spring:

profiles: prod

datasource: url: jdbc:mysql://localhost/tacocloud

username: tacouser

password: tacopassword

logging:

level:

tacos: WARN

Как вы можете видеть, этот файл application.yml разделен на две секции набором тройных дефисов (---). Во втором разделе указывается значение для spring.profiles, указывающее, что следующие свойства применяются к профилю prod. В первом разделе не указывается никакое значение для spring.profiles. Следовательно, его свойства являются общими для всех профилей или являются значениями по умолчанию, если активный профиль не переопределяет значение этих свойств.

Независимо от того, какие профили активны при запуске приложения, уровень логирвоания для пакета tacos будет установлен на DEBUG с помощью свойства, установленного в профиле по умолчанию. Но если активен профиль с именем prod, то свойство logging.level.tacos будет переопределено как WARN. Аналогично, если профиль prod активен, то свойства источника данных будут установлены для использования внешней базы данных MySQL.

Вы можете определить свойства для любого количества профилей, создав дополнительные файлы YAML или файлы свойств, названные по шаблону application-{имя профиля} .yml или application-{имя профиля} .properties. Или, если хотите, введите еще три черты в application.yml вместе с другим свойством spring.profiles, чтобы указать имя профиля. Затем добавьте все необходимые для профиля свойства.

5.3.2 Активация профилей

Установка специфичных для профиля свойств не принесет пользы, если эти профили не активны. Но как сделать профиль активным? Чтобы сделать профиль активным, достаточно лишь включить его в список имен профилей, заданный свойству spring.profiles.active. Например, вы можете установить его в application.yml следующим образом:

spring:

profiles:

active:

- prod

Но это, пожалуй, худший из возможных способов установить активный профиль. Если вы установите активный профиль в application.yml, то этот профиль станет профилем по умолчанию, и вы не добьетесь ни одного из преимуществ использования профилей для отделения специфичных для продакшена свойств от свойств разработки. Вместо этого я рекомендую вам установить активные профили с переменными окружения. В продакшен среде вы должны установить SPRING_PROFILES_ACTIVE следующим образом:

% export SPRING_PROFILES_ACTIVE=prod

С этого момента любые приложения, развернутые на этом компьютере, будут иметь активный профиль prod, и соответствующие свойства конфигурации будут иметь приоритет над свойствами в профиле по умолчанию.

Если вы запускаете приложение как исполняемый файл JAR, вы также можете установить активный профиль через аргумент командной строки, например:

% java -jar taco-cloud.jar --spring.profiles.active=prod

Обратите внимание, что имя свойства spring.profiles.active содержит слово множественного числа - profiles. Это означает, что вы можете указать более одного активного профиля. Зададим несколько активных профелей с разделением через запятую, также как можно установить и через переменную окружения:

% export SPRING_PROFILES_ACTIVE=prod,audit,ha

В YAML вы бы активные профили в виде списка:

spring:

profiles:

active:

- prod

- audit

- ha

Стоит также отметить, что если вы развертываете приложение Spring в Cloud Foundry, для вас автоматически активируется профиль с именем cloud. Если Cloud Foundry является вашей производственной средой, вы обязательно должны указать специфичные для продакшена свойства в профиле cloud.

Открою тайну - профили были бы достаточно бесполезны, если бы могли использоваться только для условной установки свойств конфигурации в приложении Spring. Давайте посмотрим, как объявить bean-компоненты, специфичные для активного профиля.

5.3.3 Создание bean-ов в зависимости от профиля

Иногда полезно предоставить уникальный набор bean-компонентов для разных профилей. Обычно любой компонент, объявленный в классе конфигурации Java, создается независимо от того, какой профиль активен. Но предположим, что есть некоторые bean-компоненты, которые вам нужно создать, только если определенный профиль активен. В этом случае аннотация @Profile может определять bean-компоненты как применимые только к данному профилю.

Например, у вас есть компонент CommandLineRunner, объявленный в TacoCloudApplication, который используется для загрузки встроенной базы данных с данными об ингредиентах при запуске приложения. Это здорово для режима разработки, но было бы ненужным (и нежелательным) в приложении на продакшене. Чтобы предотвратить загрузку данных ингредиента при каждом запуске приложения при продакшен развертывании, вы можете аннотировать метод компонента CommandLineRunner с помощью @Profile следующим образом:

@Bean

@Profile("dev")

public CommandLineRunner dataLoader(IngredientRepository repo,

UserRepository userRepo, PasswordEncoder encoder) {

...

}

Или предположим, что вам нужно создать CommandLineRunner, если активен либо dev-профиль, либо qa-профиль. В этом случае вы можете перечислить профили, для которых должен быть создан компонент:

@Bean

@Profile({"dev", "qa"})

public CommandLineRunner dataLoader(IngredientRepository repo,

UserRepository userRepo, PasswordEncoder encoder) {

...

}

Теперь данные ингредиента будут загружены только если активны профили dev или qa. Это означало бы, что вам нужно активировать профиль разработчика при запуске приложения в среде разработки. Было бы еще удобнее, если бы этот компонент CommandLineRunner создавался всегда, если профиль prod не активен. В этом случае вы можете применить @Profile следующим образом:

@Bean

@Profile("!prod")

public CommandLineRunner dataLoader(IngredientRepository repo,

UserRepository userRepo, PasswordEncoder encoder) {

...

}

Здесь восклицательный знак (!) отменяет имя профиля. По сути, он утверждает, что bean-компонент CommandLineRunner будет создан, если профиль prod не активен.

Также можно использовать @Profile для всего класса, аннотированного @Configuration. Например, предположим, что вы должны были извлечь компонент CommandLineRunner в отдельном классе конфигурации с именем DevelopmentConfig.Для этого достаточно аннотировать DevelopmentConfig с помощью @Profile:

@Profile({"!prod", "!qa"})

@Configuration

public class DevelopmentConfig {

@Bean

public CommandLineRunner dataLoader(IngredientRepository repo,

UserRepository userRepo, PasswordEncoder encoder) {

...

}

}

Здесь bean-компонент CommandLineRunner (как и любые другие bean-компоненты, определенные в DevelopmentConfig) будет создан, только если ни prod, ни qa-профили не активны.

Итог:

Spring bean-компоненты могут быть аннотированы с помощью @ConfigurationProperties, чтобы включить внедрение значений из одного из нескольких источников свойств.

Свойства конфигурации можно задавать в аргументах командной строки, переменных среды, системных свойствах JVM, файлах свойств или файлах YAML, а также в других параметрах.

Свойства конфигурации можно использовать для переопределения параметров автоконфигурации, включая возможность указать URL-адрес источника данных и уровни логирования.

Профили Spring можно использовать с источниками свойств для условной установки свойств конфигурации на основе активных профилей.

Spring in Action Covers Spring 5.0 перевод на русский. Глава 6

6. Создание REST сервисов

Эта глава охватывает

Определение REST endpoints в Spring MVC

Включение гиперссылочных REST ресурсов

Автоматические REST endpoints на основе репозитория

«Веб-браузер мертв. Что теперь?"

Примерно дюжину лет назад я слышал, как кто-то предположил, что веб-браузер приближается к статусу legacy и что что-то другое возьмет верх. Но как такое могло случиться? Что может свергнуть почти вездесущий веб-браузер? Как бы мы использовали растущее количество сайтов и онлайн-сервисов, если бы не веб-браузер? Конечно, это был бред сумасшедшего!

Вернемся в день сегодняшний и станет очевидно, что веб-браузер не исчез. Но он больше не является основным средством доступа к Интернету. Мобильные устройства, планшеты, умные часы и голосовые устройства стали обычным явлением. И даже многие браузерные приложения на самом деле работают с приложениями JavaScript, а не позволяют браузеру быть тупым терминалом для рендеринга контента на сервере.

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

В этой главе вы собираетесь использовать Spring для предоставления REST API для приложения TacoCloud. Вы будете использовать то, что узнали о Spring MVC в главе 2, для создания endpoints RESTful с контроллерами Spring MVC. Вы также автоматически выставите REST endpoints для Spring Data repository-ев, которые вы определили в главе 4. Наконец, мы рассмотрим способы тестирования и защиты этих endpoint.

Но сначала вы напишите несколько новых контроллеров Spring MVC, которые предоставляют функциональные возможности бэкэнда с endpoint REST, которые будут использоваться богатым веб-интерфейсом.

6.1 Написание RESTful контроллеров

Надеюсь, вы не возражаете, но пока вы переворачивали страницу и читали введение к этой главе, я взял на себя смелость переосмыслить пользовательский интерфейс для TacoCloud. То, с чем вы работали, было прекрасно для начала, но этого не хватало по части эстетики.

Рисунок 6.1 - это просто пример того, как выглядит Taco Cloud. Довольно шикарно, а?

Рисунок 6.1 Новая домашняя страница Taco Cloud

Когда я любовался внешним видом Taco Cloud, я решил создать веб-интерфейс в виде одностраничного приложения с использованием популярной платформы Angular. В конечном счете, этот новый пользовательский интерфейс браузера заменит серверные страницы, созданные вами в главе 2. Но для этого вам потребуется создать REST API, с которым пользовательский интерфейс построенный на Angular (Я решил использовать Angular, но выбор среды интерфейса не должен иметь никакого отношения к написанию бэккенд кода Spring. Не стесняйтесь выбирать Angular, React, Vue.js или любую другую технологию веб-интерфейса, которая подходит вам больше всего) будет связываться, чтобы сохранять и извлекать данные тако.

SPA или не SPA?

Вы разработали традиционное многостраничное приложение (MPA) с Spring MVC в главе 2, и теперь вы заменяете его одностраничным приложением (SPA) на основе Angular. Но я не утверждаю, что SPA всегда лучший выбор, чем MPA.

Поскольку представление в значительной степени отделено от серверной обработки в SPA, это дает возможность разработать более одного пользовательского интерфейса (такого как собственное мобильное приложение) для одной и той же функциональности сервера. Это также открывает возможность для интеграции с другими приложениями, которые могут использовать API. Но не все приложения требуют такой гибкости, и MPA - это более простой дизайн, если все, что вам нужно, это отображать информацию на веб-странице.

Это не книга по Angular, поэтому код в этой главе будет сосредоточен в основном на бэккенд Spring-коде. Я покажу достаточно Angular-кода, чтобы вы могли понять, как работает клиентская часть. Будьте уверены, что полный набор кода, включая Angular frontend, доступен как часть загружаемого кода для книги и по адресу https://github.com/habuma/spring-in-action-5-samples. Вас также может заинтересовать чтение Angular in Action Джереми Уилкена (Manning, 2018) и Angular Development with TypeScript, второе издание Якова Файна и Антона Моисеева (Manning, 2018).

В двух словах, клиентский код Angular будет взаимодействовать с API, который вы создадите в этой главе посредством HTTP-запросов. В главе 2 вы использовали аннотации @GetMapping и @PostMapping для извлечения и публикации данных на сервере. Те же самые аннотации по-прежнему пригодятся, когда вы определите свой REST API. Кроме того, Spring MVC поддерживает несколько других аннотаций для различных типов HTTP-запросов, как указано в таблице 6.1.

Таблица 6.1. Spring MVC HTTP аннотации обработки запросов (Сопоставление методов HTTP для создания, чтения, обновления и удаления (CRUD) операций не является идеальным соответствием, но на практике именно так они часто используются и как вы будете их использовать в Taco Cloud.)

Аннотация - HTTP метод - Стандартное применение

@GetMapping - HTTP GET requests - Чтение данных

@PostMapping - HTTP POST requests - Создание данных

@PutMapping - HTTP PUT requests - Изменение данных

@PatchMapping - HTTP PATCH requests - Изменение данных

@DeleteMapping - HTTP DELETE requests - Удаление данных

@RequestMapping - Обработка запросов общего назначения; HTTP - метод, указанный как атрибут метода

Чтобы увидеть эти аннотации в действии, вы начнете с создания простой REST endpoint, которая выбирает несколько самых последних созданных тако.

6.1.1 Получение данных с сервера

Одна из самых крутых вещей в Taco Cloud - это то, что он позволяет фанатикам тако создавать свои собственные творения тако и делиться ими со своими коллегами-любителями тако. Для этого в Taco Cloud должна быть возможность отображать список самых последних созданных тако при нажатии на ссылку «Latest Designs».

В коде Angular я определил компонент RecentTacosComponent, который будет отображать самые последние созданные тако. Полный код TypeScript для RecentTacosComponent показан в следующем листинге.

Листинг 6.1. Angular компонент для отображения последних тако

import { Component, OnInit, Injectable } from '@angular/core';

import { Http } from '@angular/http';

import { HttpClient } from '@angular/common/http';

@Component({

selector: 'recent-tacos',

templateUrl: 'recents.component.html',

styleUrls: ['./recents.component.css']

})

@Injectable()

export class RecentTacosComponent implements OnInit {

recentTacos: any;

constructor(private httpClient: HttpClient) { }

ngOnInit() {

this.httpClient.get('http://localhost:8080/design/recent') //Получает последние тако с сервера

.subscribe(data => this.recentTacos = data);

}

}

Обратите ваше внимание на метод ngOnInit(). В этом методе RecentTacosComponent использует внедренный модуль Http для выполнения HTTP-запроса GET к http://localhost:8080/design/latest, ожидая, что ответ будет содержать список дизайнов тако, который будет помещен в переменную модели recentTacos. Визуализация ( recents.component.html) представит данные этой модели в виде HTML, которые будут отображаться в браузере. Конечный результат может выглядеть примерно так, как показано на рисунке 6.2, после создания трех тако.

Рисунок 6.2 Отображение последних созданных тако

Недостающий фрагмент этой головоломки - это конечная точка (endpoint), которая обрабатывает запросы GET для /design/recent и отдает списком недавно созданных тако. Вы создадите новый контроллер для обработки такого запроса. Следующий листинг показывает этот контроллер.

Листинг 6.2. Контроллер RESTful для API запросов дизайнов тако

package tacos.web.api;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.domain.PageRequest;

import org.springframework.data.domain.Sort;

import org.springframework.hateoas.EntityLinks;

import org.springframework.http.HttpStatus;

import org.springframework.web.bind.annotation.CrossOrigin;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.PathVariable;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.ResponseStatus;

import org.springframework.web.bind.annotation.RestController;

import tacos.Taco;

import tacos.data.TacoRepository;

@RestController

@RequestMapping(path="/design", produces="application/json") //Обрабатывает запросы на /design

@CrossOrigin(origins="*") //Позволяет перекрестные запросы

public class DesignTacoController {

private TacoRepository tacoRepo;

@Autowired

EntityLinks entityLinks;

public DesignTacoController(TacoRepository tacoRepo) {

this.tacoRepo = tacoRepo;

}

@GetMapping("/recent")

public Iterable recentTacos() { //Формирует и отдает последние дизайны тако

PageRequest page = PageRequest.of(

0, 12, Sort.by("createdAt").descending());

return tacoRepo.findAll(page).getContent();

}

}

Аннотация @RestController служит двум целям. Во-первых, это аннотация стереотипа, такая как @Controller и @Service, которая отмечает класс для обнаружения при сканировании компонентов. Но наиболее релевантным для обсуждения REST, аннотация @RestController сообщает Spring, что все методы-обработчики в контроллере должны иметь свое возвращаемое значение, записанное непосредственно в тело ответа, а не записываться в модель в представление для визуализации.

В качестве альтернативы, вы могли бы аннотировать DesignTacoController с помощью @Controller, как и с любым контроллером Spring MVC. Но тогда вам также нужно аннотировать все методы-обработчики с помощью @ResponseBody для достижения того же результата. Еще один вариант - вернуть объект ResponseEntity, о котором мы поговорим чуть позже.

Аннотация @RequestMapping на уровне класса работает с аннотацией @GetMapping в методе recentTacos(), чтобы указать, что метод recentTacos() отвечает за обработку запросов GET для /design/recent (именно это необходимо для вашего кода Angular).

Вы заметите, что аннотация @RequestMapping также устанавливает атрибут produces. Это указывает, что любой из методов-обработчиков в DesignTacoController будет обрабатывать запросы только в том случае, если заголовок Accept запроса включает в себя «application/json». Это не только ограничивает ваш API только выдачей результатов в виде JSON, но также позволяет другому контроллеру (возможно, DesignTacoController из главы 2) обрабатывать запросы с одинаковыми путями, если эти запросы не имеют требований к вывода в формате JSON. Несмотря на то, что это ограничивает ваш API-интерфейс только JSON (что подходит для ваших нужд), вы можете установить produces как массив String нескольких типов контента. Например, чтобы разрешить вывод XML, вы можете добавить” text/html " в атрибут produces:

@RequestMapping(path="/design", produces={"application/json", "text/xml"})

То что еще вы, возможно, заметили в листинге 6.2, это то, что класс аннотирован @CrossOrigin. Поскольку Angular-часть приложения будет работать на отдельном хосте и/или порту по API (по крайней мере, на данный момент), веб-браузер не позволит вашему Angular-клиенту использовать API. Это ограничение можно обойти, включив заголовки CORS (Cross-Origin Resource Sharing) в ответы сервера. Spring упрощает применение CORS с аннотацией @CrossOrigin. В данном случае @CrossOrigin позволяет клиентам из любого домена использовать API.

Логика в методе recentTacos() довольно проста. Он создает объект PageRequest, который указывает, что вы хотите получить первую (0-я) страницу с первыми 12 результатами сортировки в порядке убывания по дате создания тако. Короче говоря, вы хотите дюжину самых последних созданных дизайнов тако. PageRequest передается в вызов метода findAll() объекта TacoRepository, и содержимое этой страницы результатов возвращается клиенту (ответ, как вы видели в листинге 6.1, будет использоваться в качестве данных модели для отображения пользователю).

Теперь предположим, что вы хотите создать endpoint, который извлекает один тако по его идентификатору. Используя переменную-заполнитель в пути метода обработчика и принимая переменную path, вы можете получить ID и использовать его для поиска объекта Taco используя репозиторий:

@GetMapping("/{id}")

public Taco tacoById(@PathVariable("id") Long id) {

Optional optTaco = tacoRepo.findById(id);

if (optTaco.isPresent()) {

return optTaco.get();

}

return null;

}

Поскольку базовый (корневой) путь контроллера - /design, этот метод контроллера обрабатывает запросы GET для /design/{id}, где часть пути {id} является заполнителем. Фактическое значение в запросе задается параметру id, который сопоставляется с заполнителем {id} с помощью @PathVariable.

Внутри tacoById () параметр id передается методу findById() в репозиторий для получения Taco. findById() возвращает Optional, потому что возможна ситуация, что нет тако с переданным ID. Поэтому перед тем как получить результат необходимо проверить, существует ли тако по переданному идентификатору. Если такой тако существует, вы вызываете метод get() для Optional, чтобы получить Taco.

Если идентификатор не соответствует ни одному известному tacos, вы возвращаете null. Однако это далеко не идеально. Возвращая значение null, клиент получает ответ с пустым телом и кодом состояния HTTP 200 (OK). Клиент получает ответ, который он не может использовать, но код состояния указывает, что все в порядке. Лучшим подходом было бы вернуть ответ со статусом HTTP 404 (не найден).

Текущая реализация не имеет простого способа, чтобы возвращать код статуса 404 от tacoById(). Но если произвести небольшие изменения, это станет возможным:

@GetMapping("/{id}")

public ResponseEntity tacoById(@PathVariable("id") Long id) {

Optional optTaco = tacoRepo.findById(id);

if (optTaco.isPresent()) {

return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);

}

return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);

}

Теперь вместо возврата объекта Taco функция tacoById () возвращает ResponseEntity . Если тако найдено, вы оборачиваете объект Taco в ResponseEntity с HTTP-статусом OK (что и было ранее). Но если тако не найдено, вы добавляете в ResponseEntity значение null вместе с HTTP-статусом NOT FOUND, чтобы указать, что клиент пытается получить тако, которого не существует.

Начало API Taco Cloud для вашего Angular-овского (или любого другого, по вашим предпочтениям) клиента положено. Для тестирования разработки вы также можете использовать утилиты командной строки, такие как curl или HTTPie (https://httpie.org/) чтобы лучше понимать API. Например, следующая командная строка показывает, как вы могли бы получить недавно созданные тако с curl:

$ curl localhost:8080/design/recent

Или такой командой, если вы предпочитаете HTTPie:

$ http :8080/design/recent

Но реализация endpoint, которая возвращает информацию, - это только начало. Что делать, если ваш API должен получать данные от клиента? Давайте посмотрим, как вы можете написать методы контроллера, которые обрабатывают входные данные запросов.

6.1.2 Отправка данных на сервер

Сейчас приложение может вернуть дюжину самых последних созданных тако. Но как эти тако были созданы изначально?

Вы еще не удалили ни один код из главы 2, поэтому у вас все еще есть оригинальный DesignTacoController, который отображает форму дизайна Taco и обрабатывает отправку формы. Это отличный способ получить некоторые тестовые данные для тестирования созданного вами API. Но если вы собираетесь преобразовать Taco Cloud в одностраничное приложение, вам нужно будет создать компоненты Angular и соответствующие endpoint-ы, чтобы заменить форму дизайна Taco из главы 2.

Я уже переработал клиентский код для формы дизайна taco, создав новый Angular компонент с именем DesignComponent (в файле с именем design.component.ts). Поскольку в нем должен присутствовать функционал отправки данных формы, DesignComponent имеет метод onSubmit (), который выглядит следующим образом:

onSubmit() {

this.httpClient.post(

'http://localhost:8080/design',

this.model, {

headers: new HttpHeaders().set('Content-type', 'application/json'),

}).subscribe(taco => this.cart.addToCart(taco));

this.router.navigate(['/cart']);

}

В методе onSubmit() вместо get() вызывается метод post(). Это означает, что вместо извлечения данных из API, вы отправляете данные в API. В частности, вы отправляете дизайн тако, который хранится в переменной model, в API endpoint /design с request-ом HTTP POST.

Это означает, что вам нужно будет написать метод в DesignTacoController для обработки этого запроса и сохранения дизайна. Добавив следующий метод postTaco() в DesignTacoController, вы реализуете функционал в контроллере для этой цели:

@PostMapping(consumes="application/json")

@ResponseStatus(HttpStatus.CREATED)

public Taco postTaco(@RequestBody Taco taco) {

return tacoRepo.save(taco);

}

Поскольку postTaco() будет обрабатывать HTTP POST request , он аннотируется @PostMapping вместо @GetMapping. Вы не указываете здесь атрибут path, поэтому метод postTaco() будет обрабатывать запросы для /design, как указано в классе @RequestMapping у DesignTacoController.

Вы устанавливаете атрибут consumes. Здесь вы используете consumes, чтобы сказать, что метод будет обрабатывать только запросы, Content-type которых соответствует application/json.

Параметр Taco для метода помечается @RequestBody, чтобы указать, что тело запроса должно быть преобразовано в объект Taco и привязано к параметру. Эта аннотация важна, без нее Spring MVC предполагает, что вы хотите, чтобы параметры запроса (либо параметры запроса, либо параметры формы) были связаны с объектом Taco. Но аннотация @RequestBody гарантирует, что вместо этого JSON в теле запроса будет связан с объектом Taco.

Как только postTaco() получает объект Taco, он передает его методу save() в TacoRepository.

Возможно, вы также заметили, что я аннотировал метод postTaco() с помощью @ResponseStatus (HttpStatus.CREATED). При нормальных обстоятельствах (когда не генерируются исключения) все ответы будут иметь код состояния HTTP 200 (ОК), что указывает на успешность запроса. Хотя ответ HTTP 200 всегда приветствуется, он не всегда достаточно описательный. В случае запроса POST HTTP-статус 201 (CREATED) является более информативным. Он сообщает клиенту, что запрос был не только успешным, но в результате был создан ресурс. Всегда целесообразно использовать @ResponseStatus, где это уместно, для передачи клиенту наиболее описательного и точного кода состояния HTTP.

Хотя вы использовали @PostMapping для создания нового ресурса Taco, POST-запросы также можно использовать для обновления ресурсов. Тем не менее, запросы POST обычно используются для создания ресурсов, а запросы PUT и PATCH используются для обновления ресурсов. Давайте посмотрим, как вы можете обновить данные, используя @PutMapping и @PatchMapping.

6.1.3 Обновление данных на сервере

Прежде чем написать какой-либо код контроллера для обработки команд HTTP PUT или PATCH, вам следует уделить время рассмотрению слона в комнате: почему существуют два разных метода HTTP для обновления ресурсов?

Хотя это правда, что PUT часто используется для обновления данных ресурсов, на самом деле это семантическая противоположность GET. В то время как запросы GET предназначены для передачи данных с сервера на клиент, запросы PUT предназначены для отправки данных с клиента на сервер.

В этом смысле PUT действительно предназначен для выполнения операции оптовой замены, а не операции обновления. Напротив, целью HTTP PATCH является выполнение исправления или частичное обновление данных ресурса.

Например, предположим, что вы хотите изменить адрес заказа. Один из способов добиться этого с помощью REST API - обработать запрос PUT следующим образом:

@PutMapping("/{orderId}")

public Order putOrder(@RequestBody Order order) {

return repo.save(order);

}

Это может сработать, но для этого потребуется, чтобы клиент передал полные данные заказа в запросе PUT. Семантически PUT означает «передать эти данные по этому URL», по сути, заменяя любые данные, которые уже есть. Если какое-либо из свойств заказа будет опущено, значение этого свойства будет заменено на ноль. Даже тако в заказе необходимо будет передать вместе с данными заказа, иначе они будут удалены из заказа.

Если PUT выполняет оптовую замену данных объекта, то как вам следует обрабатывать запросы, чтобы выполнить только частичное обновление? Вот для чего хороши запросы HTTP PATCH и Spring @PatchMapping. Вот как вы можете написать метод контроллера для обработки запроса PATCH для заказа:

@PatchMapping(path="/{orderId}", consumes="application/json")

public Order patchOrder(@PathVariable("orderId") Long orderId,

@RequestBody Order patch) {

Order order = repo.findById(orderId).get();

if (patch.getDeliveryName() != null) {

order.setDeliveryName(patch.getDeliveryName());

}

if (patch.getDeliveryStreet() != null) {

order.setDeliveryStreet(patch.getDeliveryStreet());

}

if (patch.getDeliveryCity() != null) {

order.setDeliveryCity(patch.getDeliveryCity());

}

if (patch.getDeliveryState() != null) {

order.setDeliveryState(patch.getDeliveryState());

}

if (patch.getDeliveryZip() != null) {

order.setDeliveryZip(patch.getDeliveryState());

}

if (patch.getCcNumber() != null) {

order.setCcNumber(patch.getCcNumber());

}

if (patch.getCcExpiration() != null) {

order.setCcExpiration(patch.getCcExpiration());

}

if (patch.getCcCVV() != null) {

order.setCcCVV(patch.getCcCVV());

}

return repo.save(order);

}

Первое, на что следует обратить внимание, это то, что метод patchOrder() имеет аннотацию @PatchMapping вместо @PutMapping, что указывает на то, что он должен обрабатывать запросы HTTP PATCH вместо запросов PUT.

Но одна вещь, которую вы, несомненно, заметили, это то, что метод patchOrder() немного сложнее, чем метод putOrder(). Это потому, что аннотации сопоставления Spring MVC, включая @Pathmapping и @PutMapping, указывают только, какие типы запросов должен обрабатывать метод. Эти аннотации не определяют, как будет обрабатываться запрос. Даже если PATCH семантически подразумевает частичное обновление, вы должны написать код в методе-обработчике, который фактически выполняет такое обновление.

В случае метода putOrder() вы приняли полные данные для заказа и сохранили их, придерживаясь семантики HTTP PUT. Но для того, чтобы patchMapping() придерживался семантики HTTP PATCH, тело метода требует большего интеллекта. Вместо полной замены заказа новыми отправленными данными, он проверяет каждое поле входящего объекта заказа и применяет любые не-null значения к существующему заказу. Этот подход позволяет клиенту отправлять только те свойства, которые должны быть изменены, и позволяет серверу сохранять существующие данные для любых свойств, не указанных клиентом.

Существует более одного подхода к реализации PATCH

Подход исправления, применяемый в методе patchOrder(), имеет несколько ограничений:

Если null значения предназначены для указания отсутствия изменений, как клиент может указать, что поле должно быть установлено в null?

Невозможно удалить или добавить подмножество элементов из коллекции. Если клиент хочет добавить или удалить запись из коллекции, он должен отправить полную измененную коллекцию.

На самом деле не существует строгого правила о том, как обрабатывать запросы PATCH или как должны выглядеть входящие данные. Вместо того, чтобы отправлять фактические данные домена, клиент может отправить специфическое для патча описание изменений, которые будут применены. Конечно, обработчик запроса должен был бы быть написан для обработки инструкций патча вместо данных домена.

Обратите внимание, что и в @PutMapping, и в @PatchMapping путь запроса ссылается на ресурс, который необходимо изменить. Это те же самые пути что обрабатываются с помощью методов аннотированных @GetMapping.

Теперь вы видели, как получать и обновлять ресурсы с помощью @GetMapping и @PostMapping. И вы видели два разных способа обновления ресурса с помощью @PutMapping и @PatchMapping. Осталось только обработать запросы на удаление ресурса.

6.1.4 Удаление данных с сервера

Иногда данные просто больше не нужны. В этих случаях клиент должен иметь возможность запросить удаление ресурса с помощью HTTP DELETE request.

Spring MVC @DeleteMapping пригодится для объявления методов, которые обрабатывают запросы на удаление. Например, предположим, вы хотите, чтобы ваш API разрешал удаление ресурса заказа. Следующий метод контроллера должен реализовать этот функционал:

@DeleteMapping("/{orderId}")

@ResponseStatus(code=HttpStatus.NO_CONTENT)

public void deleteOrder(@PathVariable("orderId") Long orderId) {

try {

repo.deleteById(orderId);

} catch (EmptyResultDataAccessException e) {}

}

К этому моменту принцип построения аннотаций должна быть вам знакома. Вы уже видели @GetMapping, @PostMapping, @PutMapping и @ PatchMapping, каждый из которых указывает, что метод должен обрабатывать HTTP запросы соответствующие им методов. Возможно, вас не удивит, что @DeleteMapping используется для указания того, что метод deleteOrder() отвечает за обработку запросов DELETE для /orders/{orderId}.

Код в методе - это то, что фактически выполняет удаление заказа. В этом случае он принимает идентификатор заказа, предоставленный в качестве переменной пути в URL-адресе, и передает его в метод deleteById () репозитория. Если заказ существует при вызове этого метода, он будет удален. Если заказ не существует, будет выброшено исключение EmptyResultDataAccessException.

Я решил отлавливать EmptyResultDataAccessException и ничего с ними не делать. Я думаю, что если вы попытаетесь удалить ресурс, который не существует, результат будет таким же, как если бы он существовал до удаления. То есть ресурс перестанет существовать. Существовал ли он раньше или нет не имеет значения. Кроме того, я мог бы написать deleteOrder(), чтобы вернуть ResponseEntity, установив тело в null и код состояния HTTP NOT FOUND.

Единственное, на что следует обратить внимание в методе deleteOrder(), это то, что он аннотирован @ResponseStatus, чтобы гарантировать, что HTTP-статус ответа равен 204 (NO CONTENT). Нет необходимости передавать какие-либо данные ресурса обратно клиенту для ресурса, который больше не существует, поэтому ответы на запросы DELETE обычно не имеют тела и, следовательно, должны сообщать код состояния HTTP, чтобы клиент знал, что не стоит ожидать никакого содержимого.

Ваш Taco Cloud API начинает обретать форму. Клиентский код теперь может легко использовать этот API для отображения ингредиентов, принятия заказов и отображения недавно созданных тако. Но есть кое-что, что вы можете сделать, что сделает ваш API еще проще для клиента. Давайте посмотрим, как вы можете добавить hypermedia в Taco Cloud API.

6.2 Добавление hypermedia

API, который вы создали до сих пор, довольно прост, но он работает, пока клиент, который его использует, знает схему URL API. Например, клиент может быть жестко закодирован, чтобы знать, что он может получить список недавно созданных тако, выполнив запрос GET для /design/recent. Аналогично, он должен понимать, что он может добавить идентификатор любого taco из полученного перечня в /design, чтобы получить URL для этого конкретного taco.

Использование жестко закодированных шаблонов URL и манипулирование строками распространено среди клиентского кода API. Но представьте на мгновение, что произойдет, если изменится схема URL API. Жестко закодированный клиентский код будет иметь устаревшее понимание API и, таким образом, будет не корректен. Жесткое кодирование URL-адресов API и использование строковых манипуляций с ними делает клиентский код хрупким.

Гипермедиа как движок состояния приложения, или HATEOAS, является средством создания API с функционалом самоописания, в которых ресурсы, возвращаемые из API, содержат ссылки на связанные ресурсы. Это позволяет клиентам перемещаться по API с минимальным пониманием URL-адресов API. Вместо этого он понимает взаимосвязи между ресурсами, обслуживаемыми API, и использует свое понимание этих взаимосвязей для обнаружения URL-адресов API по мере их прохождения.

Например, предположим, что клиент должен был запросить список недавно разработанных тако. В необработанном виде, без гиперссылок, список последних тако будет получен в клиенте с JSON, который выглядит следующим образом (для краткости все, кроме первого тако в списке, вырезаны):

[

{

"id": 4,

"name": "Veg-Out",

"createdAt": "2018-01-31T20:15:53.219+0000",

"ingredients": [

{"id": "FLTO", "name": "Flour Tortilla", "type": "WRAP"},

{"id": "COTO", "name": "Corn Tortilla", "type": "WRAP"},

{"id": "TMTO", "name": "Diced Tomatoes", "type": "VEGGIES"},

{"id": "LETC", "name": "Lettuce", "type": "VEGGIES"},

{"id": "SLSA", "name": "Salsa", "type": "SAUCE"}

]

},

...

]

Если клиент желает получить или выполнить какую-либо другую HTTP-операцию над конкретным тако, ему необходимо знать (с помощью жесткого кодирования), что он может добавить значение свойства id к URL-адресу, путь которого - /design. Аналогично, если бы он хотел выполнить операцию HTTP над одним из ингредиентов, ему нужно было бы знать, что он может добавить значение свойства id ингредиента к URL-адресу, путь которого равен /ingredients. В любом случае ему также необходимо добавить префикс к этому пути с http: // или https: // и имя хоста API.

Напротив, если API включен с гипермедиа, API будет описывать свои собственные URL-адреса, освобождая клиента от необходимости жестко кодировать эти знания. Тот же список недавно созданных тако мог бы выглядеть как следующий список, если бы были вставлены гиперссылки.

Листинг 6.3. Список тако-ресурсов с гиперссылками

{

"_embedded": {

"tacoResourceList": [

{

"name": "Veg-Out",

"createdAt": "2018-01-31T20:15:53.219+0000",

"ingredients": [

{

"name": "Flour Tortilla", "type": "WRAP",

"_links": {

"self": { "href": "http://localhost:8080/ingredients/FLTO" }

}

},

{

"name": "Corn Tortilla", "type": "WRAP",

"_links": {

"self": { "href": "http://localhost:8080/ingredients/COTO" }

}

},

{

"name": "Diced Tomatoes", "type": "VEGGIES",

"_links": {

"self": { "href": "http://localhost:8080/ingredients/TMTO" }

}

},

{

"name": "Lettuce", "type": "VEGGIES",

"_links": {

"self": { "href": "http://localhost:8080/ingredients/LETC" }

}

},

{

"name": "Salsa", "type": "SAUCE",

"_links": {

"self": { "href": "http://localhost:8080/ingredients/SLSA" }

}

}

],

"_links": {

"self": { "href": "http://localhost:8080/design/4" }

}

},

...

]

},

"_links": {

"recents": {

"href": "http://localhost:8080/design/recent"

}

}

}

Этот конкретный вариант HATEOAS известен как HAL (Hypertext Application Language; http://stateless.co/hal_specification.html), простой и обычно используемый формат для встраивания гиперссылок в JSON ответы.

Хотя этот листинг не такой лаконичный, как раньше, он предоставляет некоторую полезную информацию. Каждый элемент в этом новом списке тако включает в себя свойство с именем _links, которое содержит гиперссылки для клиента для навигации по API. В этом примере у обоих тако и ингредиентов есть собственные ссылки для ссылки на эти ресурсы, и весь список имеет ссылку recents, которая ссылается на себя.

Если клиентскому приложению необходимо выполнить HTTP-запрос к тако в массиве, его не нужно формировать на основе знания, как будет выглядеть URL ресурса тако. Вместо известно, что нужно запросить собственную ссылку, которая отображается на http: //localhost:8080/design/4. Если клиент хочет иметь дело с конкретным ингредиентом, ему нужно только перейти по ссылке на себя для этого ингредиента.

Проект Spring HATEOAS обеспечивает поддержку гиперссылок в Spring. Он предлагает набор классов и ассемблеров ресурсов, которые можно использовать для добавления ссылок на ресурсы перед их возвратом из контроллера Spring MVC.

Чтобы включить гипермедиа в Taco Cloud API, вам нужно добавить starter Spring HATEOAS в зависимости:

org.springframework.boot

spring-boot-starter-hateoas

Этот стартер не только добавляет Spring HATEOAS в classpath проекта, но также предоставляет автоконфигурацию для включения Spring HATEOAS. Все, что вам нужно сделать, это переработать ваши контроллеры так, чтобы они возвращали типы ресурсов вместо типов доменов.

Вы начнете с добавления гипермедиа-ссылок в список последних тако, возвращаемых GET-запросом в /design/recent.

6.2.1 Добавление гиперлинков

Spring HATEOAS предоставляет два основных типа, которые представляют гиперссылочные ресурсы: Resource и Resources. Тип Resource представляет один ресурс, тогда как Resources представляет собой набор ресурсов. Оба типа способны содержать ссылки на другие ресурсы. При возврате из метода Spring MVC REST контроллера ссылок, они будут включены в JSON (или XML), полученный клиентом.

Чтобы добавить гиперссылки к списку недавно созданных тако, вам нужно будет вернуться к методу recentTacos(), показанному в листинге 6.2. Исходная реализация возвращает List, который был прекрасным решением несколько страниц назад, но вам понадобилось, чтобы возвращались объекты ресурсов вместо этого. В следующем листинге показана новая реализация recentTacos(), которая включает в себя первые шаги по внедрению гиперссылок в списке недавно разработанных тако.

Листинг 6.4. Добавление гиперссылок на ресурсы

@GetMapping("/recent")

public Resources> recentTacos() {

PageRequest page = PageRequest.of(

0, 12, Sort.by("createdAt").descending());

List tacos = tacoRepo.findAll(page).getContent();

Resources> recentResources = Resources.wrap(tacos);

recentResources.add(

new Link("http://localhost:8080/design/recent", "recents"));

return recentResources;

}

В этой новой версии recentTacos(), вы больше не возвращаете непосредственно список тако. Вместо этого вы используете Resources.wrap() для переноса списка тако в качестве экземпляра Resources>, который в конечном итоге возвращается из метода. Но перед возвратом объекта Resources вы добавляете ссылку, имя отношения которой - recents, а URL-адрес которой http://localhost:8080/design/recent. Как следствие, следующий фрагмент JSON включен в ресурс, возвращаемый из запроса API:

"_links": {

"recents": {

"href": "http://localhost:8080/design/recent"

}

}

Это хорошее начало, но у вас еще есть работа. На данный момент единственная добавленная вами ссылка - на весь перечень; никакие ссылки не добавляются ни к самим ресурсам тако, ни к ингредиентам каждого тако. Вы добавите их в ближайшее время. Но сначала давайте обратимся к жестко закодированному URL, который вы задали для ссылки на recents.

Жесткое кодирование URL-адреса это достаточно плохая идея. Если ваши амбиции Taco Cloud не ограничиваются только тем, что приложение запускается только на вашей собственной машине где разрабатывается ресурс, вам нужен способ не хардкодить localhost:8080. К счастью, Spring HATEOAS предоставляет помощь в виде компоновщиков ссылок.

Наиболее полезным из компоновщиков ссылок Spring HATEOAS является ControllerLinkBuilder. Этот компоновщик ссылок достаточно умен, чтобы знать, что такое имя хоста, без необходимости его жесткого кодирования. Кроме того, он предоставляет удобный API для создания ссылок относительно базового URL-адреса любого контроллера.

Используя ControllerLinkBuilder, вы можете переписать хардкордное задание Link в RecentTacos() следующими строками:

Resources> recentResources = Resources.wrap(tacos);

recentResources.add(

ControllerLinkBuilder.linkTo(DesignTacoController.class)

.slash("recent")

.withRel("recents"));

Вам не только больше не нужно хардкодить имя хоста, вам также не нужно указывать путь /design. Вместо этого вы запрашиваете ссылку на DesignTacoController, базовый путь которого /design. ControllerLinkBuilder использует базовый путь контроллера в качестве основы создаваемого вами объекта Link.

Далее следует вызов одного из моих любимых методов в любом Spring проекте : slash (). Мне нравится этот метод, потому что он так кратко описывает, что именно он собирается делать. Он буквально добавляет косую черту (/) и заданное значение в URL. В результате путь URL-адреса /design/recent.

Наконец, вы указываете имя отношения для ссылки. В этом примере отношение называется recents.

Хотя я большой поклонник метода slash(), у ControllerLinkBuilder есть еще один метод, который может помочь устранить любое жесткое кодирование, связанное с URL-адресами ссылок. Вместо того, чтобы вызывать slash(), вы можете вызвать linkTo(), передав ему в метод контроллер, чтобы ControllerLinkBuilder получал базовый URL как из базового пути контроллера, так и из сопоставленного пути метода. Следующий код написан с использованием метода linkTo():

Resources> recentResources = Resources.wrap(tacos);

recentResources.add(

linkTo(methodOn(DesignTacoController.class).recentTacos())

.withRel("recents"));

Здесь я решил статически включить методы linkTo() и methodOn() (оба из ControllerLinkBuilder), чтобы облегчить чтение кода. Метод methodOn() берет класс контроллера и позволяет вам вызвать метод recentTacos(), который перехватывается ControllerLinkBuilder и используется для определения не только базового пути контроллера, но и пути, сопоставленного с recentTacos(). Теперь весь URL-адрес получен из сопоставлений контроллера, и нет никакого хардкода. Великолепно!

6.2.2 Создание ресурсов ассемблеров (assemblers)

Теперь вам нужно добавить ссылки на ресурс тако, содержащийся в списке. Один из вариантов - циклически проходить по каждому из элементов Resource, содержащихся в объекте Resources, добавляя ссылку на каждый из них по отдельности. Но это немного утомительно, и вам нужно будет повторять этот код цикла в API везде, где вы возвращаете список ресурсов тако.

Нам нужна другая тактика.

Вместо того чтобы позволить Resources.wrap () создать объект Resource для каждого тако в списке, вы определите служебный класс, который преобразует объекты Taco в новый объект TacoResource. Объект TacoResource будет очень похож на Taco, но он также будет иметь возможность переносить ссылки. Следующий листинг показывает, как может выглядеть TacoResource.

Листинг 6.5. Тако-ресурс, несущий данные домена и список гиперссылок

package tacos.web.api;

import java.util.Date;

import java.util.List;

import org.springframework.hateoas.ResourceSupport;

import lombok.Getter;

import tacos.Ingredient;

import tacos.Taco;

public class TacoResource extends ResourceSupport {

@Getter

private final String name;

@Getter

private final Date createdAt;

@Getter

private final List ingredients;

public TacoResource(Taco taco) {

this.name = taco.getName();

this.createdAt = taco.getCreatedAt();

this.ingredients = taco.getIngredients();

}

}

Во многом TacoResource ничем не отличается от доменного типа Taco. У них обоих есть свойства name, createAt и ingredients. Но TacoResource расширяет ResourceSupport для наследования списка объектов Link и методов для управления списком ссылок.

Более того, TacoResource не включает свойство id из Taco. Это потому, что нет необходимости предоставлять какие-либо специфичные для базы данных идентификаторы в API. Самостоятельная ссылка ресурса будет служить идентификатором ресурса с точки зрения клиента API.

ПРИМЕЧАНИЕ Домены и ресурсы: отдельные или одинаковые? Некоторые разработчики Spring могут объединить свои доменные типы и ресурсные в один тип, если их типы доменов расширяют ResourceSupport. Так тоже можно, нет правильного или неправильного ответа относительно того, какой путь правильный. Я выбрал создание отдельного типа ресурса, чтобы в Taco не было ненужных загромождений ссылками на ресурсы в тех случаях, когда ссылки не нужны. Кроме того, создав отдельный тип ресурса, я смог легко опустить свойство id, чтобы оно не отображалось в API.

TacoResource имеет единственный конструктор, который принимает Taco и копирует соответствующие свойства из Taco в его собственные свойства. Это облегчает преобразование объекта Taco в TacoResource. Но если вы остановитесь на этом, вам все равно понадобится цикл для преобразования списка объектов Taco в Resources.

Чтобы помочь в преобразовании объектов Taco в объекты TacoResource, вы также собираетесь создать ассемблер ресурсов. Следующий список - это то, что вам нужно.

Листинг 6.6. Ассемблер ресурсов, который собирает тако-ресурсы

package tacos.web.api;

import org.springframework.hateoas.mvc.ResourceAssemblerSupport;

import tacos.Taco;

public class TacoResourceAssembler

extends ResourceAssemblerSupport {

public TacoResourceAssembler() {

super(DesignTacoController.class, TacoResource.class);

}

@Override

protected TacoResource instantiateResource(Taco taco) {

return new TacoResource(taco);

}

@Override

public TacoResource toResource(Taco taco) {

return createResourceWithId(taco.getId(), taco);

}

}

TacoResourceAssembler имеет конструктор по умолчанию, который сообщает суперклассу (ResourceAssemblerSupport), что он будет использовать DesignTacoController для определения базового пути для любых URL-адресов в ссылках, которые он создает при создании TacoResource.

Метод instantiateResource() переопределяется для создания экземпляра TacoResource с данным Taco. Этот метод необязательный, если TacoResource имеет конструктор по умолчанию. В этом случае, однако, TacoResource требует для построения Taco, поэтому вы должны переопределить его.

Метод toResource() является единственным методом, строго обязательным при расширении ResourceAssemblerSupport. Здесь вы говорите ему создать объект TacoResource из Taco и автоматически дать ему собственную ссылку с URL-адресом, полученным из свойства id объекта Taco.

На первый взгляд, toResource(), похоже, имеет аналогичное назначение что и instantiateResource(), но они служат несколько иным целям. В то время как instantiateResource() предназначен только для создания экземпляра объекта Resource, метод toResource() предназначен не только для создания объекта Resource, но и для заполнения его ссылками. Под капотом toResource() находиться вызов instantiateResource().

Теперь настройте метод recentTacos(), чтобы использовать TacoResourceAssembler:

@GetMapping("/recent")

public Resources recentTacos() {

PageRequest page = PageRequest.of(

0, 12, Sort.by("createdAt").descending());

List tacos = tacoRepo.findAll(page).getContent();

List tacoResources =

new TacoResourceAssembler().toResources(tacos);

Resources recentResources =

new Resources(tacoResources);

recentResources.add(

linkTo(methodOn(DesignTacoController.class).recentTacos())

.withRel("recents"));

return recentResources;

}

Вместо того чтобы возвращать Resources>, recentTacos() теперь возвращает Resources, чтобы воспользоваться вашим новым типом TacoResource. После извлечения тако из репозитория вы передаете список объектов Taco методу toResources() класса TacoResourceAssembler. Этот удобный метод циклически перебирает все объекты Taco, вызывая метод toResource(), который вы переопределили в TacoResourceAssembler, чтобы создать список объектов TacoResource.

Используюя этот список TacoResource вы затем создаете объект Resources, а затем заполняете его ссылкой на recents, как и в предыдущей версии recentTacos().

На этом этапе GET-запрос /design/ recent создаст список тако, каждый из которых имеет self ссылку и recents ссылку в самом списке. Но ингредиенты все равно останутся без ссылок. Чтобы решить эту проблему, вы создадите новый ассемблер ресурсов для ингредиентов:

package tacos.web.api;

import org.springframework.hateoas.mvc.ResourceAssemblerSupport;

import tacos.Ingredient;

class IngredientResourceAssembler extends

ResourceAssemblerSupport {

public IngredientResourceAssembler() {

super(IngredientController2.class, IngredientResource.class);

}

@Override

public IngredientResource toResource(Ingredient ingredient) {

return createResourceWithId(ingredient.getId(), ingredient);

}

@Override

protected IngredientResource instantiateResource(

Ingredient ingredient) {

return new IngredientResource(ingredient);

}

}

Как видите, IngredientResourceAssembler очень похож на TacoResourceAssembler, но работает с объектами Ingredient и IngredientResource вместо объектов Taco и TacoResource.

Говоря о IngredientResource, он выглядит так:

package tacos.web.api;

import org.springframework.hateoas.ResourceSupport;

import lombok.Getter;

import tacos.Ingredient;

import tacos.Ingredient.Type;

public class IngredientResource extends ResourceSupport {

@Getter

private String name;

@Getter

private Type type;

public IngredientResource(Ingredient ingredient) {

this.name = ingredient.getName();

this.type = ingredient.getType();

}

}

Как и в случае с TacoResource, IngredientResource расширяет ResourceSupport и копирует соответствующие свойства из типа домена в свой собственный набор свойств (исключая свойство id).

Осталось лишь немного изменить TacoResource, чтобы он содержал объекты IngredientResource вместо объектов Ingredient:

package tacos.web.api;

import java.util.Date;

import java.util.List;

import org.springframework.hateoas.ResourceSupport;

import lombok.Getter;

import tacos.Taco;

public class TacoResource extends ResourceSupport {

private static final IngredientResourceAssembler ingredientAssembler = new IngredientResourceAssembler();

@Getter

private final String name;

@Getter

private final Date createdAt;

@Getter

private final List ingredients;

public TacoResource(Taco taco) {

this.name = taco.getName();

this.createdAt = taco.getCreatedAt();

this.ingredients =ingredientAssembler.toResources(taco.getIngredients());

}

}

Эта новая версия TacoResource создает final статический экземпляр IngredientResourceAssembler и использует его метод toResource() для преобразования списка Ingredient для данного объекта Taco в список IngredientResource.

Ваш недавний список тако теперь полностью снабжен гиперссылками, причем не только на себя (ссылка на recents), но также для всех его записей тако и ингредиентов этих тако. Ответ должен быть очень похож на JSON в листинге 6.3.

Вы можете остановиться здесь и перейти к следующей теме. Но сначала я расскажу о том, что меня беспокоило в листинге 6.3.

6.2.3 Именование встроенных отношений

Если вы внимательнее посмотрите на листинг 6.3, вы заметите, что элементы верхнего уровня выглядят так:

{

"_embedded": {

"tacoResourceList": [

...

]

}

}

Прежде всего, позвольте мне обратить ваше внимание на имя tacoResourceList под embedded. Это имя было получено из того факта, что объект Resources был создан из List. Не то чтобы это было невероятно, но если бы вы реорганизовали имя класса TacoResource в другое, имя поля в результирующем JSON изменится, чтобы соответствовать ему. Это, вероятно, сломало бы любых клиентов, закодированных, чтобы рассчитывать на это имя.

Аннотация @Relation может помочь разорвать связь между именем поля JSON и именами классов типов ресурсов, как это определено в Java. Аннотируя TacoResource с помощью @Relation, вы можете указать, как Spring HATEOAS должен называть поле в результирующем JSON:

@Relation(value="taco", collectionRelation="tacos")

public class TacoResource extends ResourceSupport {

...

}

Здесь вы указали, что когда список объектов TacoResource используется в объекте Resources, он должен называться tacos. И хотя вы не используете это в нашем API, один объект TacoResource должен называться в JSON как taco.

В результате JSON, возвращенный из /design/recent, теперь будет выглядеть следующим образом (независимо от того, какой рефакторинг вы выполните в TacoResource):

{

"_embedded": {

"tacos": [

...

]

}

}

Spring HATEOAS делает добавление ссылок на ваш API довольно простым и понятным. Тем не менее, он добавил несколько строк кода, которые в противном случае вам не понадобились бы. По этой причине некоторые разработчики могут отказаться от использования HATEOAS в своих API, даже если это означает, что клиентский код может стать некорректным при изменении схемы URL API. Я призываю вас серьезно относиться к HATEOAS, а не лениться, не добавляя гиперссылки в ваши ресурсы.

Но если вы настаиваете на лени, то, возможно, для вас есть беспроигрышный сценарий, если вы используете Spring Data для своих репозиториев. Давайте посмотрим, как Spring Data REST может помочь вам автоматически создавать API на основе репозиториев, созданных с помощью Spring Data в главе 3.

6.3 Включение служб с поддержкой данных

Как вы видели в главе 3, Spring Data выполняет особую магию, автоматически создавая реализации репозитория на основе интерфейсов, которые вы определили в своем коде. Но у Spring Data есть еще одна хитрость, которая может помочь вам сгенерировать API для вашего приложения.

Spring Data REST - это еще один член семейства Spring Data, который автоматически создает API REST для репозиториев, созданных на основе Spring Data. Делая чуть больше, чем просто добавление Spring Data REST в вашу сборку, вы получаете API с операциями для каждого интерфейса репозитория, который вы определили.

Чтобы начать использовать Spring Data REST, добавьте в свою сборку следующую зависимость:

org.springframework.boot

spring-boot-starter-data-rest

Хотите верьте, хотите нет, но это все, что необходимо для предоставления REST API в проекте, который уже использует Spring Data для автоматизации репозиториев. Просто имея стартовый Spring REST в сборке, приложение получает автоконфигурацию, которая позволяет автоматически создавать REST API для любых репозиториев, которые были созданы Spring Data (включая Spring Data JPA, Spring Data Mongo и т. д.).

REST endpoint, которые создает Spring Data REST, по крайней мере так же хороши (и, возможно, даже лучше) тех, которые вы создали сами. Поэтому на данном этапе не стесняйтесь выполнить небольшую работу по удалению всех классов аннотированных @RestController, которые вы создали до этого момента, прежде чем двигаться дальше.

Чтобы опробовать endpoint, предоставляемые Spring Data REST, вы можете запустить приложение и начать тыкать в некоторые URL-адреса. Исходя из набора репозиториев, который вы уже определили для Taco Cloud, вы сможете выполнять запросы GET для тако, ингредиентов, заказов и пользователей.

Например, вы можете получить список всех ингредиентов, сделав запрос GET для /ingredients. Используя curl, вы можете получить что-то похожее на это (сокращенно, чтобы показать только первый ингредиент):

$ curl localhost:8080/ingredients

{

"_embedded" : {

"ingredients" : [ {

"name" : "Flour Tortilla",

"type" : "WRAP",

"_links" : {

"self" : {

"href" : "http://localhost:8080/ingredients/FLTO"

},

"ingredient" : {

"href" : "http://localhost:8080/ingredients/FLTO"

}

}

},

...

]

},

"_links" : {

"self" : {

"href" : "http://localhost:8080/ingredients"

},

"profile" : {

"href" : "http://localhost:8080/profile/ingredients"

}

}

}

Вот Это Да! Не делая ничего, кроме добавления зависимости к вашей сборке, вы получаете не endpoint для ингредиентов, но и возвращающиеся ресурсы также содержат гиперссылки! Притворяясь клиентом этого API, вы также можете использовать curl, чтобы перейти по ссылке self для записи мучной лепешки (FLTO):

$ curl http://localhost:8080/ingredients/FLTO

{

"name" : "Flour Tortilla",

"type" : "WRAP",

"_links" : {

"self" : {

"href" : "http://localhost:8080/ingredients/FLTO"

},

"ingredient" : {

"href" : "http://localhost:8080/ingredients/FLTO"

}

}

}

Чтобы не отвлекаться, мы не будем тратить больше времени в этой книге, копаясь во всех endpoint и опциях, созданных Spring Data REST. Но вы должны знать, что он также поддерживает методы POST, PUT и DELETE для endpoint, которые он создал. Верно: вы можете вызвать POST для /ingredients, чтобы создать новый ингредиент, и DELETE для /ingredients/FLTO, чтобы удалить лепешки из меню.

Одна вещь, которую вы, возможно, захотите сделать, это установить базовый путь для API, чтобы его endpoints были различны и не конфликтовали ни с какими контроллерами, которые вы пишете. (Фактически, если вы не удалите созданный ранее IngredientsController, он будет мешать endpoints /ingredients, предоставляемой Spring Data REST.) Чтобы настроить базовый путь для API, установите свойство spring.data.rest.base-path:

spring:

data:

rest:

base-path: /api

Это установит базовый путь для endpoint Spring Data REST в /api. Следовательно, конечной точкой ингредиентов теперь является /api/ingredients. Теперь проверим новый базовый путь, запросив список тако:

$ curl http://localhost:8080/api/tacos

{

"timestamp": "2018-02-11T16:22:12.381+0000",

"status": 404,

"error": "Not Found",

"message": "No message available",

"path": "/api/tacos"

}

О, Боже! Это сработало не совсем так, как ожидалось. У вас есть сущность Ingredient и интерфейс IngredientRepository, которые Spring Data REST предоставляет с помощью endpoint /api/ingredients. Итак, если у вас есть сущность Taco и интерфейс TacoRepository, почему Spring Data REST не предоставляет endpoint /api/tacos?

6.3.1 Настройка путей ресурсов и имен отношений

На самом деле Spring Data REST предоставляет вам endpoint для работы с тако. Но насколько бы умным ни был Spring Data REST, он показывает несколько странным, в том как он предоставляет конечную точку tacos.

При создании endpoint для репозиториев Spring Data, Spring Data REST пытается мультиплицировать связанный класс сущностей. Для объекта Ingredient endpoint является /ingredients. Для сущностей Order и User это /orders и /users. Пока все логично.

Но иногда, например, с “taco”,, оно путается в слове, и множественная версия не совсем верна. Как выяснилось, Spring Data REST назвала“taco” как “tacoes”, поэтому, чтобы сделать запрос на тако, вы должны учесть это и запросить /api/tacoes:

% curl localhost:8080/api/tacoes

{

"_embedded" : {

"tacoes" : [ {

"name" : "Carnivore",

"createdAt" : "2018-02-11T17:01:32.999+0000",

"_links" : {

"self" : {

"href" : "http://localhost:8080/api/tacoes/2"

},

"taco" : {

"href" : "http://localhost:8080/api/tacoes/2"

},

"ingredients" : {

"href" : "http://localhost:8080/api/tacoes/2/ingredients"

}

}

}]

},

"page" : {

"size" : 20,

"totalElements" : 3,

"totalPages" : 1,

"number" : 0

}

}

Вы можете быть удивлены, откуда я знал, что “taco” будет истолковано как “tacoes”. Оказывается, Spring Data REST также предоставляет домашний ресурс, содержащий ссылки для всех открытых endpoint. Просто сделайте запрос GET к базовому пути API:

$ curl localhost:8080/api

{

"_links" : {

"orders" : {

"href" : "http://localhost:8080/api/orders"

},

"ingredients" : {

"href" : "http://localhost:8080/api/ingredients"

},

"tacoes" : {

"href" : "http://localhost:8080/api/tacoes{?page,size,sort}",

"templated" : true

},

"users" : {

"href" : "http://localhost:8080/api/users"

},

"profile" : {

"href" : "http://localhost:8080/api/profile"

}

}

}

Как вы можете видеть, здесь есть ссылки для всех ваших сущностей. Все выглядит хорошо, за исключением ссылки tacos, где и имя отношения, и URL имеют нечетное множественное число “taco”.

Хорошей новостью является то, что вам не нужно мириться с этой маленькой причудой Spring Data REST. Добавив простую аннотацию к классу Taco, вы можете настроить как имя отношения, так и этот путь:

@Data

@Entity

@RestResource(rel="tacos", path="tacos")

public class Taco {

...

}

Аннотация @RestResource позволяет вам присвоить сущности любое имя и путь отношения. В этом случае вы устанавливаете их обоих на “tacos”. Теперь, когда вы запрашиваете домашний ресурс, вы видите ссылку tacos с правильным множественным числом:

"tacos" : {

"href" : "http://localhost:8080/api/tacos{?page,size,sort}",

"templated" : true

},

Это также сортирует путь для endpoint, чтобы вы могли создавать запросы к /api/tacos для работы с ресурсами taco.

Говоря о сортировке, давайте посмотрим, как можно отсортировать результаты с Spring Data REST endpoint.

6.3.2 Paging и sorting

Возможно, вы заметили, что все ссылки на домашнем ресурсе предлагают дополнительные параметры page, size, и sort. По умолчанию запросы возвращающие коллекцию, например /api/tacos, возвращают первую страницу с количеством объектов на ней числом до 20-ти. Но вы можете настроить размер страницы и отображаемую страницу, указав параметры page и size в своем запросе.

Например, чтобы запросить первую страницу тако, где размер страницы равен 5, вы можете выполнить следующий запрос GET (используя curl):

$ curl "localhost:8080/api/tacos?size=5"

Предположим, что общее количество тако более пяти, вы можете запросить вторую страницу тако, добавив параметр page:

$ curl "localhost:8080/api/tacos?size=5&page=1"

Обратите внимание, что параметр страницы начинается с нуля, что означает, что запрос страницы 1 на самом деле запрашивает вторую страницу. (Вы также заметите, что многие запросы командной строки смещаются над амперсандом в запросе, поэтому я привел весь URL в предыдущей команде curl.)

Вы можете использовать манипуляции со строками, чтобы добавить эти параметры в URL, но HATEOAS приходит на помощь, предлагая ссылки на первую, последнюю, следующую и предыдущие страницы в ответе:

"_links" : {

"first" : {

"href" : "http://localhost:8080/api/tacos?page=0&size=5"

},

"self" : {

"href" : "http://localhost:8080/api/tacos"

},

"next" : {

"href" : "http://localhost:8080/api/tacos?page=1&size=5"

},

"last" : {

"href" : "http://localhost:8080/api/tacos?page=2&size=5"

},

"profile" : {

"href" : "http://localhost:8080/api/profile/tacos"

},

"recents" : {

"href" : "http://localhost:8080/api/tacos/recent"

}

}

С этими ссылками клиент API не должен отслеживать, на какой странице он находится, и объединять параметры с URL. Вместо этого он должен просто знать, что нужно искать одну из этих навигационных ссылок на странице по ее имени и следовать за ней.

Параметр sort позволяет сортировать полученный список по любому свойству объекта. Например, вам нужен способ получить 12 последних созданных тако. Вы можете сделать это, указав следующее сочетание параметров страницы и сортировки:

$ curl "localhost:8080/api/tacos?sort=createdAt,desc&page=0&size=12"

Здесь параметр sort указывает, что должна произвестись сортировка по свойству createdDate в порядке убывания (чтобы самые новые тако были первыми). Параметры page и size указывают, что вы должны увидеть первую страницу с 12 тако.

Это именно то, что нужно UI, чтобы показать самые последние созданные тако. Результат примерно такой же, как /design/recent endpoint, которую вы определили в DesignTacoController ранее в этой главе.

Однако есть небольшая проблема. Код пользовательского интерфейса должен быть жестко запрограммирован, чтобы запросить список тако с этими параметрами. Конечно, это будет работать. Но вы добавляете некоторую хрупкость клиенту, делая его слишком осведомленным о том, как создать запрос API. Было бы здорово, если бы клиент мог найти URL-адрес из списка ссылок. И было бы еще круче, если бы URL был более лаконичным, как /design/recent endpoint, которую вы использовали ранее.

6.3.3 Добавление пользовательских endpoint

Spring Data REST отлично подходит для создания endpoint для выполнения операций CRUD с репозиториями Spring Data. Но иногда вам нужно отойти от CRUD API по умолчанию и создать endpoint, которая доходит до сути проблемы.

Ничто не мешает вам реализовать любой endpoint, который вы хотите, в аннотированном компоненте @RestController, чтобы дополнить то, что Spring Data REST генерирует автоматически. Фактически, вы можете воскресить DesignTacoController, описанный ранее в этой главе, и он все равно будет работать вместе с конечными точками, предоставленными Spring Data REST.

Но когда вы пишете свои собственные API контроллеров, их endpoint кажутся несколько оторванными от endpoint Spring Data REST по нескольким причинам:

Ваши собственные endpoint контроллера не отображаются в соответствии с базовым путем Spring Data REST. Вы можете принудительно назначить префиксу их сопоставлений любой базовый путь, который вы хотите, включая базовый путь Spring Data REST, но если базовый путь должен был измениться, вам необходимо отредактировать сопоставления контроллера, чтобы они соответствовали.

Любые endpoint, которые вы определяете в своих собственных контроллерах, не будут автоматически включены в качестве гиперссылок в ресурсы, возвращаемые endpoint Spring Data REST. Это означает, что клиенты не смогут обнаружить ваши пользовательские endpoint с именем отношения.

Давайте сначала обратимся к проблеме базового пути. Spring Data REST включает в себя @RepositoryRestController, новую аннотацию для аннотирования классов контроллеров, чьи отображения должны принимать базовый путь, который совпадает с тем, который настроен для endpoint Spring Data REST. Проще говоря, все сопоставления в контроллере, помеченном @RepositoryRestController, будут иметь свой путь с префиксом со значением свойства spring.data.rest.base-path (которое вы настроили как / api).

Вместо того, чтобы воскрешать DesignTacoController, в котором было несколько методов-обработчиков, которые вам не нужны, вы создадите новый контроллер, который содержит только метод recentTacos(). RecentTacosController в следующем листинге помечен аннотацией @RepositoryRestController для принятия базового пути Spring Data REST для его сопоставлений запросов (для request mapping).

Листинг 6.7. Применение базового пути Spring Data REST к контроллеру

package tacos.web.api;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;

import java.util.List;

import org.springframework.data.domain.PageRequest;

import org.springframework.data.domain.Sort;

import org.springframework.data.rest.webmvc.RepositoryRestController;

import org.springframework.hateoas.Resources;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.GetMapping;

import tacos.Taco;

import tacos.data.TacoRepository;

@RepositoryRestController

public class RecentTacosController {

private TacoRepository tacoRepo;

public RecentTacosController(TacoRepository tacoRepo) {

this.tacoRepo = tacoRepo;

}

@GetMapping(path="/tacos/recent", produces="application/hal+json")

public ResponseEntity> recentTacos() {

PageRequest page = PageRequest.of(

0, 12, Sort.by("createdAt").descending());

List tacos = tacoRepo.findAll(page).getContent();

List tacoResources = new TacoResourceAssembler().toResources(tacos);

Resources recentResources = new Resources(tacoResources);

recentResources.add(

linkTo(methodOn(RecentTacosController.class).recentTacos())

.withRel("recents"));

return new ResponseEntity<>(recentResources, HttpStatus.OK);

}

}

Несмотря на то, что @GetMapping сопоставляется с путем /tacos/recent, аннотация @RepositoryRestController на уровне класса гарантирует, что к нему будет добавлен базовый путь Spring Data REST. Метод recentTacos() будет обрабатывать запросы GET для /api/tacos/recent.

Важно отметить, что хотя @RepositoryRestController назван так же, как @RestController, он не обладает той же семантикой, что и @RestController. В частности, он не гарантирует, что значения, возвращаемые из методов-обработчиков, автоматически записываются в тело ответа. Поэтому вам нужно либо аннотировать метод с помощью @ResponseBody, либо возвращать ResponseEntity, который оборачивает данные ответа. Здесь решили вернуть ResponseEntity.

При использовании RecentTacosController в запросах на /api/tacos/recent будет возвращено до 15 самых последних созданных тако без необходимости разбивать на страницы и сортировать параметры в URL-адресе. Но он все еще не появляется в списке гиперссылок при запросе /api/tacos. Давайте исправим это.

6.3.4 Добавление пользовательских гиперссылок в endpoint Spring Data

Если endpoint недавно созданных тако не входит в число гиперссылок, возвращаемых из /api/tacos, как клиент будет знать, как получить самые последние тако? Он должен либо угадать, либо использовать параметры страницы и сортировки. В любом случае, это будет жестко закодировано в клиентском коде, что не идеально.

Однако, объявив ресурс обработчик bean-компонентов (resource processor bean), вы можете добавить ссылки в список ссылок Spring Data REST. Spring Data HATEOAS предлагает ResourceProcessor, интерфейс для управления ресурсами до их возврата через API. Для ваших целей вам нужна реализация ResourceProcessor, которая добавляет recents ссылку на любой ресурс типа PagedResources > (тип, возвращаемый для /api/tacos endpoint). В следующем листинге показан метод объявления bean-компонента, определяющий ResourceProcessor.

Листинг 6.8. Добавление пользовательских ссылок в REST Spring Data endpoint

@Bean

public ResourceProcessor>>

tacoProcessor(EntityLinks links) {

return new ResourceProcessor>>() {

@Override

public PagedResources> process(

PagedResources> resource) {

resource.add(

links.linkFor(Taco.class)

.slash("recent")

.withRel("recents"));

return resource;

}

};

}

ResourceProcessor, показанный в листинге 6.8, определяется как анонимный внутренний класс и объявляется как bean-компонент, создаваемый в контексте приложения Spring. Spring HATEOAS обнаружит этот bean-компонент (как и любые другие bean-компоненты типа ResourceProcessor) автоматически и применяет их к соответствующим ресурсам. В этом случае, если из контроллера возвращается PagedResources>, он получит ссылку на самые последние созданные тако. Это включает в себя ответ на запросы для /api/tacos.

Итог

REST endpoint могут быть созданы с помощью Spring MVC с контроллерами, которые следуют той же модели программирования, что и контроллеры, ориентированные на браузер.

Методы обработчика контроллера могут быть аннотированы с помощью @ResponseBody или возвращать объекты ResponseEntity для обхода модели, просмотра и записи данных непосредственно в тело ответа.

Аннотация @RestController упрощает контроллеры REST, устраняя необходимость использования @ResponseBody в методах-обработчиках.

Spring HATEOAS обеспечивает включение гиперссылок ресурсов, возвращаемых контроллерами Spring MVC.

Репозитории Spring Data могут автоматически отображаться как API REST с помощью Spring Data REST.

Spring in Action Covers Spring 5.0 перевод на русский. Глава 7

REST сервисы

В этой главе рассматриваются

Использование RestTemplate для REST API

Навигация по API гипермедиа с помощью Traverson

Вы когда-нибудь ходили в кино и были единственный человек в театре? Это, безусловно, замечательный опыт, который по сути является приватным просмотром фильма. Вы можете выбрать любое место, которое захотите, поговорить с персонажами на экране и, возможно, даже открыть свой телефон и написать об этом в Твиттере, чтобы никто не рассердился за то, что нарушил их просмотр фильма. И самое приятное то, что никто больше не портит фильм для вас!

Это случалось не часто со мной. Но когда это произошло, я подумал, что бы случилось, если бы я не появился. Они все еще показали бы фильм? Герой все еще спас бы день? Должны ли сотрудники театра убирать театр после окончания фильма?

Фильм без аудитории - это как API без клиента. Он готов принимать и предоставлять данные, но если API никогда не вызывается, действительно ли это API? Как и кот Шредингера, мы не можем знать, активен ли API или возвращает ответы HTTP 404, пока мы не отправим к нему запрос.

В предыдущей главе мы сосредоточились на определении REST endpoint, которые могут использоваться некоторыми клиентами, внешними по отношению к вашему приложению. Хотя движущей силой для разработки такого API было одностраничное приложение Angular, которое служило веб-сайтом Taco Cloud, реальность такова, что клиент может быть любым приложением на любом языке - даже другим приложением Java.

Приложения Spring нередко предоставляют API и отправляют запросы к API другого приложения. Фактически, это становится распространенным в мире микросервисов. Поэтому стоит потратить немного времени на изучение того, как использовать Spring для взаимодействия с REST API.

Приложение Spring может использовать REST API с

RestTemplate - простой, синхронный REST-клиент, предоставляемый ядром Spring Framework.

Traverson - синхронный REST-клиент с поддержкой гиперссылок, предоставляемый Spring HATEOAS. Вдохновленный из одноименной библиотеки JavaScript.

WebClient - реактивный, асинхронный клиент REST, представленный в Spring 5.

Я отложу обсуждение WebClient до тех пор, пока мы не рассмотрим реактивную веб-инфраструктуру Spring в главе 11. Сейчас мы сосредоточимся на двух других REST клиентах, начиная с RestTemplate.

7.1 Использование REST endpoint с RestTemplate

Есть много, что входит во взаимодействие с ресурсом REST с точки зрения клиента-в основном скука и шаблонность. Работая с низкоуровневыми библиотеками HTTP, клиент должен создать экземпляр клиента и объект запроса, выполнить запрос, интерпретировать ответ, сопоставить ответ с объектами домена и обработать любые исключения, которые могут быть брошены по пути. И все это шаблонное повторяется, независимо от того, какой HTTP-запрос отправляется.

Чтобы избежать такого стандартного кода, Spring предоставляет RestTemplate. Так же, как JDBCTemplate обрабатывает уродливые части работы с JDBC, RestTemplate освобождает вас от скуки при использовании ресурсов REST.

RestTemplate предоставляет 41 метод для взаимодействия с ресурсами REST. Вместо того чтобы изучать все методы, которые он предлагает, проще рассмотреть только дюжину уникальных операций, каждая из которых перегружена, и в конечном итоге составляет полный набор в 41 метод. Эти 12 операций описаны в таблице 7.1.

Таблица 7.1 RestTemplate определяет 12 уникальных операций, каждая из которых перегружена, обеспечивая в общей сложности 41 метод.

Метод - Описание

delete(...) - Выполняет HTTP-запрос на удаление ресурса по указанному URL-адресу

exchange(...) - Выполняет указанный метод HTTP для URL, возвращая ResponseEntity, содержащий объект, сопоставленный с телом ответа

execute(...) - Выполняет указанный метод HTTP для URL, возвращая объект, сопоставленный с телом ответа

getForEntity(...) - Отправляет HTTP-запрос GET, возвращая ResponseEntity, содержащий объект, сопоставленный с телом ответа.

getForObject(...) - Отправляет HTTP-запрос GET, возвращая объект, сопоставленный с телом ответа.

headForHeaders(...) - Отправляет запрос HTTP HEAD, возвращая заголовки HTTP для указанного URL ресурса

optionsForAllow(...) - Отправляет запрос HTTP OPTIONS, возвращая заголовок Allow для указанного URL

patchForObject(...) - Отправляет запрос HTTP PATCH, возвращая полученный объект, сопоставленный с телом ответа.

postForEntity(...) - помещает POST данные в URL, возвращая ResponseEntity, содержащий объект, сопоставленный с телом ответа

postForLocation(...) - помещает POST данные в URL, возвращая URL вновь созданного ресурса

postForObject(...) - помещает POST данные в URL, возвращая объект, сопоставленный с телом ответа

put(...) - Помещает PUT данные ресурса в указанный URL

За исключением TRACE, RestTemplate имеет по крайней мере один метод для каждого из стандартных методов HTTP. Кроме того, execute () и exchange () предоставляют низкоуровневые методы общего назначения для отправки запросов любым методом HTTP.

Большинство методов в таблице 7.1 перегружены в три формы методов:

Можно принять спецификацию String URL с параметрами URL, указанными в списке переменных аргументов.

Можно принять спецификацию String URL с параметрами URL, указанными в Map.

В качестве спецификации URL-адреса принимается java.net.URI без поддержки параметризованных URL-адресов.

Как только вы разберетесь с 12 операциями, предоставляемыми RestTemplate, и с тем, как работает каждая с вариантами форм методов, вы сможете приступить к написанию ресурсоемких REST-клиентов.

Чтобы использовать RestTemplate, вам нужно либо создать экземпляр там, где вам это нужно

RestTemplate rest = new RestTemplate();

или вы можете объявить его как bean и внедрить его там, где вам это нужно:

@Bean

public RestTemplate restTemplate() {

return new RestTemplate();

}

Давайте рассмотрим операции RestTemplate, рассмотрев те из них, которые поддерживают четыре основных метода HTTP: GET, PUT, DELETE и POST. Начнем с getForObject() и getForEntity() - метод GET.

7.1.1 GET (получение) ресурсов

Предположим, вы хотите получить компонент из Taco Cloud API. Предполагая, что API не поддерживает HATEOAS, вы можете использовать getForObject() для извлечения ингредиента. Например, следующий код использует RestTemplate для извлечения объекта Ingredient по его идентификатору:

public Ingredient getIngredientById(String ingredientId) {

return rest.getForObject("http://localhost:8080/ingredients/{id}",

Ingredient.class, ingredientId);

}

Здесь вы используете вариант getForObject(), который принимает String URL и использует список переменных для переменных URL. Параметр ingredientId, передаваемый в getForObject(), используется для заполнения элемента {id} в указанном URL. Хотя в этом примере есть только одна переменная URL, важно знать, что параметры переменных назначаются местозаполнителям в том порядке, в котором они указаны.

Второй параметр в getForObject() - это тип, к которому должен быть приведен ответ. В этом случае данные ответа (вероятно, в формате JSON) должны быть десериализованы в объект Ingredient, который будет возвращен.

Кроме того, вы можете использовать Map, чтобы указать переменные URL:

public Ingredient getIngredientById(String ingredientId) {

Map urlVariables = new HashMap<>();

urlVariables.put("id", ingredientId);

return rest.getForObject("http://localhost:8080/ingredients/{id}",

Ingredient.class, urlVariables);

}

В этом случае значение ingredientId отображается на ключ id. Когда запрос выполняется, заполнитель {id} заменяется записью map-ы, ключом которой является id.

Использование параметра URI является более сложным процессом, требующим создания объекта URI перед вызовом getForObject(). В остальном он похож на оба других варианта:

public Ingredient getIngredientById(String ingredientId) {

Map urlVariables = new HashMap<>();

urlVariables.put("id", ingredientId);

URI url = UriComponentsBuilder

.fromHttpUrl("http://localhost:8080/ingredients/{id}")

.build(urlVariables);

return rest.getForObject(url, Ingredient.class);

}

Здесь объект URI определен из спецификации String, а его заполнители заполнены из записей в Map, как и в предыдущем варианте getForObject(). Метод getForObject() - это простой способ извлечения ресурса. Но если клиенту нужно больше, чем содержимое body, вы можете рассмотреть возможность использования getForEntity().

getForEntity() работает почти так же, как getForObject(), но вместо возврата объекта домена, представляющего содержимым body ответа, он возвращает объект ResponseEntity, который оборачивает этот объект домена. ResponseEntity предоставляет доступ к дополнительным деталям ответа, таким как заголовки ответа.

Например, предположим, что в дополнение к данным ингредиента вы хотите посмотреть заголовок Date из ответа. С getForEntity() это становится простым:

public Ingredient getIngredientById(String ingredientId) {

ResponseEntity responseEntity = rest.getForEntity("http://localhost:8080/ingredients/{id}",

Ingredient.class, ingredientId);

log.info("Fetched time: " +

responseEntity.getHeaders().getDate());

return responseEntity.getBody();

}

Метод getForEntity() перегружен теми же параметрами, что и getForObject(), поэтому вы можете предоставить переменные URL-адреса в качестве параметра списка переменных или вызвать getForEntity() с объектом URI.

7.1.2 PUT ресурсов

Для отправки HTTP PUT-запросов, RestTemplate предлагает метод put(). Все три перегруженных варианта put() принимают Object, который должен быть сериализован и отправлен по указанному URL. Что касается самого URL, он может быть указан как объект URI или как String. И как c getForObject() и getForEntity(), переменные URL-адреса могут быть предоставлены либо как список аргументов переменной, либо как Map.

Предположим, что вы хотите заменить ресурс ингредиента данными из нового объекта Ingredient. Следующий код должен сделать это:

public void updateIngredient(Ingredient ingredient) {

rest.put("http://localhost:8080/ingredients/{id}",

ingredient,

ingredient.getId());

}

Здесь URL задан в виде строки и содержит заполнитель, который заменяется свойством id данного объекта Ingredient. Данные для отправки - это сам объект Ingredient. Метод put() возвращает void, поэтому вам ничего не нужно делать для обработки возвращаемого значения.

7.1.3 DELETE ресурсов

Предположим, что Taco Cloud больше не предлагает ингредиент и хочет полностью удалить его в качестве опции. Чтобы это произошло, вы можете вызвать метод delete() из RestTemplate:

public void deleteIngredient(Ingredient ingredient) {

rest.delete("http://localhost:8080/ingredients/{id}",

ingredient.getId());

}

В этом примере для delete() передаются только URL-адрес (указанный как String) и значение переменной URL-адреса. Но, как и в случае с другими методами RestTemplate, URL-адрес может быть указан как объект URI или параметры URL-адреса представлены как Map.

7.1.4 POST данных ресурсов

Теперь допустим, что вы добавили новый ингредиент в меню Taco Cloud. Это сделает HTTP POST-запрос к .../ingredients endpoint с данными ингредиентов в теле запроса. RestTemplate имеет три способа отправки запроса POST, каждый из которых имеет одинаковые перегруженные варианты для указания URL. Если вы хотите получить вновь созданный ресурс Ingredient после POST-запроса, вы должны использовать postForObject() следующим образом:

public Ingredient createIngredient(Ingredient ingredient) {

return rest.postForObject("http://localhost:8080/ingredients",

ingredient,

Ingredient.class);

}

Этот вариант метода postForObject() принимает String URL спецификацию, объект, который должен быть отправлен на сервер, и доменный тип, с которым должно быть связано тело ответа. Хотя в этом случае вы не пользуетесь этим но, четвертым параметром может быть Map значения переменной URL-адреса или список переменных параметров для замены в URL.

Если для нужд клиента требуется получить ссылку на расположении только что созданного ресурса, вы можете вызвать postForLocation() :

public URI createIngredient(Ingredient ingredient) {

return rest.postForLocation("http://localhost:8080/ingredients",ingredient);

}

Обратите внимание, что postForLocation() работает так же, как postForObject(), за исключением того, что он возвращает URI вновь созданного ресурса вместо самого объекта ресурса. Возвращенный URI получен из заголовка Location ответа. В случае, если вам понадобятся как местоположение, так и полезная нагрузка ответа, вы можете вызвать postForEntity():

public Ingredient createIngredient(Ingredient ingredient) {

ResponseEntity responseEntity = rest.postForEntity("http://localhost:8080/ingredients",

ingredient, Ingredient.class);

log.info("New resource created at " +

responseEntity.getHeaders().getLocation());

return responseEntity.getBody();

}

Хотя методы RestTemplate отличаются по своему назначению, они очень похожи в том, как они используются. Это позволяет легко понять RestTemplate и использовать его в клиентском коде.

С другой стороны, если API, который вы используете, включает в свой ответ гиперссылки, RestTemplate не так полезен. Конечно, можно получить более подробные данные ресурса с помощью RestTemplate и работать с содержимым и ссылками, содержащимися в нем, но это не тривиально. Вместо того, чтобы бороться с использованием гипермедиа API с RestTemplate, давайте обратим наше внимание на клиентскую библиотеку, созданную для таких целей - Traverson.

7.2 Навигация REST API с помощью Traverson

Traverson поставляется с Spring Data HATEOAS как готовое решение для использования гипермедиа API в приложениях Spring. Эта библиотека на основе Java основана на аналогичной библиотеке JavaScript с тем же именем (https://github.com/traverson/traverson).

Возможно, вы заметили, что имя Трэверсон звучит как «traverse on», что является хорошим способом описать, как оно используется. В этом разделе вы будете использовать API путем обхода API по именам отношений.

Работа с Traverson начинается с создания экземпляра объекта Traverson с базовым API URI:

Traverson traverson = new Traverson(URI.create("http://localhost:8080/api"), MediaTypes.HAL_JSON);

Здесь я указал Traverson на базовый URL Taco Cloud (работает локально). Это единственный URL, который вам нужно указать Traverson-у. С этого момента вы будете перемещаться по API по именам отношений ссылок. Вы также укажете, что API будет генерировать ответы JSON с гиперссылками в стиле HAL, чтобы Traverson знал, как анализировать входящие данные ресурса. Как и RestTemplate, вы можете создать экземпляр объекта Traverson перед его использованием или объявить его как компонент, который будет введен везде, где это необходимо.

С объектом Traverson вы можете начать использовать API, перейдя по ссылкам. Например, предположим, что вы заинтересованы в получении списка всех ингредиентов. Из раздела 6.3.1 вы знаете, что ссылка ингредиентов имеет свойство href, которое ссылается на ресурс ingredients. Вам нужно перейти по этой ссылке:

ParameterizedTypeReference> ingredientType =

new ParameterizedTypeReference>() {};

Resources ingredientRes =

traverson

.follow("ingredients")

.toObject(ingredientType);

Collection ingredients = ingredientRes.getContent();

Вызвав метод follow() для объекта Traverson, вы можете перейти к ресурсу, имя связи которого - ingredients. Теперь, когда клиент перешел к ингредиентам, вам нужно принять содержимое этого ресурса, вызвав toObject().

Метод toObject () требует, чтобы вы указали, в какой тип объекта считывать данные. Это может быть немного сложнее, учитывая, что вам нужно прочитать его как объект Resources, а стирание типа Java затрудняет предоставление информации о типе для generic типа. Но создание ParameterizedTypeReference помогает в этом.

В качестве аналогии представьте, что вместо REST API это была домашняя страница на веб-сайте. И вместо кода REST клиента представьте, что вы просматриваете эту домашнюю страницу в браузере. Вы видите ссылку на странице с надписью «Ингредиенты» и переходите по этой ссылке, щелкая ее. По прибытии на следующую страницу вы читаете страницу, которая аналогична тому, как Traverson принял содержимое как объект Resources.

Теперь давайте рассмотрим немного более интересный вариант использования. Допустим, вы хотите получить самые последние созданные тако. Начиная с домашнего ресурса, вы можете перейти к ресурсу с последними тако, например так:

ParameterizedTypeReference> tacoType =

new ParameterizedTypeReference>() {};

Resources tacoRes =

traverson

.follow("tacos")

.follow("recents")

.toObject(tacoType);

Collection tacos = tacoRes.getContent();

Здесь вы переходите по ссылке tacos, а затем оттуда по ссылке recents. Это приведет вас к интересующему вас ресурсу, поэтому вызов toObject() с соответствующей ParameterizedTypeReference даст вам то, что вы хотите. Метод .follow() можно упростить, перечислив имена отношений через запятую:

Resources tacoRes =

traverson

.follow("tacos", "recents")

.toObject(tacoType);

Как видите, Traverson упрощает навигацию по API с поддержкой HATEOAS и использование его ресурсов. Но он не предлагает никаких методов для записи или удаления по этим API. С другой стороны, RestTemplate может писать и удалять ресурсы, но не облегчает навигацию по API.

Если вам нужно не только перемещаться по API, но и обновлять или удалять ресурсы, вам нужно использовать RestTemplate и Traverson вместе. Traverson по-прежнему можно использовать для перехода по ссылке, где будет создан новый ресурс. Затем RestTemplate может быть предоставлена эта ссылка для выполнения POST, PUT, DELETE или любого другого HTTP-запроса.

Например, предположим, что вы хотите добавить новый ингредиент в меню Taco Cloud. Следующий метод addIngredient() объединяет Traverson и RestTemplate для добавления нового ингредиента по API:

private Ingredient addIngredient(Ingredient ingredient) {

String ingredientsUrl = traverson

.follow("ingredients")

.asLink()

.getHref();

return rest.postForObject(ingredientsUrl,

ingredient,

Ingredient.class);

}

После перехода по ссылке ingredients вы запрашиваете саму ссылку, вызывая asLink(). По этой ссылке вы запрашиваете URL ссылки, вызывая getHref(). Имея URL-адрес, у вас есть все, что нужно для вызова postForObject() в экземпляре RestTemplate и сохранения нового ингредиента.

ИТОГ:

Клиенты могут использовать RestTemplate для выполнения HTTP-запросов к REST API.

Traverson позволяет клиентам перемещаться по API с помощью гиперссылок, встроенных в ответы.

Spring in Action Covers Spring 5.0 перевод на русский. Глава 8

8.Отправка сообщений асинхронно

Эта глава охватывает

Асинхронный обмен сообщениями

Messages Отправка сообщений с помощью JMS, RabbitMQ и Kafka

Вытягивание сообщений из broker

Листинер для сообщений

Сейчас 4:55 вечера пятницы. В нескольких минутах от долгожданного отпуска. У вас достаточно времени, чтобы доехать до аэропорта и успеть на самолет. Но прежде чем собраться и отправиться в путь, вы должны быть уверены, что ваш начальник и коллеги знают о состоянии работы, которую вы выполняли, чтобы в понедельник они могли узнать, где вы остановились. К сожалению, некоторые из ваших коллег уже пропустили выходные, а ваш начальник связывается с вами с митинга. Чем ты занимаешься?

Самый практичный способ сообщить о своем статусе и при этом сесть на самолет - это отправить быстрое электронное письмо своему боссу и коллегам с подробным описанием вашего прогресса и обещанием отправить открытку. Вы не знаете, где они находятся и когда они будут читать электронную почту, но вы знаете, что они в конечном итоге вернутся за свои столы и прочитают ее. Тем временем вы едете в аэропорт.

Синхронное взаимодействие, которое мы видели в REST, имеет свое предназначение. Но это не единственный стиль взаимодействия между приложениями, доступный для разработчиков. Асинхронный обмен сообщениями - это способ косвенной отправки сообщений из одного приложения в другое без ожидания ответа. Эта косвенность обеспечивает более слабую связь и большую масштабируемость между взаимодействующими приложениями.

В этой главе вы будете использовать асинхронный обмен сообщениями для отправки заказов с веб-сайта Taco Cloud в отдельное приложение кухни Taco Cloud, где будут готовиться тако. Мы рассмотрим три варианта, которые Spring предлагает для асинхронного обмена сообщениями: Java Message Service (JMS), RabbitMQ и Advanced Message Queuing Protocol (AMQP), Apache Kafka. В дополнение к базовой отправке и получению сообщений, мы рассмотрим поддержку Spring для POJO, управляемых сообщениями: способ получения сообщений, который напоминает EJB-компоненты, управляемые сообщениями (MDB).

8.1 Отправка сообщений с помощью JMS

JMS - это стандарт Java, который определяет общий API для работы с брокерами (broker) сообщений. Впервые представленный в 2001 году, JMS был подходом для асинхронного обмена сообщениями в Java в течение очень долгого времени. До JMS каждый брокер сообщений имел собственный API, что делало код обмена сообщениями приложения менее переносимым между брокерами. Но с JMS все совместимые реализации могут работать через общий интерфейс почти так же, как JDBC предоставил операциям с реляционными базами данных общий интерфейс.

Spring поддерживает JMS через абстракцию на основе шаблонов, известную как JmsTemplate. Используя JmsTemplate, легко отправлять сообщения по очередям и темам со стороны производителя (producer) и получать эти сообщения на стороне потребителя. Spring также поддерживает понятие сообщение-упраляемые POJO: простые объекты Java, которые реагируют на сообщения, поступающие в очередь или тему асинхронно.

Мы собираемся изучить поддержку Spring JMS, включая JmsTemplate и сообщение-упраляемые POJO. Но прежде чем вы сможете отправлять и получать сообщения, вам нужен брокер сообщений, который готов передавать эти сообщения между производителями и потребителями. Давайте начнем наше исследование Spring JMS, настроив брокер сообщений в Spring.

8.1.1 Настройка JMS

Прежде чем вы сможете использовать JMS, вы должны добавить JMS-клиент в сборку вашего проекта. С Spring Boot это очень просто. Все, что вам нужно сделать, это добавить starter зависимость в сборку. Однако сначала вы должны решить, собираетесь ли вы использовать Apache ActiveMQ или более нового брокера Apache ActiveMQ Artemis.

Если вы используете ActiveMQ, вам нужно добавить следующую зависимость в файл pom.xml вашего проекта:

org.springframework.boot

spring-boot-starter-activemq

Если вы решили использовать ActiveMQ Artemis, starter зависимость должна выглядеть следующим образом:

org.springframework.boot

spring-boot-starter-artemis

Artemis - это новое воплощение ActiveMQ следующего поколения, которое делает ActiveMQ устаревшим вариантом. Поэтому для Taco Cloud вы выберете Artemis. Но выбор в конечном итоге мало влияет на то, как вы будете писать код, который отправляет и получает сообщения. Единственным существенным отличием будет то, как вы настраиваете Spring для создания соединений с брокером.

По умолчанию Spring предполагает, что ваш брокер Artemis прослушивает localhost на порту 61616. Это хорошо для целей разработки, но как только вы будете готовы отправить свое приложение в продакшен, вам нужно будет установить несколько свойств, которые сообщат Spring, как получить доступ к брокеру. Свойства, которые вы найдете наиболее полезными, перечислены в таблице 8.1.

Таблица 8.1 Свойства для настройки расположения и учетных данных брокера Artemis

Свойство - Описание

spring.artemis.host - broker host

spring.artemis.port - broker’s port

spring.artemis.user - user использующийся для доступа к broker (опционально)

spring.artemis.password - password использующийся для доступа к broker (опционально)

Например, рассмотрим следующую запись файла application.yml, который может использоваться в параметрах, не относящихся к режиму разработке:

spring:

artemis:

host: artemis.tacocloud.com

port: 61617

user: tacoweb

password: l3tm31n

Это настраивает Spring для создания подключений к брокеру Artemis, который прослушивает artemis.tacocloud.com, порт 61617. Он также устанавливает учетные данные для приложения, которое будет взаимодействовать с этим брокером. Учетные данные не являются обязательными, но они рекомендуются для рабочих развертываний.

Если бы вы использовали ActiveMQ вместо Artemis, вам нужно было бы использовать специфичные для ActiveMQ свойства, перечисленные в таблице 8.2.

Таблица 8.2 Свойства для настройки расположения и учетных данных брокера ActiveMQ

Свойство - Описание

spring.activemq.broker-url - URL broker-а

spring.activemq.user - user использующийся для доступа к broker (опционально)

spring.activemq.password - password использующийся для доступа к broker (опционально)

spring.activemq.in-memory - Стоит ли запускать брокер в памяти (по умолчанию: true)

Обратите внимание, что вместо того, чтобы предлагать отдельные свойства для имени хоста и порта брокера (посредника), адрес брокера ActiveMQ указывается с помощью одного свойства, spring.activemq.broker-url. URL должен быть tcp://URL , как показано в следующем фрагменте YAML:

spring:

activemq:

broker-url: tcp://activemq.tacocloud.com

user: tacoweb

password: l3tm31n

Независимо от того, выбираете ли вы Artemis или ActiveMQ, вам не нужно настраивать эти свойства для разработки, когда брокер работает локально.

Если вы используете ActiveMQ, вам, однако, необходимо установить для свойства spring.activemq.in-memory значение false, чтобы Spring не запускал брокер в памяти. Брокер в памяти может показаться полезным, но он полезен только тогда, когда вы будете использовать сообщения из того же приложения, которое их публикует (что имеет ограниченную полезность).

Вместо использования встроенного брокера вы должны установить и запустить брокера Artemis (или ActiveMQ), прежде чем двигаться дальше. Вместо того, чтобы повторять инструкции по установке здесь, я отсылаю вас к документации брокера:

Artemis—https://activemq.apache.org/artemis/docs/latest/using-server.html

ActiveMQ—http://activemq.apache.org/getting-started.html#GettingStarted-Pre-InstallationRequirements

С JMS-стартером в вашей сборке и брокером, ожидающим пересылки сообщений из одного приложения в другое, вы готовы начать отправку сообщений.

8.1.2 Отправка сообщений с помощью JmsTemplate

С JMS starter зависимостью (Artemis или ActiveMQ) в вашей сборке Spring Boot автоматически настроит JmsTemplate (среди прочего), который вы можете внедрить и использовать для отправки и получения

JmsTemplate является центральным элементом поддержки интеграции Spring JMS. Подобно другим шаблонно-ориентированным компонентам Spring, JmsTemplate устраняет много стандартного кода, который в противном случае потребовался бы для работы с JMS. Без JmsTemplate вам нужно было бы написать код для создания соединения и сеанса с брокером сообщений, а также дополнительный код для обработки любых исключений, которые могут возникнуть в процессе отправки сообщения. JmsTemplate фокусируется на том, что вы действительно хотите сделать: отправить сообщение.

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

// Отправка сырых (raw) сообщений

void send(MessageCreator messageCreator) throws JmsException;

void send(Destination destination, MessageCreator messageCreator) throws JmsException;

void send(String destinationName, MessageCreator messageCreator) throws JmsException;

// Отправка сообщений сконвертированных из объектов

void convertAndSend(Object message) throws JmsException;

void convertAndSend(Destination destination, Object message) throws JmsException;

void convertAndSend(String destinationName, Object message) throws JmsException;

// Отправка сообщений, преобразованных из объектов с post-обработкойvoid convertAndSend(Object message,

MessagePostProcessor postProcessor) throws JmsException;

void convertAndSend(Destination destination, Object message,

MessagePostProcessor postProcessor) throws JmsException;

void convertAndSend(String destinationName, Object message,

MessagePostProcessor postProcessor) throws JmsException;

Как видите, на самом деле есть только два метода, send() и convertAndSend(), каждый из которых переопределен для поддержки различных параметров. И если вы посмотрите поближе, вы заметите, что различные формы convertAndSend() можно разбить на две подкатегории. Чтобы понять, что делают все эти методы, рассмотрим следующий список:

Три метода send() требуют, чтобы MessageCreator создал объект Message.

Три метода convertAndSend() принимают Object и автоматически преобразуют этот Object в Message.

Три метода convertAndSend() автоматически преобразуют Object в Message, но также принимают MessagePostProcessor, позволяющий настроить Message до его отправки.

Кроме того, каждая из этих трех категорий методов состоит из трех переопределяющих методов, которые различаются тем, как указывается место назначения JMS (очередь или тема):

Один метод не принимает параметр пункта назначения и отправляет сообщение в пункт назначения по умолчанию.

Один метод принимает объект Destination, который указывает место назначения для сообщения.

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

Чтобы эти методы работали, рассмотрим JmsOrderMessagingService в следующем листинге, который использует самую основную форму метода send().

Листинг 8.1. Отправка заказа с помощью .send() в пункт назначения по умолчанию

package tacos.messaging;

import javax.jms.JMSException;

import javax.jms.Message;

import javax.jms.Session;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.jms.core.JmsTemplate;

import org.springframework.jms.core.MessageCreator;

import org.springframework.stereotype.Service;

@Service

public class JmsOrderMessagingService implements OrderMessagingService {

private JmsTemplate jms;

@Autowired

public JmsOrderMessagingService(JmsTemplate jms) {

this.jms = jms;

}

@Override

public void sendOrder(Order order) {

jms.send(new MessageCreator() {

@Override

public Message createMessage(Session session)

throws JMSException {

return session.createObjectMessage(order);

}

}

);

}

}

Метод sendOrder() вызывает jms.send(), передавая анонимную внутреннюю реализацию класса MessageCreator. Эта реализация переопределяет createMessage() для создания нового сообщения объекта из заданного объекта Order.

Я не уверен насчет вас, но я думаю, что код в листинге 8.1, хотя и простой, немного неуклюжий. Церемония, связанная с объявлением анонимного внутреннего класса, усложняет простой вызов метода. Признавая, что MessageCreator является функциональным интерфейсом, вы можете немного привести в порядок метод sendOrder() с помощью лямбды:

@Override

public void sendOrder(Order order) {

jms.send(session -> session.createObjectMessage(order));

}

Но обратите внимание, что вызов jms.send() не указывает адресата. Чтобы это работало, вы также должны указать имя получателя по умолчанию в свойстве spring.jms.template.default-destination. Например, вы можете установить свойство в вашем файле application.yml следующим образом:

spring:

jms:

template:

default-destination: tacocloud.order.queue

Во многих случаях использование пункта назначения по умолчанию является самым простым выбором. Это позволяет вам указать имя получателя один раз, позволяя коду относиться только к отправке сообщений, независимо от того, куда они отправляются. Но если вам когда-либо понадобится отправить сообщение в пункт назначения, отличный от пункта назначения по умолчанию, вам нужно будет указать этот пункт назначения в качестве параметра send().

Один из способов сделать это - передать объект Destination в качестве первого параметра send(). Самый простой способ сделать это - объявить bean-объект Destination, а затем внедрить его в bean-компонент, выполняющий обмен сообщениями. Например, следующий bean-компонент объявляет Destination очереди Taco Cloud:

@Bean

public Destination orderQueue() {

return new ActiveMQQueue("tacocloud.order.queue");

}

Важно отметить, что ActiveMQQueue, использованный здесь, на самом деле от Artemis (из пакета org.apache.activemq.artemis.jms.client). Если вы используете ActiveMQ (не Artemis), есть также класс с именем ActiveMQQueue (из пакета org.apache.activemq.command).

Если этот целевой объект внедряется в JmsOrderMessagingService, вы можете использовать его для указания получателя при вызове send():

private Destination orderQueue;

@Autowired

public JmsOrderMessagingService(JmsTemplate jms, Destination orderQueue) {

this.jms = jms;

this.orderQueue = orderQueue;

}

...

@Override

public void sendOrder(Order order) {

jms.send(

orderQueue,

session -> session.createObjectMessage(order));

}

Указание адресата с помощью объекта Destination, подобного этому, дает вам возможность настроить Destination с использованием не только имени пункта назначения. Но на практике вы почти никогда не будете указывать ничего, кроме имени пункта назначения. Часто проще просто отправить имя в качестве первого параметра send():

@Override

public void sendOrder(Order order) {

jms.send(

"tacocloud.order.queue",

session -> session.createObjectMessage(order));

}

Хотя метод send() не особенно сложен в использовании (особенно, когда MessageCreator задается как лямбда-выражение), добавляется небольшая сложность, требующая предоставления MessageCreator. Разве не проще, если вам нужно только указать объект, который должен быть отправлен (и, возможно, пункт назначения)? Это кратко описывает, как работает convertAndSend (). Давайте взглянем.

Конвертация сообщения перед отправкой

Метод convertAndSend() в JmsTemplates упрощает публикацию сообщений, устраняя необходимость предоставлять MessageCreator. Вместо этого вы передаете объект, который должен быть отправлен напрямую, в convertAndSend(), и перед отправкой объект будет преобразован в Message.

Например, следующая переопределение sendOrder() использует convertAndSend() для отправки Order в заданный пункт назначения:

@Override

public void sendOrder(Order order) {

jms.convertAndSend("tacocloud.order.queue", order);

}

Как и метод send(), метод convertAndSend () примет значение Destination или String, чтобы указать адресата, или вы можете вообще не указывать адресата, чтобы отправить сообщение в пункт назначения по умолчанию.

Какую бы реализацию convertAndSend() вы ни выбрали, Order, переданный в convertAndSend(), перед отправкой преобразуется в Message. Под капотом это достигается с помощью реализации MessageConverter, которая выполняет грязную работу по преобразованию объектов в Message -ы.

НАСТРОЙКА КОНВЕРТЕРА СООБЩЕНИЙ

MessageConverter - это Spring-определнный интерфейс, который имеет только два метода для реализации:

public interface MessageConverter {

Message toMessage(Object object, Session session)

throws JMSException, MessageConversionException;

Object fromMessage(Message message)

}

Хотя этот интерфейс достаточно прост для реализации, вам часто не нужно создавать собственную реализацию. Spring уже предлагает несколько реализаций, таких как описанные в таблице 8.3.

Таблица 8.3. Spring message конвертеры для общих задач преобразования (все в пакете org.springframework.jms.support.converter)

Message конвертер - Что он делает

MappingJackson2MessageConverter - Использует Jackson 2 JSON библиотеку конвертирования сообщений в и из JSON

MarshallingMessageConverter - Использует JAXB для конвертирования сообщений в и из XML

MessagingMessageConverter - Конвертирует Message из абстракции в и из Message используя базовый MessageConverter для полезной нагрузки и JmsHeaderMapper для сопоставления заголовков JMS в и из стандартных заголовков сообщений

SimpleMessageConverter - Преобразует String в и из TextMessage, байтовые массивы в и из BytesMessage, Maps в и из MapMessage, и Serializable объекты в и из ObjectMessage.

SimpleMessageConverter является значением по умолчанию, но требует, чтобы отправляемый объект реализовывал Serializable. Это может быть хорошей идеей, но вы можете предпочесть использовать один из других конвертеров сообщений, например MappingJackson2MessageConverter, чтобы избежать этого ограничения.

Чтобы применить другой конвертер сообщений, все, что вам нужно сделать, - это объявить экземпляр выбранного конвертера как bean-компонент. Например, следующее объявление компонента позволит использовать MappingJackson2MessageConverter вместо SimpleMessageConverter:

@Bean

public MappingJackson2MessageConverter messageConverter() {

MappingJackson2MessageConverter messageConverter =

new MappingJackson2MessageConverter();

messageConverter.setTypeIdPropertyName("_typeId");

return messageConverter;

}

Обратите внимание, что вы вызвали setTypeIdPropertyName() для MappingJackson2MessageConverter, прежде чем возвращать его. Это очень важно, так как позволяет получателю знать, в какой тип конвертировать входящее сообщение. По умолчанию он будет содержать полное имя класса конвертируемого типа. Но это несколько негибко, требуя, чтобы получатель также имел тот же тип, с тем же полностью определенным именем класса.

Чтобы обеспечить большую гибкость, вы можете сопоставить синтетическое имя типа с реальным типом, вызвав setTypeIdMappings() в конвертере сообщений. Например, следующее изменение метода bean-объекта конвертера сообщений отображает синтетический идентификатор типа order в класс Order:

@Bean

public MappingJackson2MessageConverter messageConverter() {

MappingJackson2MessageConverter messageConverter =

new MappingJackson2MessageConverter();

messageConverter.setTypeIdPropertyName("_typeId");

Map> typeIdMappings = new HashMap>();

typeIdMappings.put("order", Order.class);

messageConverter.setTypeIdMappings(typeIdMappings);

return messageConverter;

}

Вместо того, чтобы полное имя класса отправлялось в свойстве _typeId сообщения, будет отправлено значение order. В принимающем приложении будет сконфигурирован аналогичный конвертер сообщений, отображающий order в собственном понимании order-а. Эта реализация order может быть в другом пакете, иметь другое имя и даже иметь подмножество свойств Order отправителя.

POST-ОБРАБОТКА СООБЩЕНИЙ

Предположим, что в дополнение к своему прибыльному веб-бизнесу Taco Cloud решила открыть несколько магазинов в строительном секторе. Учитывая, что любой из их ресторанов также может быть центром для веб-бизнеса, им нужен способ сообщить источник заказа на кухню в ресторан. Это позволит кухонному персоналу использовать другой процесс для заказов в магазине, нежели для заказов через Интернет.

Было бы разумно добавить новое свойство источника в объект Order для переноса этой информации, установв его как WEB для заказов, размещенных в Интернете, и STORE для заказов, размещенных в магазинах. Но для этого потребуется изменить как класс Order на веб-сайте, так и класс Order для кухонного приложения, когда на самом деле это информация, которая требуется только для составителей тако.

Более простым решением было бы добавить пользовательский заголовок к сообщению для установки источника заказа. Если бы вы использовали метод send() для отправки тако-заказов, это можно легко сделать, вызвав setStringProperty() для объекта Message:

jms.send("tacocloud.order.queue",

session -> {

Message message = session.createObjectMessage(order);

message.setStringProperty("X_ORDER_SOURCE", "WEB");

});

Проблема в том, что вы не используете send(). При выборе использования convertAndSend() объект Message создается в обертке, и у вас нет к нему доступа.

К счастью, есть способ настроить Message, созданное в обертке, до его отправки. Передав MessagePostProcessor в качестве последнего параметра для convertAndSend(), вы можете делать с сообщением все, что захотите, после его создания. Следующий код по-прежнему использует convertAndSend(), но использует MessagePostProcessor для добавления заголовка X_ORDER_SOURCE до отправки сообщения:

jms.convertAndSend("tacocloud.order.queue", order, new MessagePostProcessor() {

Загрузка...