常见坑:循环依赖、懒加载、时区、事务代理
框架越好用,出问题时越隐蔽。本章把 Spring Boot 日常四大坑讲透:启动报循环依赖、@Lazy 误用、时间错 8 小时、@Transactional 悄悄失效。示例基于 Boot 3.x。
循环依赖
A 构造器里要 B,B 构造器里又要 A,Bean 无法创建:
@Service
public class A {
private final B b;
public A(B b) { this.b = b; } // 启动报错:Bean A 与 B 构成循环引用
}
@Service
public class B {
private final A a;
public B(A a) { this.a = a; }
}
Boot 2.6+ 默认 spring.main.allow-circular-references=false,启动直接失败并提示,这是好事。正确解法是重构:职责拆层(Controller→Service 单向依赖)、用事件解耦(A 发布事件由 B 监听)、或把公共逻辑下沉为第三个 Bean。@Lazy 打断循环只是推迟报错,属临时手段,别用放开开关掩盖设计问题。
懒加载 @Lazy
@Lazy 让 Bean 到首次使用时才初始化。适用场景:启动慢的可选组件、确实存在的循环依赖打补丁。两个代价要清楚:
- 全局
spring.main.lazy-initialization=true:启动快了,但首次请求变慢、初始化期错误被推迟到运行时才暴露,还可能与部分代理/AOP 行为冲突——生产慎用全局懒加载; - @Lazy 注入的是代理对象,强转具体类型、instanceof 判断可能出意外。原则:默认饿汉式启动,个别重组件按需 @Lazy。
时区:时间为什么差 8 小时
三层各有时区,各错一层就偏 8 小时:
- JVM 层:容器默认 UTC、宿主机 Asia/Shanghai 时,
new Date()/LocalDateTime.now()就错了。容器加ENV TZ=Asia/Shanghai,或 JVM 参数-Duser.timezone=Asia/Shanghai; - JDBC 层:连接串里显式指定(配合连接器参数体系,MySQL 8 驱动还支持 connectionTimeZone 等新参数,以 Connector/J 官方文档为准):
jdbc:mysql://127.0.0.1:3306/demo?serverTimezone=Asia/Shanghai&useSSL=false
- 序列化层:Jackson 输出时间按应用时区,接口给前端建议统一约定。Java 侧两个选择:
Instant/OffsetDateTime(自带时区,推荐跨时区场景),或LocalDateTime(无时区,靠约定)。常用配置:
spring:
jackson:
time-zone: Asia/Shanghai
date-format: yyyy-MM-dd HH:mm:ss
LocalDateTime 本身不含时区,spring.jackson.time-zone 只影响 Date/Calendar 类的序列化,别指望它给 LocalDateTime 补时区——统一约定即可。
事务代理:@Transactional 为什么没生效
@Transactional 靠 AOP 代理实现,四条经典失效场景:
- 自调用:同类里
this.inner()不走代理,注解被跳过; - 非 public / final 方法:默认不织入代理;
- 异常类型不符:默认只回滚 RuntimeException 与 Error,受检异常要显式 rollbackFor;
- 异常被吞:try-catch 里把异常吃掉,框架看不到异常自然"正常提交"。
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void create(Order o) {
saveOrder(o); // 经代理调用,事务生效
this.markStock(o); // 自调用:绕过代理,markStock 上的注解不生效
}
@Transactional
public void markStock(Order o) { /* 独立事务意图落空 */ }
}
解法三选一:拆成两个 Service Bean 互相调用;注入自身(或 ObjectProvider<OrderService>);或干脆用 TransactionTemplate 手动包事务,最直白可控。另外注意:代理方法抛出的异常要能传到代理边界,被 @RestControllerAdvice 处理后事务方法内异常已抛出则回滚正常,别在 Service 里先 catch 再吞。
小结:构造器注入天然规避循环依赖;@Lazy 是补丁不是方案;时区要在 JVM/JDBC/序列化三层统一约定;@Transactional 记住"经代理调用、public 方法、rollbackFor=Exception.class、别吞异常"。