楼层: 首页/ 软件技术/ Spring 核心框架/ 常见坑与最佳实践
10

常见坑与最佳实践

Common Pitfalls & Best Practices
坑 1:循环依赖

A 依赖 B,B 又依赖 A,启动直接报 BeanCurrentlyInCreationException。Spring 用三级缓存试图解决 setter/字段注入的循环依赖,但构造器注入的循环依赖无解——你还没 new 完就被要求 new 别人,逻辑上死锁。解法:重新设计,把共用逻辑抽到第三个类,或用 @Lazy 延迟一方的注入。

坑 2:@Transactional 静默失效

前面第 5 章列过五种场景,这里再强调一遍最常踩的:同类内部调用 this.methodTransactional()。因为 AOP 代理是"包了一层壳",this 调用绕过了壳。解决:把方法移到另一个 Bean,或者注入自己(@Autowired private Self self;)再调 self.methodTransactional()。

坑 3:Bean 覆盖

同一个类型被注册了两次(比如扫描到一个、Java 配置类又 @Bean 了一个),Spring Boot 2.1+ 默认直接报错。老配置 spring.main.allow-bean-definition-overriding=true 能放开,但那是给老项目兜底的,新项目别用——老老实实把重复的 Bean 删掉一个。

坑 4:AOP 不生效

第 4 章讲过:同类内部调用、非 public 方法、自己 new 的对象,这三种情况下切面都不会织入。排查时先在切面方法第一行打日志,没打日志就是没织入,别怀疑业务逻辑。

坑 5:@Value 注入 null

字段上写 @Value("${app.name}"),结果启动后是 null?八成是这个类没被 Spring 扫描到——你自己 new 出来的对象,注解不会生效。另一个常见原因是配置文件没放在 classpath 的正确位置,或者 profile 没激活。先用 --debug 看配置加载报告。

最佳实践清单

实践为什么
一律构造器注入依赖不可变、启动即失败、方便测试。
@Transactional 写在 public 方法上非 public 方法注解不生效,别白费力气。
rollbackFor = Exception.class默认只回 RuntimeException,业务里抛 checked 异常会不回滚。
Service 之间单向依赖避免循环依赖,依赖方向从底层到上层。
配置走 @ConfigurationProperties类型安全、批量绑定、IDE 能补全。
横切逻辑用切面,别复制粘贴日志、监控、事务、权限——一律切面化。

Spring 常用注解速查表

注解一句话说明
@Component通用组件标记,其他三层注解的父类。
@Service业务层组件,语义化标记。
@Repository数据访问层组件,自动转译数据访问异常。
@ControllerWeb 层控制器(返回视图)。
@RestControllerWeb 层 REST 控制器(返回 JSON),Spring 4+。
@Configuration标记配置类。
@Bean在配置类里定义一个 Bean。
@Autowired自动注入(推荐构造器注入,单构造器可省略)。
@Value注入单个配置值。
@Qualifier多实现时按名字指定。
@Primary多实现时默认选它。
@Scope指定 Bean 作用域。
@Lazy懒加载,首次使用才创建。
@Profile多环境条件装配。
@Transactional声明式事务。
@Aspect标记切面类。
@Pointcut定义切点表达式。
@Around / @Before / @After通知类型。

这张表混个眼熟,用到时再回来查。Spring 的注解看着多,其实核心就那十几个——IoC 相关 6 个、AOP 相关 4 个、事务 1 个、配置 3 个。

Spring 6.1 新特性速览

特性说明
JDK 17+ 基线 Records、switch 模式匹配、文本块等新语法全面可用。
RestClient同步 HTTP 客户端,替代 RestTemplate,流式 API。
@ConfigurationProperties 绑定 Records配置类直接写成 record,不用 getter/setter。
AOT 处理支持 GraalVM 原生镜像编译。
HTTP Interface声明式 HTTP 客户端接口,类似 Feign。

Spring 核心学习路线(四周计划)

周次目标
第一周跑通 IoC:写一个 Maven 项目,用 @Configuration + @Bean + 构造器注入,把三个类组装起来。
第二周写 AOP:给所有 Service 方法加耗时统计,验证 @Around 和切点表达式。
第三周写事务:用 H2 数据库实现银行转账,故意制造余额不足,观察回滚。
第四周写事件:注册成功后异步发邮件,用 @TransactionalEventListener 保证事务提交后才发。
记
Spring 核心一句话总结

① IoC 是大管家:对象不用自己 new,找 Spring 要。

② AOP 是拦截器:把日志、事务、安全从业务里抽出来。

③ @Transactional 是保险丝:贴对地方才管用,贴错地方就是装饰。

④ 下一页讲 Spring Boot:约定优于配置,把这些底层都自动化掉。

自测题 · Spring 核心

1.(概念题)Spring 里"控制反转"到底反转了什么?请用自己的话说。

查看答案

答案:反转的是"对象的创建权"。以前你在代码里主动 new 依赖,现在把创建和组装交给 Spring 容器,容器在启动时把对象准备好,需要时注入给你。控制权从代码里移到了容器手里。解析:DI(依赖注入)是 IoC 的具体实现方式。

2.(选择题)下面哪种注入方式是 Spring 官方最推荐的?A. 字段注入 @Autowired B. Setter 注入 C. 构造器注入

查看答案

答案:C,构造器注入。解析:构造器注入保证依赖不可变(final 字段)、启动时立刻发现缺失依赖(不会跑一半 NPE)、写单元测试时直接 new 就行不用反射。字段注入写着爽但坑多。

3.(排错题)小李在 UserService 里写了一个 private 方法 updateProfile(),上面贴了 @Transactional,结果发现这个方法的事务根本没生效。为什么?

查看答案

答案:Spring AOP 基于动态代理,只对 public 方法生效。private 方法上贴 @Transactional,注解会被静默忽略。解析:另外,如果这个 private 方法是被同类的 public 方法 this 调用的,同样会绕过代理。两个问题叠加,事务当然不生效。

4.(思考题)银行转账时,A 扣完钱、B 加钱前,B 账户突然被删除了。用 @Transactional 怎么保证 A 的钱不丢?

查看答案

答案:transfer 方法上贴 @Transactional,扣钱和加钱在同一个事务里。加钱那步抛异常 → 事务回滚 → 扣钱那步被撤销,A 的余额恢复原样。解析:这就是事务的原子性——多条 SQL 绑成一个整体,要么全成要么全败。

5.(判断题)Spring 6.1 里,javax.servlet.http.HttpServletRequest 这个类还能用吗?

查看答案

答案:不能。Spring Framework 6 全面迁移到 Jakarta EE 9 命名空间,所有 javax.* 包名都改成了 jakarta.*。正确写法是 jakarta.servlet.http.HttpServletRequest。解析:从 Spring 5 升级到 Spring 6,最大的机械工作量就是批量改包名。

综合练习题 · 学完整个 Spring 核心再做

1.(设计题)你要写一个"支付服务",它要扣钱、写流水、发通知。请画出 IoC 依赖关系,并说明每个类该贴什么注解、怎么注入。

查看答案

参考:PaymentService(@Service)构造器注入 AccountDao(@Repository)、TransactionLogDao(@Repository)、NoticeService(@Service)。所有类交给容器管,PaymentService 用构造器接收依赖。加分:写流水和发通知的方法标 @Transactional(propagation=REQUIRES_NEW),保证支付失败流水也留下。

2.(调试题)启动报 BeanCurrentlyInCreationException,说是 "Error creating bean with name 'A' ... in construction ... requested bean currently in creation"。A 和 B 互相依赖。怎么修?

查看答案

答案:这是构造器循环依赖,Spring 三级缓存也救不了。修法:① 重新设计,把 A、B 的共同依赖抽到第三个类 C;② 在其中一方的注入点加 @Lazy,让它先用代理延迟注入;③ 改成 setter/字段注入(不推荐,治标不治本)。解析:根治永远是重新设计依赖方向,别让两个类互相 new。

3.(实现题)用 AOP 写一个切面:给 com.example.service 包下所有方法统计耗时,超过 500ms 的打 WARN 日志。写出切点表达式和通知方法。

查看答案

参考:@Aspect @Component;@Pointcut("execution(* com.example.service..*.*(..))");@Around 里 long start=...; Object ret = pjp.proceed(); long cost=...; if(cost>500) log.warn(...); return ret;。加分:记得 finally 里计算耗时、记得调 proceed() 否则业务不执行。

4.(排错题)@Transactional 贴在 transfer() 上,里面 catch 了异常打日志后没抛出。调用方说"钱扣了但对方没收到,事务怎么没回滚?"问题出在哪?

查看答案

答案:异常被 try-catch 吞掉了,Spring 事务管理器根本不知道发生了异常,自然不会回滚。解析:catch 住后要么重新 throw 出去,要么手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。别为了"优雅"把异常吃掉。

5.(迁移题)老项目是 Spring 5.3 + JDK 8 + javax.persistence.Entity,要升到 Spring 6.1 + JDK 17。列出至少三件必做的事。

查看答案

答案:① JDK 升到 17+;② 全局把 javax.* 改成 jakarta.*(javax.persistence→jakarta.persistence、javax.servlet→jakarta.servlet);③ 检查被 @Deprecated 删除的 API 有没有用到;④ 构建工具和依赖版本对齐。解析:顺序:先升 JDK、再改包名、最后升框架版本,一步一验证。