事情是这样的。
我们一个订单异步处理的服务,消费端配的是手动 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)。
于是发生了这么一条链:
- 公共组件创建了一个名为
rabbitListenerContainerFactory的 Bean(因为用了@ConditionalOnMissingBean,而业务方此前没有定义过同名的,所以它生效了)。 - Spring Boot 的自动配置看到这个名字已经被占用了,退让,不创建自己的 factory。
- 结果: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 配置静默失效。
这是一篇高价值的排障复盘,揭示了自定义 Bean 与自动配置的替换关系。适合使用 Spring Boot、RabbitMQ 以及编写公共组件的开发者阅读,能直接用于排查配置不生效和消息丢失问题。