大家好,我是晓凡。
事情要从一个看似平常的周五下午说起。
那天阳光明媚,我正准备提交代码下班,突然测试组小妹甩来一条消息:“这个退款流程又失败了,生产环境!”
我一看日志,心凉了一半:
java.lang.NullPointerException: Cannot invoke "Object.toString()" because the return value of "...Service.getRefundInfo(...)" is null
空指针?这玩意儿谁还没写过。我心想,十分钟解决的事,稳得很。
结果这一调,就是整整一天。
🚨 问题初现
退款接口是这样设计的:
public RefundInfo getRefundInfo(String orderId) {
// 一堆逻辑...
return someCondition ? refundInfo : null;
}
然后在 Controller 层直接用了:
String result = refundService.getRefundInfo(orderId).toString();
是不是很眼熟?对,就是那个经典的 NPE 地雷 —— 没做判空就直接 .toString()
!
我赶紧加了个空判断:
RefundInfo info = refundService.getRefundInfo(orderId);
if (info == null) {
return "退款信息不存在";
}
String result = info.toString();
自信地提交代码,再测一遍……居然还是报错!
🤯 WTF?为什么明明判空了还出错?
这时候我才意识到问题没那么简单。我把日志拉出来细看,发现并不是同一个空指针错误:
Caused by: java.lang.NullPointerException: Cannot invoke "org.springframework.data.redis.core.StringRedisTemplate.opsForValue()" because "this.redisTemplate" is null
什么?我的 RedisTemplate 居然为 null!
但奇怪的是,这个 Bean 明明在配置类里注册了,而且本地测试没问题。那问题出在哪呢?
🔍 抽丝剥茧
经过一番排查,我发现问题出现在 Bean 的加载顺序 上。
有一个自定义配置类依赖了 RedisTemplate:
@Configuration
public class MyConfig {
@Autowired
private StringRedisTemplate redisTemplate;
@PostConstruct
public void init() {
// 使用 redisTemplate 初始化数据
}
}
而该配置类被另一个 @Bean
依赖,导致它比 RedisTemplate 更早加载,此时 Spring 容器还未完成注入,redisTemplate
是 null。
于是我就在这里掉进了坑里。
🧠 解决方案
- 延迟初始化:将
@PostConstruct
改为通过InitializingBean
或 XML 配置的方式延迟初始化。 - 调整加载顺序:使用
@DependsOn("redisTemplate")
强制指定依赖顺序。 - 避免构造函数/PostConstruct 中访问未初始化 Bean:这是最容易踩的坑,尤其在复杂项目中。
💡 总结一下这次教训:
- 空指针并不可怕,可怕的是你以为已经处理好了,其实还有隐藏更深的问题。
- Spring 的自动装配虽然强大,但在 Bean 加载顺序和生命周期管理上,一定要小心谨慎。
- 能不放在
@PostConstruct
里做的操作,尽量延迟到真正使用时去做。
现在回头看看,这个 Bug 其实并不复杂,但结合了多个组件、环境差异和逻辑误判,让我足足调了一整天,真的是边查边骂自己:“怎么这么不小心!”
所以啊,写代码容易,写稳定的代码难。
你也有那种“以为十分钟搞定,结果搞了一天”的经历吗?欢迎评论区一起吐槽👇