常见坑与最佳实践
A 依赖 B,B 又依赖 A,启动直接报 BeanCurrentlyInCreationException。Spring 用三级缓存试图解决 setter/字段注入的循环依赖,但构造器注入的循环依赖无解——你还没 new 完就被要求 new 别人,逻辑上死锁。解法:重新设计,把共用逻辑抽到第三个类,或用 @Lazy 延迟一方的注入。
前面第 5 章列过五种场景,这里再强调一遍最常踩的:同类内部调用 this.methodTransactional()。因为 AOP 代理是"包了一层壳",this 调用绕过了壳。解决:把方法移到另一个 Bean,或者注入自己(@Autowired private Self self;)再调 self.methodTransactional()。
同一个类型被注册了两次(比如扫描到一个、Java 配置类又 @Bean 了一个),Spring Boot 2.1+ 默认直接报错。老配置 spring.main.allow-bean-definition-overriding=true 能放开,但那是给老项目兜底的,新项目别用——老老实实把重复的 Bean 删掉一个。
第 4 章讲过:同类内部调用、非 public 方法、自己 new 的对象,这三种情况下切面都不会织入。排查时先在切面方法第一行打日志,没打日志就是没织入,别怀疑业务逻辑。
字段上写 @Value("${app.name}"),结果启动后是 null?八成是这个类没被 Spring 扫描到——你自己 new 出来的对象,注解不会生效。另一个常见原因是配置文件没放在 classpath 的正确位置,或者 profile 没激活。先用 --debug 看配置加载报告。
最佳实践清单
| 实践 | 为什么 |
|---|---|
| 一律构造器注入 | 依赖不可变、启动即失败、方便测试。 |
| @Transactional 写在 public 方法上 | 非 public 方法注解不生效,别白费力气。 |
| rollbackFor = Exception.class | 默认只回 RuntimeException,业务里抛 checked 异常会不回滚。 |
| Service 之间单向依赖 | 避免循环依赖,依赖方向从底层到上层。 |
| 配置走 @ConfigurationProperties | 类型安全、批量绑定、IDE 能补全。 |
| 横切逻辑用切面,别复制粘贴 | 日志、监控、事务、权限——一律切面化。 |
Spring 常用注解速查表
| 注解 | 一句话说明 |
|---|---|
@Component | 通用组件标记,其他三层注解的父类。 |
@Service | 业务层组件,语义化标记。 |
@Repository | 数据访问层组件,自动转译数据访问异常。 |
@Controller | Web 层控制器(返回视图)。 |
@RestController | Web 层 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 保证事务提交后才发。 |
① IoC 是大管家:对象不用自己 new,找 Spring 要。
② AOP 是拦截器:把日志、事务、安全从业务里抽出来。
③ @Transactional 是保险丝:贴对地方才管用,贴错地方就是装饰。
④ 下一页讲 Spring Boot:约定优于配置,把这些底层都自动化掉。
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,最大的机械工作量就是批量改包名。
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、再改包名、最后升框架版本,一步一验证。