SSM项目Service嵌套注入报错空指针如何解决循环依赖问题附常见报错案例
先看看你遇到的报错长什么样
开发SSM项目的时候,最容易让人抓狂的就是这个:
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'xxxService': Bean with name 'xxxService' has been injected into other beans [yyyService] but its raw factory reference is not yet available.
或者更常见的——运行到一半直接 NullPointerException,堆栈信息指向某个Service的某个方法,一脸懵逼地打开代码一看,对象明明new出来了啊?
这种情况十有八九是 循环依赖 搞的鬼。下面我把它掰开了揉碎了讲清楚。
一、到底什么叫”循环依赖”
用大白话说:A依赖B,B又依赖A,两人互相等着对方先活过来,结果谁也没法先出生。
UserService 需要注入 → OrderService
OrderService 需要注入 → UserService
Spring在创建Bean的时候是按顺序来的:
- 先创建UserServiceImpl
- 发现它需要OrderService,暂停创建UserServiceImpl,去创建OrderServiceImpl
- OrderServiceImpl又需要UserService,但此时UserServiceImpl还没创建完
- 死锁,报错
这就是循环依赖的本质——两个Bean互相持有对方的引用,陷入”鸡生蛋、蛋生鸡”的死循环。
二、最常见的三种循环依赖写法(看看你踩了哪一个)
写法一:Service互相注入(最常见)
@Service
public class UserServiceImpl implements UserService {
@Autowired
private OrderService orderService; // ← 注入了OrderService
public void createUser() {
orderService.checkOrder(); // 运行时才用到
}
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private UserService userService; // ← 又注入了UserService,循环了!
public void checkOrder() {
userService.getUserById(1L);
}
}
写法二:通过构造器注入(Spring默认不允许)
@Service
public class UserServiceImpl {
private final OrderService orderService;
// 构造器注入 —— 这是最危险的循环依赖
public UserServiceImpl(OrderService orderService) {
this.orderService = orderService;
}
}
@Service
public class OrderServiceImpl {
private final UserService userService;
public OrderServiceImpl(UserService userService) {
this.userService = userService;
}
}
构造器注入和字段注入不一样,构造器注入在Bean创建完成之前就无法完成依赖注入,Spring默认对构造器注入的循环依赖直接报错。
写法三:在@PostConstruct或初始化方法中注入
@Service
public class InitService {
@Autowired
private OtherService otherService;
@PostConstruct
public void init() {
// 这里使用otherService,如果otherService又依赖InitService,就会出问题
otherService.doSomething();
}
}
三、解决方案(从最简单到最彻底)
方案一:用 @Lazy 延迟注入(最推荐,改动最小)
@Lazy 的意思是:”你先别急着注入,等我真的要用你的时候再创建。”
@Service
public class UserServiceImpl implements UserService {
@Autowired
@Lazy // ← 加上这个注解,延迟注入
private OrderService orderService;
@Override
public void createUser() {
// 这里才会真正去获取OrderService的代理对象
orderService.checkOrder();
}
}
另一边也要对称处理:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
@Lazy // ← 同样加上
private UserService userService;
@Override
public void checkOrder() {
userService.getUserById(1L);
}
}
原理:Spring在创建Bean时,会先创建一个早期的”半成品”对象(只完成了实例化,还没有填充属性),然后把这个半成品放入缓存。当遇到循环依赖时,先从缓存中取这个半成品代理对象注入进去,等两个Bean都创建完了,再替换成完整的对象。加上@Lazy后,注入的是一个代理对象,真正调用时才触发实际Bean的创建,从而打破循环。
⚠️ 注意:
@Lazy只对 setter/字段注入 有效,对构造器注入无效。
方案二:改用 Setter 注入(Spring默认支持的解法)
Spring框架本身对 setter注入 和 字段注入(@Autowired写在字段上) 是有循环依赖解决机制的,它依靠 三级缓存 来实现。但如果你的代码写得不对,就会绕过这个机制。
确保你用的是字段注入或setter注入,而不是构造器注入:
// ✅ 正确:字段注入,Spring三级缓存可以处理
@Service
public class UserServiceImpl {
@Autowired
private OrderService orderService;
}
// ❌ 危险:构造器注入,Spring默认无法处理循环依赖
public class UserServiceImpl {
private final OrderService orderService;
public UserServiceImpl(OrderService orderService) {
this.orderService = orderService;
}
}
方案三:抽出一个中间Service(治本之策)
循环依赖的根本原因是设计上有问题,最好的解决方式是 重构代码,打破循环。
比如上面的例子,可以把公共逻辑抽到一个新的Service:
// 抽取一个专门的校验服务
@Service
public class CheckService {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
public void doCheck(Long userId) {
User user = userService.getById(userId);
orderService.findByUser(user.getId());
}
}
// UserService 和 OrderService 都不再互相依赖
@Service
public class UserServiceImpl implements UserService {
// 不再注入OrderService
}
@Service
public class OrderServiceImpl implements OrderService {
// 不再注入UserService
}
虽然多了一个类,但架构更清晰,依赖关系变成单向的:
CheckService → UserService
CheckService → OrderService
UserService ↛ OrderService (不再有依赖)
OrderService ↛ UserService (不再有依赖)
方案四:用 ApplicationContext 手动获取(最后手段)
如果你不想改注解,也不想重构,可以用这种方式”曲线救国”:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private ApplicationContext applicationContext;
private OrderService orderService;
@PostConstruct
public void init() {
// 在初始化时手动从容器中获取,避免构造阶段的循环
orderService = applicationContext.getBean(OrderService.class);
}
}
这种方法不推荐作为首选,但它确实能解决问题,适合在老项目 refactor 空间很小的场景应急使用。
四、报错案例全解析
案例一:启动时报错,直接进不去
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'userService':
Bean with name 'userService' has been injected into other beans [orderService]
but its raw factory reference is not yet available.
原因:UserService 和 OrderService 互相注入了,Spring在创建UserService时发现它需要OrderService,而OrderService又需要UserService,此时UserService已经部分创建(放入了二级缓存),但还没完成注入,所以报了这个错。
解决:在其中一个Service的@Autowired字段上加@Lazy。
案例二:项目能启动,但运行时报 NullPointerException
java.lang.NullPointerException: null
at com.example.service.UserServiceImpl.createUser(UserServiceImpl.java:25)
at com.example.controller.UserController.register(UserController.java:40)
打开UserServiceImpl.java:25一看:
public void createUser(UserDTO dto) {
// orderService 是 null!
orderService.checkAvailability(dto.getUsername()); // ← NPE
}
为什么启动没报错,运行才爆?
这种情况通常发生在:你用反射或者某种延迟初始化方式绕过了Spring的正常注入流程,或者你在 @PostConstruct 之前就调用了依赖的方法。
另一种常见场景是 静态变量缓存了早期空对象:
@Service
public class UserServiceImpl {
// ❌ 静态变量!Spring注入的是实例变量,静态变量不会被动注入
private static OrderService orderService;
@Autowired
public void setOrderService(OrderService orderService) {
UserServiceImpl.orderService = orderService; // 注入时可能是null
}
}
如果OrderServiceImpl还在创建中,传入的orderService就是null,静态变量就被赋值成了null,后续使用就NPE了。
解决:去掉static,或者加@Lazy:
@Service
public class UserServiceImpl {
@Autowired
@Lazy
private OrderService orderService; // ✅ 用代理对象,延迟解析
}
案例三:多模块项目中的循环依赖
module-a: OrderService 依赖 module-b 的 UserService
module-b: UserService 依赖 module-a 的 OrderService
这种跨模块的循环依赖更隐蔽,IDE不会直接提示你。启动时可能报:
BeanCreationException: Error creating bean with name 'orderService'
defined in file [...OrderServiceImpl.class]:
Cannot resolve reference to bean 'userService' while setting bean property 'userService';
nested exception is org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'userService':
Cannot resolve reference to bean 'orderService' while setting field 'orderService';
解决:
- 先加
@Lazy让项目能跑起来 - 再逐步重构,把共用的逻辑提取到第三个模块或公共模块中
案例四:MyBatis Mapper 注入导致的循环依赖
@Service
public class UserServiceImpl {
@Autowired
private UserMapper userMapper; // MyBatis的Mapper
@Autowired
private OrderService orderService;
}
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserService userService; // ← 循环了
}
Mapper本身没问题,但Service层之间循环依赖了。解决方式和其他案例一样,加@Lazy或者重构。
五、如何提前发现循环依赖
与其事后debug,不如提前预防。以下方法可以帮你快速定位问题:
方法一:开启Spring的循环依赖检测
在application.properties中添加:
# Spring Boot 2.x+ 默认已经开启了循环依赖检测
spring.main.allow-circular-references=false
设为false后,启动时如果存在循环依赖会直接报错并提示是哪两个Bean,一目了然。
方法二:用IDEA插件或依赖图分析
IntelliJ IDEA 有”Diagrams”功能:
- 右键点击某个Service类
- 选择
Diagrams → Show Dependencies - 可以看到哪些Service之间互相关联
方法三:代码审查时留意
看到以下写法就要警觉:
// 警示信号1:A Service 注入了 B Service,B Service 又注入了 A Service
// 警示信号2:父类注入了子类,子类又注入了父类
// 警示信号3:使用了静态变量来存储Spring注入的Bean
六、总结一张表
| 方案 | 改动大小 | 适用场景 | 推荐指数 |
|---|---|---|---|
@Lazy 延迟注入 |
极小,加一个注解 | 快速修复,不想改架构 | ⭐⭐⭐⭐⭐ |
| 改为字段/Setter注入 | 小,改注入方式 | 原本是构造器注入导致的问题 | ⭐⭐⭐⭐ |
| 抽取中间Service | 中等,需重构 | 长期维护,彻底解决问题 | ⭐⭐⭐⭐⭐ |
| ApplicationContext手动获取 | 小,改几行代码 | 老项目应急 | ⭐⭐⭐ |
| 禁止循环依赖配置 | 无代码改动 | 预防性配置 | ⭐⭐⭐⭐ |
七、一段完整的修复示例
假设你现在的代码是这样的(有问题):
// ===== 有问题的代码 =====
@Service
public class UserServiceImpl implements UserService {
@Autowired
private OrderService orderService;
@Override
public void register(UserDTO dto) {
// 校验订单是否冲突
orderService.validateUser(dto.getUserId());
// ...其他逻辑
}
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private UserService userService;
@Override
public void validateUser(Long userId) {
User user = userService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
}
}
修复后(方案一:@Lazy):
// ===== 修复后的代码 =====
@Service
public class UserServiceImpl implements UserService {
@Autowired
@Lazy // 加这一行就够了
private OrderService orderService;
@Override
public void register(UserDTO dto) {
orderService.validateUser(dto.getUserId());
}
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
@Lazy // 对称处理,更保险
private UserService userService;
@Override
public void validateUser(Long userId) {
User user = userService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
}
}
或者 方案三:彻底重构:
// ===== 重构后的代码 =====
@Service
public class UserOrderValidator {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
public void validateUser(Long userId) {
User user = userService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
}
}
@Service
public class UserServiceImpl implements UserService {
// 不再依赖OrderService,依赖关系单向
}
@Service
public class OrderServiceImpl implements OrderService {
// 不再依赖UserService,依赖关系单向
}
循环依赖不是什么可怕的东西,它只是提醒你:代码的依赖关系需要梳理一下了。用@Lazy快速解决,用重构彻底解决,两种方式结合使用,项目才能既跑得快又走得远。