将Spring AMQP消费者/生产者服务迁移到Spring Stream源的问题

时间:2019-08-30 11:14:15

标签: spring-boot spring-amqp spring-cloud-stream

我正在迁移一个Spring Boot微服务,该服务使用服务器A上3个RabbitMQ队列中的数据,将其保存到Redis中,最后将消息生成到服务器B上不同RabbitMQ的交换中,以便其他微服务可以使用这些消息。该流程工作正常,但我想使用RabbitMQ绑定器将其迁移到Spring Cloud Stream。所有Spring AMQP配置都是在属性文件中自定义的,没有spring属性用于创建连接,队列,绑定等...

我的第一个想法是在Spring Cloud Stream中设置两个绑定,一个连接到服务器A(消费者),另一个连接到服务器B(生产者),然后将现有代码迁移到Processor,但是我放弃了它,因为看起来如果使用了多个绑定器,则无法设置连接名称,并且我需要添加多个绑定以从服务器A的队列中使用,并且bindingRoutingKey属性不支持值列表(我知道可以按照here的说明进行编程)

因此,我决定只重构与生产者相关的部分代码,以便在RabbitMQ上使用Spring Cloud Stream,因此同一微服务应通过Spring AMQP从服务器A消费(原始代码),并应通过Spring Cloud Stream生成到服务器B

我发现的第一个问题是Spring Cloud Stream中的NonUniqueBeanDefinitionException,因为org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory bean被 handlerMethodFactory integrationMessageHandlerMethodFactory 定义了两次。 i>名称。

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory' available: expected single matching bean but found 2: handlerMethodFactory,integrationMessageHandlerMethodFactory
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1144)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:411)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:344)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:337)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1123)
    at org.springframework.cloud.stream.binding.StreamListenerAnnotationBeanPostProcessor.injectAndPostProcessDependencies(StreamListenerAnnotationBeanPostProcessor.java:317)
    at org.springframework.cloud.stream.binding.StreamListenerAnnotationBeanPostProcessor.afterSingletonsInstantiated(StreamListenerAnnotationBeanPostProcessor.java:113)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:862)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:743)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:390)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:312)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1214)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1203)

似乎前一个bean是由Spring AMQP创建的,而后者是由Spring Cloud Stream创建的,所以我创建了自己的主bean:

@Bean
@Primary
public MessageHandlerMethodFactory messageHandlerMethodFactory() {
    return new DefaultMessageHandlerMethodFactory();
}

现在应用程序可以启动,但是输出通道是由服务器A中的Spring Cloud Stream创建的,而不是服务器B中的。似乎Cloud Cloud Stream配置使用的是Spring AMQP创建的连接,而不是使用其自己的配置。

Spring AMQP的配置是这样的:

@Bean
public SimpleRabbitListenerContainerFactory priceRabbitListenerContainerFactory(
 ConnectionFactory consumerConnectionFactory) {
  return
    getSimpleRabbitListenerContainerFactory(
      consumerConnectionFactory,
      rabbitProperties.getConsumer().getListeners().get(LISTENER_A));
}

@Bean
public SimpleRabbitListenerContainerFactory maxbetRabbitListenerContainerFactory(
  ConnectionFactory consumerConnectionFactory) {
    return
      getSimpleRabbitListenerContainerFactory(
        consumerConnectionFactory,
       rabbitProperties.getConsumer().getListeners().get(LISTENER_B));
}

@Bean
public ConnectionFactory consumerConnectionFactory() throws Exception {
  return
    new CachingConnectionFactory(
      getRabbitConnectionFactoryBean(
        rabbitProperties.getConsumer()
      ).getObject()
    );
}

private SimpleRabbitListenerContainerFactory getSimpleRabbitListenerContainerFactory(
  ConnectionFactory connectionFactory,
  RabbitProperties.ListenerProperties listenerProperties) {
    //return a SimpleRabbitListenerContainerFactory set up from external properties
}

/**
 * Create the AMQ Admin.
 */
@Bean
public AmqpAdmin consumerAmqpAdmin(ConnectionFactory consumerConnectionFactory) {
  return new RabbitAdmin(consumerConnectionFactory);
}

/**
 * Create the map of available queues and declare them in the admin.
 */
@Bean
public Map<String, Queue> queues(AmqpAdmin consumerAmqpAdmin) {
  return
    rabbitProperties.getConsumer().getListeners().entrySet().stream()
      .map(listenerEntry -> {
        Queue queue =
          QueueBuilder
            .nonDurable(listenerEntry.getValue().getQueueName())
            .autoDelete()
            .build();

          consumerAmqpAdmin.declareQueue(queue);

          return new AbstractMap.SimpleEntry<>(listenerEntry.getKey(), queue);
      }).collect(
        Collectors.toMap(
          AbstractMap.SimpleEntry::getKey,
          AbstractMap.SimpleEntry::getValue
        )
      );
}

/**
 * Create the map of available exchanges and declare them in the admin.
 */
@Bean
public Map<String, TopicExchange> exchanges(AmqpAdmin consumerAmqpAdmin) {
  return
    rabbitProperties.getConsumer().getListeners().entrySet().stream()
      .map(listenerEntry -> {
        TopicExchange exchange =
          new TopicExchange(listenerEntry.getValue().getExchangeName());

        consumerAmqpAdmin.declareExchange(exchange);

        return new AbstractMap.SimpleEntry<>(listenerEntry.getKey(), exchange);
      }).collect(
        Collectors.toMap(
          AbstractMap.SimpleEntry::getKey,
          AbstractMap.SimpleEntry::getValue
        )
      );
}

/**
 * Create the list of bindings and declare them in the admin.
 */
@Bean
public List<Binding> bindings(Map<String, Queue> queues, Map<String, TopicExchange> exchanges, AmqpAdmin consumerAmqpAdmin) {
  return
    rabbitProperties.getConsumer().getListeners().keySet().stream()
      .map(listenerName -> {
        Queue queue = queues.get(listenerName);
        TopicExchange exchange = exchanges.get(listenerName);

        return
          rabbitProperties.getConsumer().getListeners().get(listenerName).getKeys().stream()
            .map(bindingKey -> {
              Binding binding = BindingBuilder.bind(queue).to(exchange).with(bindingKey);

              consumerAmqpAdmin.declareBinding(binding);

              return binding;
            }).collect(Collectors.toList());
      }).flatMap(Collection::stream)
      .collect(Collectors.toList());
}

消息侦听器是:

@RabbitListener(
  queues="${consumer.listeners.LISTENER_A.queue-name}",
  containerFactory = "priceRabbitListenerContainerFactory"
)
public void handleMessage(Message rawMessage, org.springframework.messaging.Message<ModelPayload> message) {
   // call a service to process the message payload
}

@RabbitListener(
  queues="${consumer.listeners.LISTENER_B.queue-name}",
  containerFactory = "maxbetRabbitListenerContainerFactory"
)
public void handleMessage(Message rawMessage, org.springframework.messaging.Message<ModelPayload> message) {
  // call a service to process the message payload
}

属性:

#
# Server A config (Spring AMQP)
#
consumer.host=server-a
consumer.username=
consumer.password=
consumer.port=5671
consumer.ssl.enabled=true
consumer.ssl.algorithm=TLSv1.2
consumer.ssl.validate-server-certificate=false
consumer.connection-name=local:microservice-1
consumer.thread-factory.thread-group-name=server-a-consumer
consumer.thread-factory.thread-name-prefix=server-a-consumer-
# LISTENER_A configuration
consumer.listeners.LISTENER_A.queue-name=local.listenerA
consumer.listeners.LISTENER_A.exchange-name=exchangeA
consumer.listeners.LISTENER_A.keys[0]=*.1.*.*
consumer.listeners.LISTENER_A.keys[1]=*.3.*.*
consumer.listeners.LISTENER_A.keys[2]=*.6.*.*
consumer.listeners.LISTENER_A.keys[3]=*.8.*.*
consumer.listeners.LISTENER_A.keys[4]=*.9.*.*
consumer.listeners.LISTENER_A.initial-concurrency=5
consumer.listeners.LISTENER_A.maximum-concurrency=20
consumer.listeners.LISTENER_A.thread-name-prefix=listenerA-consumer-
# LISTENER_B configuration
consumer.listeners.LISTENER_B.queue-name=local.listenerB
consumer.listeners.LISTENER_B.exchange-name=exchangeB
consumer.listeners.LISTENER_B.keys[0]=*.1.*
consumer.listeners.LISTENER_B.keys[1]=*.3.*
consumer.listeners.LISTENER_B.keys[2]=*.6.*
consumer.listeners.LISTENER_B.initial-concurrency=5
consumer.listeners.LISTENER_B.maximum-concurrency=20
consumer.listeners.LISTENER_B.thread-name-prefix=listenerB-consumer-

#
# Server B config (Spring Cloud Stream)
#
spring.rabbitmq.host=server-b
spring.rabbitmq.port=5672
spring.rabbitmq.username=
spring.rabbitmq.password=

spring.cloud.stream.bindings.outbound.destination=microservice-out
spring.cloud.stream.bindings.outbound.group=default

spring.cloud.stream.rabbit.binder.connection-name-prefix=local:microservice

所以我的问题是:是否可以在同一Spring Boot应用程序代码中使用该代码,该代码通过Spring AMQP消耗RabbitMQ的数据,并通过Spring Cloud Stream RabbitMQ将消息生成到另一台服务器中?如果是这样,有人可以告诉我我做错了吗?

Spring AMQP版本是引导版本2.1.7(2.1.8-RELEASE)提供的版本,而Spring Cloud Stream版本是Spring Cloud列车Greenwich.SR2(2.1.3.RELEASE)提供的版本。

编辑

我能够通过多个配置属性(而不是默认配置属性)来配置活页夹。因此,使用此配置可​​以正常工作:

#
# Server B config (Spring Cloud Stream)
#
spring.cloud.stream.binders.transport-layer.type=rabbit
spring.cloud.stream.binders.transport-layer.environment.spring.rabbitmq.host=server-b
spring.cloud.stream.binders.transport-layer.environment.spring.rabbitmq.port=5672
spring.cloud.stream.binders.transport-layer.environment.spring.rabbitmq.username=
spring.cloud.stream.binders.transport-layer.environment.spring.rabbitmq.password=

spring.cloud.stream.bindings.stream-output.destination=microservice-out
spring.cloud.stream.bindings.stream-output.group=default

不幸的是,尚无法在多个绑定器配置中设置连接名称:A custom ConnectionNameStrategy is ignored if there is a custom binder configuration

无论如何,我仍然不明白为什么在使用Spring AMQP和Spring Cloud Stream RabbitMQ时上下文似乎是“混合的”。为了使实现正常工作,仍然有必要设置一个主要的MessageHandlerMethodFactory bean。

编辑

我发现是 NoUniqueBeanDefinitionException 引起的,因为微服务本身正在创建 ConditionalGenericConverter ,Spring AMQP部件将使用它来反序列化来自服务器A的消息。

我将其删除,并添加了一些 MessageConverter 。现在问题已解决,不再需要@Primary bean。

1 个答案:

答案 0 :(得分:0)

无关,但是

  1. consumerAmqpAdmin.declareQueue(queue);

永远不要在@Bean定义内与经纪人进行交流;在应用程序上下文生命周期中为时尚早。可能有效,但是YMMV;如果代理不可用,也会阻止您的应用启动。

最好定义类型Declarables的bean,其中包含队列,通道,绑定的列表,并且在首次成功打开连接时,管理员会自动声明它们。请参阅参考手册。

  1. 我从未见过MessageHandlerFactory问题; Spring AMQP声明没有此类bean。如果您可以提供一个显示该行为的小型示例应用程序,那将很有用。

  2. 我将看看是否可以解决连接名称问题。

编辑

我找到了解决连接名称问题的方法;它涉及一些反思,但确实有效。我建议您打开一个new feature request against the binder,以请求一种机制来使用多个活页夹时设置连接名称策略。

无论如何;这是解决方法...

@SpringBootApplication
@EnableBinding(Processor.class)
public class So57725710Application {

    public static void main(String[] args) {
        SpringApplication.run(So57725710Application.class, args);
    }

    @Bean
    public Object connectionNameConfigurer(BinderFactory binderFactory) throws Exception {
        setConnectionName(binderFactory, "rabbit1", "myAppProducerSide");
        setConnectionName(binderFactory, "rabbit2", "myAppConsumerSide");
        return null;
    }

    private void setConnectionName(BinderFactory binderFactory, String binderName,
            String conName) throws Exception {

        binderFactory.getBinder(binderName, MessageChannel.class); // force creation
        @SuppressWarnings("unchecked")
        Map<String, Map.Entry<Binder<?, ?, ?>, ApplicationContext>> binders =
                (Map<String, Entry<Binder<?, ?, ?>, ApplicationContext>>) new DirectFieldAccessor(binderFactory)
                    .getPropertyValue("binderInstanceCache");
        binders.get(binderName)
                .getValue()
                .getBean(CachingConnectionFactory.class).setConnectionNameStrategy(queue -> conName);
    }

    @StreamListener(Processor.INPUT)
    @SendTo(Processor.OUTPUT)
    public String listen(String in) {
        System.out.println(in);
        return in.toUpperCase();
    }

}

spring.cloud.stream.binders.rabbit1.type=rabbit
spring.cloud.stream.binders.rabbit1.environment.spring.rabbitmq.host=localhost
spring.cloud.stream.binders.rabbit1.environment.spring.rabbitmq.port=5672
spring.cloud.stream.binders.rabbit1.environment.spring.rabbitmq.username=guest
spring.cloud.stream.binders.rabbit1.environment.spring.rabbitmq.password=guest

spring.cloud.stream.bindings.output.destination=outDest
spring.cloud.stream.bindings.output.producer.required-groups=outQueue
spring.cloud.stream.bindings.output.binder=rabbit1

spring.cloud.stream.binders.rabbit2.type=rabbit
spring.cloud.stream.binders.rabbit2.environment.spring.rabbitmq.host=localhost
spring.cloud.stream.binders.rabbit2.environment.spring.rabbitmq.port=5672
spring.cloud.stream.binders.rabbit2.environment.spring.rabbitmq.username=guest
spring.cloud.stream.binders.rabbit2.environment.spring.rabbitmq.password=guest

spring.cloud.stream.bindings.input.destination=inDest
spring.cloud.stream.bindings.input.group=default
spring.cloud.stream.bindings.input.binder=rabbit2

enter image description here