Spring AMQP в реактивных приложениях
1. обзор
В этом руководстве показано, как создать простое реактивное приложение Spring Boot, которое интегрируется с сервером обмена сообщениями RabbitMQ, популярной реализацией стандарта обмена сообщениями AMQP.
Мы рассмотрим сценарии «точка-точка» и «публикация-подписка», используя распределенную настройку, которая подчеркивает различия между обоими шаблонами.
Обратите внимание, что мы предполагаем базовые знания AMQP, RabbitMQ и Spring Boot, в частности, такие ключевые понятия, как обмены, очереди, темы и т. Д. Более подробную информацию об этих концепциях можно найти по ссылкам ниже:
2. Установка сервера RabbitMQ
Хотя мы могли бы настроить локальный RabbitMQ локально, на практике мы, скорее всего, будем использовать выделенную установку с дополнительными функциями, такими как высокая доступность, мониторинг, безопасность и т. Д.
Чтобы смоделировать такую среду на нашей машине разработки, мы будем использовать Docker для создания сервера, который будет использовать наше приложение.
Следующая команда запустит автономный сервер RabbitMQ:
$ docker run -d --name rabbitmq -p 5672:5672 rabbitmq:3
Мы не объявляем постоянный том, поэтому непрочитанные сообщения будут потеряны между перезапусками. Служба будет доступна через порт 5672 на хосте.
Мы можем проверить журналы сервера с помощью командыdocker logs, которая должна выдать примерно такой результат:
$ docker logs rabbitmq
2018-06-09 13:42:29.718 [info] <0.33.0>
Application lager started on node [email protected]
// ... some lines omitted
2018-06-09 13:42:33.491 [info] <0.226.0>
Starting RabbitMQ 3.7.5 on Erlang 20.3.5
Copyright (C) 2007-2018 Pivotal Software, Inc.
Licensed under the MPL. See http://www.rabbitmq.com/
## ##
## ## RabbitMQ 3.7.5. Copyright (C) 2007-2018 Pivotal Software, Inc.
########## Licensed under the MPL. See http://www.rabbitmq.com/
###### ##
########## Logs:
Starting broker...
2018-06-09 13:42:33.494 [info] <0.226.0>
node : [email protected]
home dir : /var/lib/rabbitmq
config file(s) : /etc/rabbitmq/rabbitmq.conf
cookie hash : CY9rzUYh03PK3k6DJie09g==
log(s) :
database dir : /var/lib/rabbitmq/mnesia/[email protected]
// ... more log lines
Поскольку образ включает утилитуrabbitmqctl, мы можем использовать ее для выполнения административных задач в контексте нашего работающего образа.
Например, мы можем получить информацию о состоянии сервера с помощью следующей команды:
$ docker exec rabbitmq rabbitmqctl status
Status of node [email protected] ...
[{pid,299},
{running_applications,
[{rabbit,"RabbitMQ","3.7.5"},
{rabbit_common,
"Modules shared by rabbitmq-server and rabbitmq-erlang-client",
"3.7.5"},
// ... other info omitted for brevity
Другие полезные команды включают в себя:
-
list_exchanges: перечислить все объявленные биржи
-
list_queues: список всех объявленных очередей, включая количество непрочитанных сообщений
-
list_bindings: перечислить все определения привязок между обменами и очередями, включая ключи маршрутизации
3. Spring AMQP Project Setup
После того, как наш сервер RabbitMQ будет запущен и запущен, мы можем приступить к созданию проекта Spring. Этот пример проекта позволит любому клиенту REST публиковать и / или получать сообщения на сервер обмена сообщениями, используя модуль Spring AMQP и соответствующий стартовый модуль Spring Boot для связи с ним.
Основные зависимости, которые нам нужно добавить в наш файл проектаpom.xml:
org.springframework.boot
spring-boot-starter-amqp
2.0.3.RELEASE
org.springframework.boot
spring-boot-starter-webflux
2.0.2.RELEASE
spring-boot-starter-amqp содержит все, что связано с AMQP, тогда какspring-boot-starter-webflux - это основная зависимость, используемая для реализации нашего реактивного сервера REST.
4. Сценарий 1: обмен сообщениями "точка-точка"
В этом первом сценарии мы будем использовать Direct Exchange, который является логическим элементом брокера, который получает сообщения от клиентов.
A Direct Exchange will route all incoming messages to one – and only one – queue, откуда он будет доступен для потребления клиентами. Несколько клиентов могут подписаться на одну и ту же очередь, но только один получит данное сообщение.
4.1. Настройка Exchange и очередей
В нашем сценарии мы используем объектDestinationInfo, который инкапсулирует имя обмена и ключ маршрутизации. Карта с указанием имени получателя будет использоваться для хранения всех доступных получателей.
За эту начальную настройку отвечает следующий метод@PostConstruct :
@Autowired
private AmqpAdmin amqpAdmin;
@Autowired
private DestinationsConfig destinationsConfig;
@PostConstruct
public void setupQueueDestinations() {
destinationsConfig.getQueues()
.forEach((key, destination) -> {
Exchange ex = ExchangeBuilder.directExchange(
destination.getExchange())
.durable(true)
.build();
amqpAdmin.declareExchange(ex);
Queue q = QueueBuilder.durable(
destination.getRoutingKey())
.build();
amqpAdmin.declareQueue(q);
Binding b = BindingBuilder.bind(q)
.to(ex)
.with(destination.getRoutingKey())
.noargs();
amqpAdmin.declareBinding(b);
});
}
Этот метод использует объектadminAmqp , созданный Spring, для объявления обменов, очередей и связывания их вместе с помощью заданного ключа маршрутизации.
Все назначения происходят изDestinationsConfig bean, который является классом@ConfigurationProperties, используемым в нашем примере.
Этот класс имеет свойство, которое заполняется объектамиDestinationInfo, созданными из сопоставлений, считанных из файла конфигурацииapplication.yml.
4.2. Конечная точка продюсера
Производители будут отправлять сообщения, отправляяHTTP POST в расположение/queue/{name}.
Это реактивная конечная точка, поэтому мы используемMono для возврата простого подтверждения:
@SpringBootApplication
@EnableConfigurationProperties(DestinationsConfig.class)
@RestController
public class SpringWebfluxAmqpApplication {
// ... other members omitted
@Autowired
private AmqpTemplate amqpTemplate;
@PostMapping(value = "/queue/{name}")
public Mono> sendMessageToQueue(
@PathVariable String name, @RequestBody String payload) {
DestinationInfo d = destinationsConfig
.getQueues().get(name);
if (d == null) {
return Mono.just(
ResponseEntity.notFound().build());
}
return Mono.fromCallable(() -> {
amqpTemplate.convertAndSend(
d.getExchange(),
d.getRoutingKey(),
payload);
return ResponseEntity.accepted().build();
});
}
Сначала мы проверяем, соответствует ли параметр имени допустимому месту назначения, и если да, мы используем автоматически подключенный экземплярamqpTemplate для фактической отправки полезной нагрузки - простого сообщенияString - в RabbitMQ.
4.3. MessageListenerContainer Завод
Чтобы получать сообщения асинхронно, Spring AMQP использует абстрактный классMessageContainerListener, который передает информационный поток из очередей AMQP и слушателей, предоставляемых приложением.
Поскольку нам нужна конкретная реализация этого класса для присоединения наших слушателей сообщений, мы определяем фабрику, которая изолирует код контроллера от его фактической реализации.
В нашем случае фабричный метод возвращает новыйSimpleMessageContainerListener каждый раз, когда мы вызываем его методcreateMessageListenerContainer:
@Component
public class MessageListenerContainerFactory {
@Autowired
private ConnectionFactory connectionFactory;
public MessageListenerContainerFactory() {}
public MessageListenerContainer createMessageListenerContainer(String queueName) {
SimpleMessageListenerContainer mlc = new SimpleMessageListenerContainer(connectionFactory);
mlc.addQueueNames(queueName);
return mlc;
}
}
4.4. Конечная точка потребителя
Потребители будут обращаться к тому же адресу конечной точки, который используется производителями (/queue/{name}) для получения сообщений.
Эта конечная точка возвращает событияFlux of, где каждое событие соответствует полученному сообщению:
@Autowired
private MessageListenerContainerFactory messageListenerContainerFactory;
@GetMapping(
value = "/queue/{name}",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux> receiveMessagesFromQueue(@PathVariable String name) {
DestinationInfo d = destinationsConfig
.getQueues()
.get(name);
if (d == null) {
return Flux.just(ResponseEntity.notFound()
.build());
}
MessageListenerContainer mlc = messageListenerContainerFactory
.createMessageListenerContainer(d.getRoutingKey());
Flux f = Flux. create(emitter -> {
mlc.setupMessageListener((MessageListener) m -> {
String payload = new String(m.getBody());
emitter.next(payload);
});
emitter.onRequest(v -> {
mlc.start();
});
emitter.onDispose(() -> {
mlc.stop();
});
});
return Flux.interval(Duration.ofSeconds(5))
.map(v -> "No news is good news")
.mergeWith(f);
}
После первоначальной проверки имени назначения конечная точка потребителя создаетMessageListenerContainer, используяMessageListenerContainerFactory и имя очереди, восстановленное из нашего реестра.
Когда у нас естьMessageListenerContainer, мы создаем сообщениеFlux, используя один из его методов построенияcreate().
В нашем конкретном случае мы используем тот, который принимает лямбду с аргументомFluxSink, который мы затем используем для соединения асинхронного API на основе прослушивателя Spring AMQP с нашим реактивным приложением.
Мы также присоединяем две дополнительные лямбды к обратным вызовам эмиттераonRequest() иonDispose(), чтобы наше сканированиеMessageListenerContainer выделяло / высвобождало свои внутренние ресурсы в соответствии с жизненным цикломFlux.
Наконец, мы объединяем полученныйFlux w с другим, созданным с помощьюinterval(), w, что создает новое событие каждые пять секунд. Those dummy messages play an important function in our case: без них мы могли бы обнаруживать отключение клиента только после получения сообщения и невозможности его отправки, что может занять много времени в зависимости от вашего конкретного варианта использования.
4.5. тестирование
Настроив конечные точки как для потребителя, так и для издателя, мы можем провести некоторые тесты с нашим примером приложения.
Нам нужно определить детали подключения к серверу RabbitMQ и по крайней мере одно место назначения на нашемapplication.yml, которое должно выглядеть так:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
destinations:
queues:
NYSE:
exchange: nyse
routing-key: NYSE
Свойстваspring.rabbitmq.* определяют основные свойства, необходимые для подключения к нашему серверу RabbitMQ, работающему в локальном контейнере Docker. Обратите внимание, что показанный выше IP-адрес является лишь примером и может отличаться в конкретной настройке.
Очереди определяются с использованиемdestinations.queues.<name>.*, где<name> используется в качестве имени назначения. Здесь мы объявили один пункт назначения с именем «NYSE», который будет отправлять сообщения в обмен «nyse» на RabbitMQ с ключом маршрутизации «NYSE».
Как только мы запустим сервер из командной строки или из нашей IDE, мы сможем начать отправку и получение сообщений. Мы будем использовать утилитуcurl, распространенную утилиту, доступную как для ОС Windows, Mac и Linux.
В следующем листинге показано, как отправить сообщение в пункт назначения и ожидаемый ответ от сервера:
$ curl -v -d "Test message" http://localhost:8080/queue/NYSE
* timeout on name lookup is not supported
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /queue/NYSE HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
> Content-Length: 12
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 12 out of 12 bytes
< HTTP/1.1 202 Accepted
< content-length: 0
<
* Connection #0 to host localhost left intact
После выполнения этой команды мы можем проверить, что сообщение было получено RabbitMQ и готово к использованию, выполнив следующую команду:
$ docker exec rabbitmq rabbitmqctl list_queues
Timeout: 60.0 seconds ...
Listing queues for vhost / ...
NYSE 1
Теперь мы можем читать сообщения с помощью curl с помощью следующей команды:
$ curl -v http://localhost:8080/queue/NYSE
* timeout on name lookup is not supported
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /queue/NYSE HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Content-Type: text/event-stream;charset=UTF-8
<
data:Test message
data:No news is good news...
... same message repeating every 5 secs
Как мы видим, сначала мы получаем ранее сохраненное сообщение, а затем начинаем получать наше фиктивное сообщение каждые 5 секунд.
Если мы снова запустим команду для вывода списка очередей, мы увидим, что сообщения не сохраняются:
$ docker exec rabbitmq rabbitmqctl list_queues
Timeout: 60.0 seconds ...
Listing queues for vhost / ...
NYSE 0
5. Сценарий 2: публикация-подписка
Другим распространенным сценарием для приложений обмена сообщениями является шаблон «публикация-подписка», в котором одно сообщение должно отправляться нескольким потребителям.
RabbitMQ предлагает два типа обменов, которые поддерживают такие виды приложений: разветвление и тема.
Основное различие между этими двумя типами состоит в том, что последний позволяет нам фильтровать, какие сообщения получать на основе шаблона ключа маршрутизации (например, «Alarm.mailserver. *») Предоставляется во время регистрации, тогда как первый просто реплицирует входящие сообщения во все связанные очереди.
RabbitMQ также поддерживает обмен заголовками, что обеспечивает более сложную фильтрацию сообщений, но его использование выходит за рамки данной статьи.
5.1. Настройка направлений
Мы определяем назначения Pub / Sub во время запуска с другим методом@PostConstruct , как мы это делали в сценарии точка-точка.
Единственная разница в том, что мы создаем толькоExchanges, но неQueues - они будут созданы по запросу и привязаны кExchange позже, так как нам нужен эксклюзивныйQueue для каждого клиента:
@PostConstruct
public void setupTopicDestinations(
destinationsConfig.getTopics()
.forEach((key, destination) -> {
Exchange ex = ExchangeBuilder
.topicExchange(destination.getExchange())
.durable(true)
.build();
amqpAdmin.declareExchange(ex);
});
}
5.2. Конечная точка издателя
Клиенты будут использовать конечную точку издателя, доступную в местоположении/topic/{name}, для публикации сообщений, которые будут отправлены всем подключенным клиентам.
Как и в предыдущем сценарии, мы используем@PostMapping, который возвращаетMono со статусом после отправки сообщения:
@PostMapping(value = "/topic/{name}")
public Mono> sendMessageToTopic(
@PathVariable String name, @RequestBody String payload) {
DestinationInfo d = destinationsConfig
.getTopics()
.get(name);
if (d == null) {
return Mono.just(ResponseEntity.notFound().build());
}
return Mono.fromCallable(() -> {
amqpTemplate.convertAndSend(
d.getExchange(), d.getRoutingKey(),payload);
return ResponseEntity.accepted().build();
});
}
5.3. Конечная точка подписчика
Наша конечная точка подписчика будет расположена в/topic/{name}, создаваяFlux сообщений для подключенных клиентов.
Эти сообщения включают в себя как полученные, так и фиктивные сообщения, генерируемые каждые 5 секунд:
@GetMapping(
value = "/topic/{name}",
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux> receiveMessagesFromTopic(@PathVariable String name) {
DestinationInfo d = destinationsConfig.getTopics()
.get(name);
if (d == null) {
return Flux.just(ResponseEntity.notFound()
.build());
}
Queue topicQueue = createTopicQueue(d);
String qname = topicQueue.getName();
MessageListenerContainer mlc = messageListenerContainerFactory.createMessageListenerContainer(qname);
Flux f = Flux. create(emitter -> {
mlc.setupMessageListener((MessageListener) m -> {
String payload = new String(m.getBody());
emitter.next(payload);
});
emitter.onRequest(v -> {
mlc.start();
});
emitter.onDispose(() -> {
amqpAdmin.deleteQueue(qname);
mlc.stop();
});
});
return Flux.interval(Duration.ofSeconds(5))
.map(v -> "No news is good news")
.mergeWith(f);
}
Этот код в основном такой же, как мы видели в предыдущем случае, только со следующими отличиями: сначала мы создаем новыйQueue для каждого нового подписчика.
Мы делаем это с помощью вызова методаcreateTopicQueue(), который использует информацию из экземпляраDestinationInfo для создания эксклюзивной, непродолжительной очереди, которую мы затем связываем сExchange, используя настроенный ключ маршрутизации:
private Queue createTopicQueue(DestinationInfo destination) {
Exchange ex = ExchangeBuilder
.topicExchange(destination.getExchange())
.durable(true)
.build();
amqpAdmin.declareExchange(ex);
Queue q = QueueBuilder
.nonDurable()
.build();
amqpAdmin.declareQueue(q);
Binding b = BindingBuilder.bind(q)
.to(ex)
.with(destination.getRoutingKey())
.noargs();
amqpAdmin.declareBinding(b);
return q;
}
Обратите внимание, что, несмотря на то, что мы снова объявляемExchange, RabbitMQ не создаст новый, поскольку мы уже объявили его во время запуска.
Второе отличие заключается в лямбде, которую мы передаем методуonDispose(), который на этот раз также удалитQueue при отключении подписчика.
5.3. тестирование
Чтобы протестировать сценарий Pub-Sub, мы должны сначала определить место назначения темы в outapplication.yml следующим образом:
destinations:
## ... queue destinations omitted
topics:
weather:
exchange: alerts
routing-key: WEATHER
Здесь мы определили конечную точку темы, которая будет доступна в местоположении/topic/weather. Эта конечная точка будет использоваться для отправки сообщений в обмен «оповещениями» на RabbitMQ с ключом маршрутизации «ПОГОДА».
После запуска сервера мы можем проверить, что обмен создан с помощью командыrabbitmqctl:
$ docker exec docker_rabbitmq_1 rabbitmqctl list_exchanges
Listing exchanges for vhost / ...
amq.topic topic
amq.fanout fanout
amq.match headers
amq.headers headers
direct
amq.rabbitmq.trace topic
amq.direct direct
alerts topic
Теперь, если мы введем командуlist_bindings, мы увидим, что нет очередей, связанных с обменом «предупреждениями»:
$ docker exec rabbitmq rabbitmqctl list_bindings
Listing bindings for vhost /...
exchange NYSE queue NYSE []
nyse exchange NYSE queue NYSE []
Давайте запустим пару подписчиков, которые подпишутся на наш пункт назначения, открыв две оболочки команд и выполнив следующую команду для каждого:
$ curl -v http://localhost:8080/topic/weather
* timeout on name lookup is not supported
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /topic/weather HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
>
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< Content-Type: text/event-stream;charset=UTF-8
<
data:No news is good news...
# ... same message repeating indefinitely
Наконец, мы снова используем curl для отправки некоторых уведомлений нашим подписчикам:
$ curl -v -d "Hurricane approaching!" http://localhost:8080/topic/weather
* timeout on name lookup is not supported
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /topic/weather HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
> Content-Length: 22
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 22 out of 22 bytes
< HTTP/1.1 202 Accepted
< content-length: 0
<
* Connection #0 to host localhost left intact
После того, как мы отправим сообщение, мы почти сразу увидим сообщение «Ураган приближается!» На оболочке каждого подписчика.
Если мы сейчас проверим доступные привязки, то увидим, что у нас есть одна очередь для каждого подписчика:
$ docker exec rabbitmq rabbitmqctl list_bindings
Listing bindings for vhost /...
exchange IBOV queue IBOV []
exchange NYSE queue NYSE []
exchange spring.gen-i0m0pbyKQMqpz2_KFZCd0g
queue spring.gen-i0m0pbyKQMqpz2_KFZCd0g []
exchange spring.gen-wCHALTsIS1q11PQbARJ7eQ
queue spring.gen-wCHALTsIS1q11PQbARJ7eQ []
alerts exchange spring.gen-i0m0pbyKQMqpz2_KFZCd0g
queue WEATHER []
alerts exchange spring.gen-wCHALTsIS1q11PQbARJ7eQ
queue WEATHER []
ibov exchange IBOV queue IBOV []
nyse exchange NYSE queue NYSE []
quotes exchange NYSE queue NYSE []
Как только мы нажмем Ctrl-C на оболочке подписчика, наш шлюз в конечном итоге обнаружит, что клиент отключился, и удалит эти привязки.
6. Заключение
В этой статье мы продемонстрировали, как создать простое реактивное приложение, которое взаимодействует с сервером RabbitMQ с помощью модуляspring-amqp.
С помощью всего лишь нескольких строк кода мы смогли создать функциональный шлюз HTTP-AMQP, который поддерживает шаблоны интеграции «точка-точка» и «публикация-подписка», которые мы можем легко расширить, добавив дополнительные функции, такие как безопасность с помощью добавление стандартных функций Spring.
Код, показанный в этой статье, доступенover on Github.