«1. Обзор

В предыдущих частях серии мы видели, как мы можем использовать Spring Remoting и связанные с ним технологии для включения синхронных удаленных вызовов процедур поверх HTTP-канала между сервером и клиентом.

В этой статье мы рассмотрим Spring Remoting поверх AMQP, который позволяет выполнять синхронный RPC, используя среду, которая по своей природе асинхронна.

2. Установка RabbitMQ

Существуют различные системы обмена сообщениями, совместимые с AMQP, которые мы могли бы использовать, и мы выбираем RabbitMQ, потому что это проверенная платформа и она полностью поддерживается в Spring — оба продукта управляются одним и тем же компания (Опорная).

Если вы не знакомы с AMQP или RabbitMQ, вы можете прочитать наше краткое введение.

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

3. Зависимости Maven

Мы собираемся настроить серверные и клиентские приложения Spring Boot, чтобы показать, как работает AMQP Remoting. Как это часто бывает с Spring Boot, нам просто нужно выбрать и импортировать правильные начальные зависимости, как описано здесь:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Мы явно исключили spring-boot-starter-tomcat, потому что нам не нужны встроенные HTTP-сервер — вместо этого он будет запускаться автоматически, если мы позволим Maven импортировать все транзитивные зависимости в пути к классам.

4. Серверное приложение

4.1. Предоставление службы

Как мы показали в предыдущих статьях, мы предоставим CabBookingService, который имитирует вероятную удаленную службу.

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

@Bean 
CabBookingService bookingService() {
    return new CabBookingServiceImpl();
}

Давайте определим очередь, из которой сервер будет извлекать вызовы. В этом случае достаточно указать для него имя, предоставив его в конструкторе:

@Bean 
Queue queue() {
    return new Queue("remotingQueue");
}

Как мы уже знаем из предыдущих статей, одной из основных концепций Spring Remoting является Service Exporter, компонент который фактически собирает запросы на вызов из некоторого источника — в данном случае из очереди RabbitMQ — и вызывает желаемый метод в реализации службы.

В этом случае мы определяем AmqpInvokerServiceExporter, который, как вы видите, нуждается в ссылке на AmqpTemplate. Класс AmqpTemplate предоставляется Spring Framework и упрощает работу с AMQP-совместимыми системами обмена сообщениями так же, как JdbcTemplate упрощает работу с базами данных.

Мы не будем явно определять такой bean-компонент AmqpTemplate, потому что он будет автоматически предоставлен модулем автоконфигурации Spring Boot:

@Bean AmqpInvokerServiceExporter exporter(
  CabBookingService implementation, AmqpTemplate template) {
 
    AmqpInvokerServiceExporter exporter = new AmqpInvokerServiceExporter();
    exporter.setServiceInterface(CabBookingService.class);
    exporter.setService(implementation);
    exporter.setAmqpTemplate(template);
    return exporter;
}

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

Затем мы подключим этот контейнер к экспортеру сервисов, созданному на предыдущем шаге, чтобы позволить ему получать сообщения из очереди. Здесь ConnectionFactory автоматически предоставляется Spring Boot так же, как AmqpTemplate:

@Bean 
SimpleMessageListenerContainer listener(
  ConnectionFactory facotry, 
  AmqpInvokerServiceExporter exporter, 
  Queue queue) {
 
    SimpleMessageListenerContainer container
     = new SimpleMessageListenerContainer(facotry);
    container.setMessageListener(exporter);
    container.setQueueNames(queue.getName());
    return container;
}

4.2. Конфигурация

Давайте не забудем настроить файл application.properties, чтобы позволить Spring Boot настроить основные объекты. Очевидно, что значения параметров также будут зависеть от способа установки RabbitMQ.

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

spring.rabbitmq.dynamic=true
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=localhost

5. Клиентское приложение

5.1. Вызов удаленной службы

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

@Bean 
Queue queue() {
    return new Queue("remotingQueue");
}

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

@Bean 
Exchange directExchange(Queue someQueue) {
    DirectExchange exchange = new DirectExchange("remoting.exchange");
    BindingBuilder
      .bind(someQueue)
      .to(exchange)
      .with("remoting.binding");
    return exchange;
}

«

«Хорошее введение в основные концепции RabbitMQ, таких как обмены и привязки, доступно здесь.

@Bean RabbitTemplate amqpTemplate(ConnectionFactory factory) {
    RabbitTemplate template = new RabbitTemplate(factory);
    template.setRoutingKey("remoting.binding");
    template.setExchange("remoting.exchange");
    return template;
}

Поскольку Spring Boot не настраивает AmqpTemplate автоматически, мы должны настроить его самостоятельно, указав ключ маршрутизации. При этом нам нужно дважды проверить, что ключ маршрутизации и обмен совпадают с теми, которые использовались для определения Exchange на предыдущем шаге:

@Bean AmqpProxyFactoryBean amqpFactoryBean(AmqpTemplate amqpTemplate) {
    AmqpProxyFactoryBean factoryBean = new AmqpProxyFactoryBean();
    factoryBean.setServiceInterface(CabBookingService.class);
    factoryBean.setAmqpTemplate(amqpTemplate);
    return factoryBean;
}

Затем, как мы делали с другими реализациями Spring Remoting, мы определяем FactoryBean, который будет создавать локальные прокси-серверы удаленно предоставляемой службы. Здесь нет ничего особенного, нам просто нужно предоставить интерфейс удаленной службы:

CabBookingService service = context.getBean(CabBookingService.class);
out.println(service.bookRide("13 Seagate Blvd, Key Largo, FL 33037"));

Теперь мы можем использовать удаленную службу, как если бы она была объявлена ​​как локальный компонент:

5.2. Настройка

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

5.3. Запустите пример

Этого должно быть достаточно, чтобы продемонстрировать удаленный вызов через RabbitMQ. Затем запустим RabbitMQ, серверное приложение и клиентское приложение, которое вызывает удаленную службу.

За кулисами происходит то, что AmqpProxyFactoryBean создаст прокси, реализующий CabBookingService.

Когда метод вызывается на этом прокси, он ставит сообщение в очередь RabbitMQ, указывая в нем все параметры вызова и имя очереди, которая будет использоваться для отправки обратно результата.

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

AmqpProxyFactoryBean получает обратно результат и, наконец, возвращает значение, которое изначально было создано на стороне сервера.

6. Заключение

В этой статье мы увидели, как мы можем использовать Spring Remoting для предоставления RPC поверх системы обмена сообщениями.

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