@ConditionalOnMissingBean 把我配好的 RabbitMQ 手动 ack 吃掉了

文章来源声明: 原文作者:卷毛的技术笔记; 来源站点:掘金; 原文链接:https://juejin.cn/post/7701986591326765098; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

这是一篇高价值的排障复盘,揭示了自定义 Bean 与自动配置的替换关系。适合使用 Spring Boot、RabbitMQ 以及编写公共组件的开发者阅读,能直接用于排查配置不生效和消息丢失问题。

现象:消息消费失败,但消息不见了 ----------------

事情是这样的。

我们一个订单异步处理的服务,消费端配的是手动 ack:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual
        prefetch: 10
        concurrency: 4
        max-concurrency: 8

消费端代码也很标准:

@RabbitListener(queues = "order.process.queue")
public void onMessage(OrderMessage message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
    try {
        orderService.process(message);
        channel.basicAck(deliveryTag, false);
    } catch (Exception e) {
        log.error("处理失败", e);
        // 按设计应该 nack 并重新入队
        channel.basicNack(deliveryTag, false, true);
    }
}

设计意图很明确:处理失败就 nack 重新入队,等一会儿重试。

但上线之后,测试同学报了一个诡异的问题:有一条消息处理失败了(日志里有 error),但队列里没有这条消息,也没有进死信队列,就这么消失了。

按手动 ack 的逻辑,失败了应该 nack 重新入队才对。消息消失说明——它被自动 ack 了。

也就是说,acknowledge-mode: manual 根本没生效。

第一步:确认模式

我第一件事是在消费方法里打了断点和日志:

@RabbitListener(queues = "order.process.queue")
public void onMessage(OrderMessage message, Channel channel, ...) {
    log.info("ack mode = {}", channel.getConnection().getDelegate().getChannel() ...);
    ...
}

绕了点弯。更直接的办法是在启动后拿容器看:

@Autowired
private RabbitListenerEndpointRegistry registry;

@PostConstruct
public void printAckMode() {
    registry.getListenerContainers().forEach(c -> {
        if (c instanceof SimpleMessageListenerContainer) {
            log.info("container={}, ackMode={}", c, ((SimpleMessageListenerContainer) c).getAcknowledgeMode());
        }
    });
}

打出来:

container=... ackMode=AUTO

果然是 AUTO。

配置写了 manual,实际是 AUTO。 说明配置根本没被应用。

第二步:为什么配置没被应用

我去翻了 Spring Boot 的 RabbitAnnotationDrivenConfiguration(在 spring-boot-autoconfigure 里)。

关键代码长这样:

@Bean
@ConditionalOnMissingBean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
SimpleRabbitListenerContainerFactoryConfigurer simpleRabbitListenerContainerFactoryConfigurer(...) {
    ...
}

@Bean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
@ConditionalOnMissingBean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
        SimpleRabbitListenerContainerFactoryConfigurer configurer,
        ConnectionFactory connectionFactory) {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    configurer.configure(factory, connectionFactory);   // <---- 关键这一行
    return factory;
}

RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME 就是 "rabbitListenerContainerFactory"。

第二行那个注解 @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory") 的意思是:如果容器里已经有了一个叫这个名字的 Bean,我就不创建了。

那我们项目里有没有这个 Bean?有。

问题出在我们引用的一个内部公共组件里。这个组件的作者想做一件"贴心"的事——为业务方提供一个开箱即用的 RabbitMQ 配置。他的代码大概是这样的:

@Configuration
@ConditionalOnClass(RabbitTemplate.class)
public class CommonRabbitConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory")
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
            ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setConcurrentConsumers(4);
        factory.setMaxConcurrentConsumers(8);
        factory.setPrefetchCount(10);
        // 注意:这里没有设置 acknowledgeMode
        // 也没有调用 configurer.configure(...)
        return factory;
    }
}

看到了吧。

他手动设置了 concurrency 和 prefetch,但没设 acknowledge-mode,也没调用 configurer.configure(factory, connectionFactory)。

于是发生了这么一条链:

  1. 公共组件创建了一个名为 rabbitListenerContainerFactory 的 Bean(因为用了 @ConditionalOnMissingBean,而业务方此前没有定义过同名的,所以它生效了)。
  2. Spring Boot 的自动配置看到这个名字已经被占用了,退让,不创建自己的 factory。
  3. 结果:yml 里配的 spring.rabbitmq.listener.simple.* 全部没有生效——因为那些配置是靠 SimpleRabbitListenerContainerFactoryConfigurer 应用上去的,而这一环被跳过了。

acknowledge-mode: manual 没生效,自然就变成默认的 AUTO 了。

这就是最讽刺的地方:@ConditionalOnMissingBean 的本意是"我提供一个兜底,用户配了我就不管"。但因为自动配置也有同样的条件,两边都在"等人先定义",结果谁先谁赢,反而把标准配置顶掉了。

第三步:怎么修

最彻底的修法是把公共组件里那个 factory Bean 删掉,还给 Spring Boot 自动配置。

但这是内部公共组件,好几个项目在用,删了怕影响别人。

我采取的折中方案是:在自己的项目里显式覆盖这个 Bean,并且正确地调用 configurer。

@Configuration
public class MyRabbitConfig {

    @Bean(name = "rabbitListenerContainerFactory")
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
            SimpleRabbitListenerContainerFactoryConfigurer configurer,
            ConnectionFactory connectionFactory) {

        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        // 关键:让 Spring Boot 把 yml 里的所有 listener.simple.* 配置应用上来
        configurer.configure(factory, connectionFactory);
        return factory;
    }
}

就这么简单。只要你自定义了 rabbitListenerContainerFactory,就一定要注入 SimpleRabbitListenerContainerFactoryConfigurer 并调用 configure(),否则你丢的不是一个配置,是 yml 里 spring.rabbitmq.listener.simple.* 下的全部配置。

改完之后再打印:

container=... ackMode=MANUAL

手动 ack 回来了。

顺手我也给那个公共组件提了个 PR,让它至少调一下 configurer。不过这是后话了。

还有个坑:MessageConverter 只设了一边

手动 ack 修好之后,我才发现还有个隐患。

我们的消息用 JSON 传,消费端希望自动反序列化成对象。我在配置类里加了个转换器:

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
    RabbitTemplate template = new RabbitTemplate(connectionFactory);
    template.setMessageConverter(new FastJson2MessageConverter());
    return template;
}

注意——这只影响发送端。

@RabbitListener 消费时的反序列化,用的是 SimpleRabbitListenerContainerFactory 上的 messageConverter,跟 RabbitTemplate 没有半毛钱关系。

结果就是:发送的时候是 application/json,消费的时候用默认的 SimpleMessageConverter 去解,收到的要么是 byte[],要么直接报 MessageConversionException。

正确做法是两边都设:

@Bean
public MessageConverter fastJson2MessageConverter() {
    return new FastJson2MessageConverter();
}

@Bean(name = "rabbitListenerContainerFactory")
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
        SimpleRabbitListenerContainerFactoryConfigurer configurer,
        ConnectionFactory connectionFactory,
        MessageConverter fastJson2MessageConverter) {     // 注入进来

    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    configurer.configure(factory, connectionFactory);
    factory.setMessageConverter(fastJson2MessageConverter);   // 关键
    return factory;
}

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
                                     MessageConverter fastJson2MessageConverter) {
    RabbitTemplate template = new RabbitTemplate(connectionFactory);
    template.setMessageConverter(fastJson2MessageConverter);
    return template;
}

记忆口诀:RabbitTemplate 管发,ContainerFactory 管收,两边都要设。

还有个细节:FastJson2MessageConverter 如果你用 new 无参构造,它会用一个内部默认的 codec。如果业务对象里有泛型嵌套(比如 List<OrderDetail>),反序列化可能丢类型。稳妥的写法是自己构造一个 JSONWriter/JSONReader 配置,或者干脆在消息体里带类型字段。我们的做法是统一在消息 DTO 上加一个 @JSONType 标注,或者在 converter 里注册好对应的类。

关于 @ConditionalOnMissingBean 的一点经验

这个坑之后,我对 @ConditionalOnMissingBean 这个注解的态度从"香"变成了"谨慎"。

它最大的问题是静默。

你定义了一个 @ConditionalOnMissingBean 的 Bean,它生效了,日志里不会有任何提示。它没生效,日志里也不会有提示。等到出问题时,你在代码里翻来翻去,根本想不到是因为"另一个地方也定义了同名 Bean"。

所以我现在有几个习惯:

1. 写公共组件的时候,Bean 名字一定要加前缀。 别叫 rabbitListenerContainerFactory,叫 silkyRabbitListenerContainerFactory。你不该去抢框架的标准 Bean 名。

2. 真要用 @ConditionalOnMissingBean,加个 @ConditionalOnProperty 开关。 让业务方可以显式地关掉它。

3. 排查"配置不生效"时,第一件事是查 Bean 定义来源。 加个 BeanFactoryPostProcessor 或者在启动时打印,比翻代码快得多:

@Bean
public static BeanFactoryPostProcessor beanNameLogger() {
    return beanFactory -> {
        if (beanFactory.containsBeanDefinition("rabbitListenerContainerFactory")) {
            String source = String.valueOf(beanFactory.getBeanDefinition("rabbitListenerContainerFactory").getResource());
            log.warn("rabbitListenerContainerFactory 来自: {}", source);
        }
    };
}

这一招帮我定位过好几次类似的"配置神秘失效"问题。

最后

回顾一下整条链路:

环节表现根因
手动 ack 失效ackMode 实际是 AUTO自定义 factory 未调 `configurer.configure()`
配置整体失效yml 里 listener.simple.\* 都不生效同上
消费端反序列化失败收到 byte\[\] / 转换异常converter 只挂在 RabbitTemplate 上
问题难定位找不到谁覆盖了 Bean`@ConditionalOnMissingBean` 静默生效

一句话总结:Spring Boot 的自动配置是一套很精密的"条件装配"体系,你在上面盖一层自定义 Bean,很可能不是"叠加",而是"替换"。 替换掉了还没关系,最怕的是替换掉了一半——留下一堆你以为生效、其实没生效的配置。

写公共组件的人尤其要注意这一点。一个看起来"无害"的 @ConditionalOnMissingBean Bean,可能让使用方所有的 yml 配置静默失效。