首页 时政热点 科技头条 智能AI 安全攻防 数码硬件 开发者生态 汽车 游戏 社会热点 开源推荐 医疗健康 归档 标签 关于

一次空指针问题引发的 Spring 生命周期思考

2026-09-02 1 阅读 约10分钟阅读 王硕
分享:
字号:
背景介绍 我们团队负责的是系统的核心组件——内容安全审核系统。该系统对接了公司内部众多的业务方(如视频、评论、账号、私信等),其核心交互架构基于 RocketMQ 构建,采用典型的异步管道模式: 进审阶段:业务方将待审核的数据发送到其对应的 RocketMQ 进审 Topic。审核阶段:我们的审核系统根据自身处理能力,消费这些进审消息,进行机审、人审、AI 文本/图像识别等一系列安全检测。出审阶段:检测完成后,系统会根据预先配置好的映射关系,将审核结果发送到业务方指定的出审 Topic(或通过 Webhook 下发)。同时,为了进行审计和排查,审核记录会通过一个专用的异步 MQ 发送到 Elasticsearch(ES)进行存储。 其简化架构如下: 诡异的线上异常 在一次服务升级重启发布后,我们收到业务方的反馈: 部分内容已经生产成功了,但一直没有收到审核结果。 我们随即展开排查。诡异的是: 我们在后台的 ES 审计日志中,能够明确查到这几条内容的审核记录。 这说明系统确实正常消费了这几条消息,并成功通过异步 MQ 写入了 ES。但这些业务确实没有收到出审结果。而且这种现象是偶现的。项目刚刚启动时会出现,运行一段时间之后,所有业务的下发就完全恢复正常了。 顺着下发逻辑,我们排查到了发送结果的代码: public static boolean sendProduce(Producer producer, Object message, String key, int count) { if (Objects.isNull(producer)) { // 线上正是卡在了这里! logger.info("message " + message + "key " + key + "sendProduce producer is null,return."); throw new RuntimeException("sendProduce err.producer is null."); } if (Objects.isNull(message) || StringUtils.isBlank(key)) { logger.info("message=" + message + ",key=" + key + " is null,return."); return true; } try { Result res = producer.publish(message, key); if (res != null && res.isSuccess) { logger.info("produce message:" + GsonUtil.toGson(message) + ",success:" + GsonUtil.toGson(res)); return true; } else { if (count <= 3) { logger.info(count + " try produce message: " + GsonUtil.toGson(message)); count++; sendProduce(producer, message, key, count); } logger.info("produce error message: " + GsonUtil.toGson(message)); throw new RuntimeException("sendProduce retry 3,fail! group: " + producer.getGroup() + " ,topic: " + producer.getTopic()); } } catch (Exception e) { logger.error("send Produce method message: " + GsonUtil.toGson(message) + ", error: " + e.getMessage(), e); throw new RuntimeException(e); } } 摆在面前的两个“不合常理” 面对这个空指针异常,结合我们当时的系统认知,脑海中冒出了两个极度矛盾的疑问: 疑问一:为什么 ES 审计 MQ 正常,而业务出审 MQ 却报空指针? 如果是 ProducerManager 这个管理类在注入时存在时序问题,那为什么同样由它管理的、往 ES 写审计日志的 esTransProducer 可以正常工作,唯独下发给业务结果的 Producer 是 null? 当时 ProducerManager 的定义中,ES 生产者是这样声明的: @Bean(initMethod = "start", destroyMethod = "shutdown", name = "esTransProducer") public Producer esTransProducer() { return new Producer(esTransProducerStr, esTransTopic); } 疑问二:为什么不是全量失败,而是部分业务失败、偶现失败? 按照我们当时的直觉:一个组件要么没加载完(大家都不能用),要么加载完了(大家都能用)。 如果是 Spring 创建 Bean 的顺序或者类加载的时机出了问题,不应该“众生平等”全部报空指针吗?为什么偏偏是有些业务的 Producer 已经可用,而有些业务的 Producer 却拿到了 null? 带着这两个巨大的问号,我们不得不按图索骥,重新审视团队对于 Spring 生命周期以及代码初始化时序的理解。 快速止血:首要解决生产环境问题 我们服务在不断迭代,不断升级,不能每次升级都出现类似不下发审核结果的情况,当务之急是快速止血。 回到 ProducerManager 和 ConsumerManager 的代码,我们发现它们都实现了 ApplicationRunner 接口。由于当时没有配置任何 @Order 注解,Spring Boot 启动时,这两个 Runner 的执行顺序是不确定的。 顺着这个线索,我们做出了第一个推断:加载顺序问题。 如果 ConsumerManager 先执行,消费者便会立刻启动并开始拉取 MQ 消息;而此时 ProducerManager 的 run() 方法可能还没执行(或者还没执行完),导致动态创建 Producer 的 Map 依然为空。此时一旦有消息进来,必然会触发 producer is null 的异常。 为了验证这个推断,我们紧急引入了 @Order 注解进行时序控制: // 1. 让 ProducerManager 优先执行,先初始化所有的发送端 @Component @Order(10) public class ProducerManager implements ApplicationRunner { ... } // 2. 让 ConsumerManager 最后执行,等发送端全部 Ready 后,再开启消费者 @Component @Order(Ordered.LOWEST_PRECEDENCE) publ
这篇文章对您有帮助吗?

订阅66必读

每日精选科技资讯,直达你的邮箱