三、Spring Boot 事务、AOP 与高级特性(S13–S20)
S13 · 知识点:事务传播行为
【题目】Spring 中 @Transactional 注解默认的传播行为(Propagation)是?
A. REQUIRES_NEW B. NESTED C. REQUIRED D. SUPPORTS
答案:C
【考点】事务传播行为枚举及其默认值。
【结论】选 C。Spring 中 @Transactional 默认采用 REQUIRED 传播行为,即优先加入已有事务,无事务时新建。
【逐项辨析】 A. REQUIRES_NEW:错误。REQUIRES_NEW 会挂起当前事务并强制创建全新独立事务,提交或回滚均不影响原事务,并非默认行为。 B. NESTED:错误。NESTED 依赖数据库 Savepoint 实现嵌套回滚,仅部分事务管理器支持,且需显式指定,不是默认值。 C. REQUIRED:正确。Spring 默认传播行为,当前存在事务则加入,不存在则新建事务,是最常用且安全的默认策略。 D. SUPPORTS:错误。SUPPORTS 表示有事务则参与,无事务则以非事务方式执行,常用于查询类方法,但不是默认行为。
【知识点】Spring 事务传播行为定义了事务边界在方法调用链中的传播方式,由 TransactionDefinition 接口规定。Propagation.REQUIRED 作为默认值,其核心语义是“融入或创建”:当调用链上游已开启事务时,当前方法逻辑运行于该事务上下文;若上游无事务,则由当前方法自身负责开启和关闭事务。这一设计保证了大多数业务方法在默认情况下具备原子性,同时避免了无意义的多事务开销。其余七种传播行为(REQUIRES_NEW、NESTED、SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER)分别适用于拆分独立事务、嵌套回滚、非事务查询、强制要求已有事务等特定场景。Spring 通过 AOP 代理在方法调用前根据传播行为决定是复用 TransactionStatus 还是创建新的。
【记忆锚点】“默认 Required,没事务就新建,有事务就加入;想独立用 New,嵌套用 Nested。”
【易混对比】
| 传播行为 | 有无事务时的表现 | 典型用途 |
|---|---|---|
| REQUIRED | 有则加入,无则新建 | 默认,通用业务方法 |
| REQUIRES_NEW | 挂起当前,新建独立事务 | 日志记录、审计,需独立提交 |
| NESTED | 在已有事务内创建 Savepoint | 部分回滚,如订单中的优惠券核销 |
| SUPPORTS | 有则加入,无则非事务 | 查询方法,不强制事务 |
换问法:若题目改为“@Transactional(propagation = Propagation.REQUIRES_NEW) 的作用是什么?”,则应回答:挂起当前事务并创建一个全新的独立事务。
【自测】某 Service 的 methodA 已标注 @Transactional,在其内部调用另一个 Service 的 methodB(同样标注 @Transactional,未指定 propagation)。methodB 执行时实际处于何种事务状态?
答:methodB 会加入 methodA 已开启的事务,二者共享同一个事务上下文。若 methodB 抛出 RuntimeException 且未捕获,会导致整个 methodA 的事务回滚。 与第 S14 题连考。
【知识关联】
- 同库关联:与 S14(失效)、S15(AOP 实现);传播行为枚举。
- 实现层:TransactionInterceptor 按 Propagation 决定新建/加入/挂起。
- 面试追问:① REQUIRED 与 REQUIRES_NEW?② NESTED 与 REQUIRES_NEW?
【拓展延伸】
- 变式问法:嵌套调用时回滚范围。
- 版本差异:注解稳定。
- 工程注意点:日志/审计独立事务用 REQUIRES_NEW;避免嵌套事务滥用。
S14 · 知识点:事务失效场景
【题目】以下哪种情况会导致 @Transactional 注解的事务失效?
A. 方法被 private 修饰 B. 同类内部通过 this 调用(自调用) C. 方法内异常被 try-catch 捕获且未重新抛出 D. 以上都会导致失效
答案:D
【考点】基于 AOP 代理的事务实现及其失效的典型场景。
【结论】选 D。private 修饰、同类 this 自调用以及异常被捕获未重新抛出,都会导致声明式事务失效。
【逐项辨析】 A. 方法被 private 修饰:正确(会导致失效)。Spring AOP 代理基于继承或 JDK 动态代理,private 方法对代理类不可见,无法织入事务拦截逻辑。 B. 同类内部通过 this 调用(自调用):正确(会导致失效)。this 指向目标对象本身而非代理对象,绕过了 TransactionInterceptor,事务切面无法生效。 C. 方法内异常被 try-catch 捕获且未重新抛出:正确(会导致失效)。事务管理器依赖异常信号触发回滚,异常被吞没后,TransactionInterceptor 正常返回,默认执行提交。 D. 以上都会导致失效:正确。A、B、C 三类场景均会破坏代理机制或异常传播路径,使 @Transactional 失去事务控制能力。
【知识点】Spring 声明式事务的实现根基是 AOP 动态代理。当容器中的 Bean 被注入时,实际持有的是代理对象引用。事务切面(TransactionInterceptor)在代理对象的调用链中拦截目标方法,执行 before advice 开启或加入事务,在 after advice 中根据异常状态决定提交或回滚。因此,任何导致调用不经过代理对象、或异常信号无法到达拦截器的场景都会造成事务失效。具体包括:(1)访问修饰符限制——private、final、static 方法均无法被代理子类重写或 JDK 代理拦截;(2)自调用问题——目标方法内部通过 this.method() 调用同类其他方法,属于硬编码调用,不经过代理;(3)异常处理不当——包括 catch 后未抛出、抛出 checked 异常且未配置 rollbackFor、或异常在另一个线程中产生。理解这些失效原理有助于在架构层避免事务漏洞。
【记忆锚点】“私事自己藏,this 不走代理,异常被吞下,事务全白忙。”
【易混对比】
| 失效场景 | 根因 | 解决方案 |
|---|---|---|
| private/final/static | 代理机制无法拦截 | 改为 public,避免 final |
| 同类 this 自调用 | 绕过代理对象 | 注入自身代理,或拆分到另一个 Bean |
| 异常被吞没 | 事务管理器感知不到异常 | catch 后重新抛出,或配置 rollbackFor |
| Checked 异常默认不回滚 | 默认只回滚 RuntimeException/Error | 显式设置 rollbackFor = Exception.class |
换问法:若题目改为“如何在同类中调用另一个带 @Transactional 的方法并使其生效?”,则应回答:通过注入自身的代理对象(如 @Autowired 本类接口代理)再调用目标方法。
【自测】以下代码中事务是否会回滚?为什么?
@Service
public class OrderService {
@Transactional
public void createOrder() {
try {
saveOrder();
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}
}答:不会回滚。因为 createOrder 方法内部将异常捕获并打印,未向上抛出,TransactionInterceptor 感知不到异常,会认为方法正常返回从而提交事务。若需回滚,应在 catch 块中重新抛出 RuntimeException,或去掉 try-catch 让异常自然上抛。 与第 S18 题连考。
【知识关联】
- 同库关联:与 S13/S15/S18;代理与自调用。
- 实现层:AOP 代理;private/自调用/吞异常/checked 默认不回滚。
- 面试追问:① 如何解决自调用?② rollbackFor?
【拓展延伸】
- 变式问法:同类 this 调用为何不生效。
- 版本差异:注解稳定;部分场景可用 AspectJ 织入替代代理限制。
- 工程注意点:public 方法;拆分 Bean;显式 rollbackFor=Exception。
S15 · 知识点:AOP 通知类型
【题目】以下哪个不是 Spring AOP 的通知(Advice)类型?
A. @Before B. @AfterReturning C. @Around D. @Transactional
答案:D
【考点】Spring AOP 五大通知类型与事务注解的区分。
【结论】选 D。@Transactional 是事务管理注解,并非 Spring AOP 的通知类型。
【逐项辨析】 A. @Before:错误。@Before 是标准的前置通知,在目标方法执行前织入增强逻辑,属于 AOP 五大通知之一。 B. @AfterReturning:错误。@AfterReturning 是返回通知,在目标方法正常返回后执行,常用于结果日志记录或后置处理。 C. @Around:错误。@Around 是环绕通知,功能最强大,通过 ProceedingJoinPoint 可控制是否执行目标方法及修改返回值,属于 AOP 通知。 D. @Transactional:正确。@Transactional 是声明式事务注解,其底层通过 AOP 代理实现,但注解本身定义的是事务属性(传播、隔离、超时、只读、回滚规则),而非 Advice 类型。
【知识点】Spring AOP 的通知(Advice)是切面在特定连接点执行的增强动作,由 org.aspectj.lang.annotation 包定义,包含 @Before、@After、@AfterReturning、@AfterThrowing、@Around 五种。其中 @Around 最特殊,它包裹目标方法,可在前后自定义逻辑,并通过 ProceedingJoinPoint.proceed() 决定是否执行原方法。而 @Transactional 的语义完全不同:它属于 org.springframework.transaction.annotation,描述的是事务边界。Spring 在解析 @Transactional 时,会为其生成一个专门的 Advisor(BeanFactoryTransactionAttributeSourceAdvisor),并通过 TransactionInterceptor 实现方法拦截。虽然底层也用到了 AOP 基础设施,但 @Transactional 在概念层属于事务管理范畴,而不是通用的 AOP 通知。面试中需严格区分 Aspect(切面)、Advice(通知)、Pointcut(切点)、JoinPoint(连接点)四个核心概念。
【记忆锚点】"Before 先,After 后,Returning 正常走,Throwing 出错留,Around 全包收;Transactional 管事务,不是通知别混熟。"
【易混对比】
| 概念 | 定义 | 对应注解/实现 |
|---|---|---|
| Advice(通知) | 切面在连接点执行的增强逻辑 | @Before、@After、@AfterReturning、@AfterThrowing、@Around |
| Pointcut(切点) | 定义通知应用于哪些连接点 | @Pointcut 表达式 |
| Aspect(切面) | Advice + Pointcut 的模块化封装 | @Aspect 类 |
| JoinPoint(连接点) | 程序执行过程中的可插入点 | 方法调用、异常抛出等 |
| 事务拦截器 | 专门处理事务的 AOP 拦截器 | TransactionInterceptor |
换问法:若题目改为“@Transactional 的实现原理是什么?”,则应回答:Spring 通过 AOP 代理拦截标注了 @Transactional 的方法,利用 TransactionInterceptor 在方法前后开启、提交或回滚事务。
【自测】以下代码中 doAround 方法输出的日志顺序是什么?
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("A");
Object result = pjp.proceed();
System.out.println("B");
return result;
}
@Before("pointcut()")
public void doBefore() {
System.out.println("C");
}答:输出顺序为 A → C →(目标方法执行)→ B。因为 @Around 先进入,在 proceed() 调用前输出 A;proceed() 内部会触发 @Before,输出 C;目标方法执行完毕后返回 @Around,输出 B。 与第 S14 题连考。
【知识关联】
- 同库关联:与 S13/S14;五种通知与 @Transactional 区分。
- 实现层:AspectJ 注解;Around 最灵活;Pointcut 表达式。
- 面试追问:① 执行顺序?② 与拦截器 Filter 区别?
【拓展延伸】
- 变式问法:自定义注解切面;@Around 修改返回值。
- 版本差异:Spring AOP 基于代理,Boot 版本升级注意 CGLIB/JDK 代理选择。
- 工程注意点:切点表达式要收敛;避免在 Around 吞异常破坏事务。
S16 · 知识点:Bean 作用域
【题目】@Scope("prototype") 标注的 Bean,其生命周期特征是?
A. 容器中全局唯一单例 B. 每个 HTTP Session 对应一个实例 C. 每个 HTTP 请求对应一个实例 D. 每次从容器获取(getBean/注入)都创建新实例
答案:D
【考点】Bean 作用域(singleton / prototype / request / session)语义。
【结论】选 D。@Scope("prototype") 表示每次从容器获取或注入时都会创建一个新的 Bean 实例。
【逐项辨析】 A. 容器中全局唯一单例:错误。全局唯一单例是 @Scope("singleton") 或默认无标注时的行为,与 prototype 相反。 D. 每次从容器获取(getBean/注入)都创建新实例:正确。prototype 的核心语义是“每次请求都新建”,Spring 容器在注入点或 getBean 调用时都会触发新的实例化逻辑。 C. 每个 HTTP 请求对应一个实例:错误。这是 @Scope("request") 或 @RequestScope 的语义,仅适用于 Web 应用上下文。 B. 每个 HTTP Session 对应一个实例:错误。这是 @Scope("session") 或 @SessionScope 的语义,同样仅适用于 Web 场景。
【知识点】Spring 容器对 Bean 的生命周期管理高度依赖作用域(Scope)定义。singleton 是默认作用域,容器初始化时(或首次延迟加载时)创建唯一实例,后续所有注入和 getBean 均返回该实例,由容器统一管理销毁回调。prototype 则是一种“创建后放手”的模式:容器负责完成实例化、属性填充和初始化回调(如 @PostConstruct),但此后不再持有该实例引用,也不管理其销毁(@PreDestroy 不会被执行)。request 和 session 作用域依赖 Spring Web 的 RequestContextListener 或 RequestContextFilter,将 Bean 实例绑定到 ServletRequest 或 HttpSession 的属性域中。此外,Spring 还提供了 application(ServletContext 生命周期)和 websocket 作用域。选择作用域时需权衡内存开销、线程安全与状态隔离需求。
【记忆锚点】“Single 独一份,Proto 每次新,Request 一次求,Session 会话跟。”
【易混对比】
| 作用域 | 实例数量 | 生命周期管理 | 适用场景 |
|---|---|---|---|
| singleton | 全局唯一 | 容器管理创建与销毁 | 无状态 Service、DAO、配置类 |
| prototype | 每次获取新建 | 容器只负责创建,不销毁 | 有状态对象、多线程独立上下文 |
| request | 每个 HTTP 请求一个 | 请求结束销毁 | 请求级缓存、表单对象 |
| session | 每个 HTTP Session 一个 | Session 过期销毁 | 用户登录信息、购物车 |
换问法:若题目改为“@Scope(”prototype“) 的 Bean 在使用 @PreDestroy 时是否会执行?”,则应回答:不会执行。prototype 实例在创建后容器不再持有引用,因此不会触发销毁回调。
【自测】以下代码中,userService 被两个 Controller 注入时,UserService 实例各有几个?
@Service
@Scope("prototype")
public class UserService { ... }答:每个 Controller 注入点时 Spring 都会新建一个 UserService 实例。若有两个 Controller 分别注入,则至少创建两个实例;如果同一个 Controller 中有多个注入点,每个注入点也会创建独立实例。 与第 S17 题连考。
【知识关联】
- 同库关联:与 S17(注入)、S18(循环依赖);prototype 与有状态。
- 实现层:singleton 默认;prototype 每次新建且容器不管销毁。
- 面试追问:① 线程安全的单例如何写?② request/session 作用域?
【拓展延伸】
- 变式问法:有状态 Service 是否该 prototype。
- 版本差异:注解稳定。
- 工程注意点:默认单例避免成员可变状态;有状态用 ThreadLocal 或 prototype。
S17 · 知识点:@Autowired 与 @Resource
【题目】关于 @Autowired 与 @Resource 两个注入注解,下列说法正确的是?
A. 两个注解的注入规则完全相同 B. @Autowired 默认按名称注入 C. @Resource 是 Spring 框架自带的注解 D. @Autowired 默认按类型注入,@Resource 默认按名称注入
答案:D
【考点】@Autowired(Spring)与 @Resource(JSR-250)的注入规则差异。
【结论】选 D。@Autowired 默认按类型注入,而 @Resource 默认先按名称注入,名称不匹配时再按类型注入。
【逐项辨析】 D. @Autowired 默认按类型注入,@Resource 默认按名称注入:正确。@Autowired 的核心匹配维度是 Type,当容器中存在多个同类型 Bean 时需配合 @Qualifier 指定名称;@Resource 的默认策略是 byName,若 name 属性未指定且按字段名找不到 Bean,则回退到 byType。 B. @Autowired 默认按名称注入:错误。@Autowired 是 byType 优先,只有配合 @Qualifier 或在构造器参数上使用 @Qualifier 时才显式按名称匹配。 C. @Resource 是 Spring 框架自带的注解:错误。@Resource 源自 JSR-250 规范(javax.annotation.Resource 或 jakarta.annotation.Resource),属于 Java 标准注解,Spring 仅提供兼容实现;@Autowired 才是 Spring 原生注解。 A. 两个注解的注入规则完全相同:错误。二者在来源规范、默认匹配维度、是否要求依赖必须存在(@Autowired required 默认 true,即依赖必须存在;@Resource 无此属性)等方面均存在差异。
【知识点】依赖注入(DI)是 Spring 框架的核心机制,@Autowired 和 @Resource 是两种最常用的注入注解,但二者在规范和实现上有本质区别。@Autowired 由 Spring 定义,支持按类型、按名称(配合 @Qualifier)、按优先级(@Primary)等多种匹配策略,可标注在构造器、Setter 方法、字段及方法参数上。Spring 4.3 后,若类只有一个构造器,可省略 @Autowired 自动注入。@Resource 由 JSR-250 定义,语义更接近 JNDI 查找,优先按 name 解析(默认取字段名或方法参数名),解析失败则回退到按类型解析。由于 @Resource 是标准注解,其可移植性更强,但功能相对简单,不支持 @Primary 或 required 等 Spring 特有语义。在实际开发中,Spring 官方推荐优先使用构造器注入(结合 @Autowired 或隐式注入),因为这样可以确保依赖不可变且不为 null,有利于单元测试和对象完整性。
【记忆锚点】“Auto 按类型找同类,Resource 先名后型记;一个 Spring 亲儿子,一个 Java 标准命。”
【易混对比】
| 对比维度 | @Autowired | @Resource |
|---|---|---|
| 来源规范 | Spring 框架原生 | JSR-250 / Jakarta 标准 |
| 默认匹配 | 按类型(byType) | 按名称(byName),失败回退 byType |
| 指定名称 | @Qualifier("name") | name = "name" |
| 是否必需 | required 默认 true(必须存在),可设 false | 无此属性,按名找不到再按类型,找不到则失败 |
| 支持位置 | 构造器、字段、方法、参数 | 字段、Setter 方法 |
| 是否采纳 @Primary | 支持(多个候选时取 @Primary) | 按名命中时用不上;名未命中回退按类型匹配的这一段仍会采纳 @Primary(实测 3 候选中带 @Primary 者被选中) |
换问法:若题目改为“当容器中存在两个同类型的 Bean 时,仅用 @Autowired 如何避免注入异常?”,则应回答:配合 @Qualifier("beanName") 指定具体 Bean,或在其中一个 Bean 上标注 @Primary。
【自测】以下代码能否成功注入?若不能,应如何修改?
@Service
public class OrderService {
@Autowired
private AlipayService alipayService;
@Autowired
private WechatPayService wechatPayService;
}
interface PayService { }
@Service class AlipayService implements PayService { }
@Service class WechatPayService implements PayService { }答:不能直接成功注入,因为容器中存在两个 PayService 类型实现,@Autowired 按类型匹配时会抛出 NoUniqueBeanDefinitionException。应将字段类型改为具体实现类(AlipayService / WechatPayService),或保留 PayService 类型并配合 @Qualifier("alipayService") / @Qualifier("wechatPayService") 指定 Bean 名称,也可在一个实现类上标注 @Primary。 与第 S18 题连考。
【知识关联】
- 同库关联:与 S16/S18;byType vs byName。
- 实现层:@Autowired 默认 byType,可 @Qualifier;@Resource 默认 byName(JSR-250)。
- 面试追问:① 多个实现如何选?② 构造器注入优势?
【拓展延伸】
- 变式问法:required=false;@Primary。
- 版本差异:Boot2.2+ 推荐构造器注入减少循环依赖。
- 工程注意点:优先构造器注入;字段注入不利测试。
S18 · 知识点:循环依赖
【题目】Spring 默认单例模式下,两个 Bean 通过构造器注入形成循环依赖,会发生什么?
A. 自动通过三级缓存解决 B. 静默跳过,注入 null C. 两个 Bean 各自创建成功但功能异常 D. 抛出 BeanCurrentlyInCreationException
答案:D
【考点】循环依赖的解决方案(三级缓存)及其适用边界。
【结论】选 D。Spring 默认单例模式下,构造器注入产生的循环依赖无法通过三级缓存解决,会直接抛出 BeanCurrentlyInCreationException。
【逐项辨析】 A. 自动通过三级缓存解决:错误。三级缓存机制仅适用于 setter 注入或字段注入,在对象实例化后即可提前暴露早期引用;构造器注入时对象尚未实例化,无法暴露引用。 D. 抛出 BeanCurrentlyInCreationException:正确。Spring 检测到构造器循环依赖时,会在创建 Bean 的过程中发现该 Bean 已处于“正在创建”状态,从而抛出此异常以中断死循环。 C. 两个 Bean 各自创建成功但功能异常:错误。Spring 不会允许存在构造器循环依赖的 Bean 成功创建,而是直接抛出异常阻止容器启动。 B. 静默跳过,注入 null:错误。Spring 对构造器循环依赖采取“快速失败”策略,绝不会静默注入 null,以免引入难以排查的空指针问题。
【知识点】Spring 解决循环依赖的核心机制是三级缓存,位于 DefaultSingletonBeanRegistry 中:(1)singletonObjects(一级缓存):存放完全初始化好的成品 Bean;(2)earlySingletonObjects(二级缓存):存放已实例化但尚未完成属性填充和初始化的早期引用;(3)singletonFactories(三级缓存):存放用于生成早期引用的 ObjectFactory。当 Bean 通过构造器创建时,实例化阶段需要传入所有依赖参数,而此时依赖的 Bean 尚未创建,导致无法完成实例化,更无法将自身提前暴露到三级缓存。相反,setter/字段注入在对象实例化(通过无参构造器)之后、属性填充之前即可通过 ObjectFactory 将早期引用暴露出去,供循环依赖方注入。对于必须使用的构造器注入场景,可通过 @Lazy 注解延迟注入其中一个依赖,Spring 会注入一个代理对象而非真实 Bean,从而打破循环。
【记忆锚点】“构造器循环死结牢,三级缓存救不了;setter 字段能暴露,@Lazy 代理来破套。”
【易混对比】
| 注入方式 | 是否支持循环依赖 | 解决机制 | 异常风险 |
|---|---|---|---|
| 构造器注入 | 不支持(默认) | 无,直接抛异常 | BeanCurrentlyInCreationException |
| Setter 注入 | 支持 | 三级缓存暴露早期引用 | 裸 Spring 默认允许;Spring Boot 2.6+ 默认禁止(实测 3.4.6 直接启动失败,root=BeanCurrentlyInCreationException,需 spring.main.allow-circular-references=true) |
| 字段注入 | 支持 | 同上 | 同上(裸 Spring 默认允许、Boot 2.6+ 默认禁止) |
| @Lazy + 构造器 | 支持 | 注入延迟加载代理对象 | 首次访问时才可能暴露问题 |
换问法:若题目改为“Spring 为什么要用三级缓存而不是两级缓存解决循环依赖?”,则应回答:三级缓存中的 ObjectFactory 是为了支持 AOP 代理,若直接放入早期引用对象,可能拿到的是未经代理的原始对象,破坏 AOP 语义;通过 ObjectFactory 可在需要时生成代理后的早期引用。
【自测】以下代码启动 Spring 容器时会发生什么?如何解决?
@Component
public class A {
private final B b;
public A(B b) { this.b = b; }
}
@Component
public class B {
private final A a;
public B(A a) { this.a = a; }
}答:容器启动时会抛出 BeanCurrentlyInCreationException,因为 A 和 B 均通过构造器注入形成循环依赖,且默认单例模式下无法提前暴露早期引用。解决方案有三种:(1)将其中一个改为 Setter 或字段注入;(2)在构造器参数上加 @Lazy,如 public A(@Lazy B b),让 Spring 注入 B 的延迟代理对象;(3)重新设计架构,引入中间层或事件机制解耦。 与第 S14 题连考。
【知识关联】
- 同库关联:与 S17;三级缓存解决 setter/字段循环依赖(非构造器)。
- 实现层:singletonObjects / earlySingletonObjects / singletonFactories。
- 面试追问:① 构造器循环依赖为何失败?② 如何打破?
【拓展延伸】
- 变式问法:@Lazy 打破;改设计拆分。
- 版本差异:Boot 版本对部分场景更严格,设计上避免循环。
- 工程注意点:循环依赖是设计坏味道;优先重构。
S19 · 知识点:定时任务
【题目】在 Spring Boot 中启用基于 cron 表达式的定时任务,需要以下哪个操作?
A. 启动类加 @EnableScheduling,方法加 @Scheduled(cron = "...") B. 启动类加 @EnableAsync,方法加 @Async C. 只需方法上加 @Scheduled,无需任何配置 D. 启动类加 @EnableCaching
答案:A
【考点】定时任务(@EnableScheduling + @Scheduled)与异步任务(@EnableAsync + @Async)的区分。
【结论】选 A。Spring Boot 中启用 cron 定时任务需在启动类标注 @EnableScheduling,并在任务方法上标注 @Scheduled(cron = "...")。
【逐项辨析】 A. 启动类加 @EnableScheduling,方法加 @Scheduled(cron = "..."):正确。这是 Spring 定时任务的标准两步配置:@EnableScheduling 负责向容器注册 ScheduledAnnotationBeanPostProcessor,@Scheduled 负责声明触发规则。 B. 启动类加 @EnableAsync,方法加 @Async:错误。@EnableAsync + @Async 是异步执行支持,用于将方法放入线程池异步执行,与定时触发无关。 C. 只需方法上加 @Scheduled,无需任何配置:错误。若缺少 @EnableScheduling,Spring 容器不会识别 @Scheduled 注解,任务不会被注册到调度器。 D. 启动类加 @EnableCaching:错误。@EnableCaching 是缓存抽象开关,用于开启基于注解的缓存管理(如 @Cacheable),与定时任务完全无关。
【知识点】Spring 的定时任务调度基于 TaskScheduler 接口,默认实现为 ThreadPoolTaskScheduler,内部依赖 JDK 的 ScheduledExecutorService。@EnableScheduling 注解通过 @Import(SchedulingConfiguration.class) 向容器注入 ScheduledAnnotationBeanPostProcessor,该后置处理器负责扫描 @Scheduled 注解并注册触发任务。@Scheduled 支持三种触发模式:cron(最灵活,兼容 Unix cron 语法)、fixedRate(固定频率,以上次开始时间为基准)、fixedDelay(固定延迟,以上次结束时间为基准)。Spring Boot 中可通过 spring.task.scheduling.* 配置线程池大小。需要特别注意的是,默认情况下定时任务是单线程串行执行的,若某个任务耗时超过间隔,会影响后续任务准时触发;可通过配置线程池或结合 @Async 实现并行执行。
【记忆锚点】“EnableScheduling 开开关,@Scheduled 定时间;cron 表达式最灵活,fixedRate 固定连。”
【易混对比】
| 注解组合 | 功能 | 核心接口/类 | 配置前缀 |
|---|---|---|---|
| @EnableScheduling + @Scheduled | 定时任务调度 | TaskScheduler、ScheduledTaskRegistrar | spring.task.scheduling.* |
| @EnableAsync + @Async | 异步方法执行 | AsyncTaskExecutor、ThreadPoolTaskExecutor | spring.task.execution.* |
| @EnableCaching + @Cacheable | 缓存管理 | CacheManager、Cache | spring.cache.* |
| @EnableTransactionManagement + @Transactional | 声明式事务 | PlatformTransactionManager | spring.transaction.* |
换问法:若题目改为“@Scheduled(fixedRate = 5000) 与 fixedDelay = 5000 的区别是什么?”,则应回答:fixedRate 以上次任务开始时间计算间隔,fixedDelay 以上次任务结束时间计算间隔;若任务执行耗时超过间隔,fixedRate 可能堆积,fixedDelay 保证休息间隔。
【自测】以下 cron 表达式表示每天凌晨两点执行,请判断是否正确:@Scheduled(cron = "0 0 2 * * ?")
答:正确。该 cron 表达式中,第一位 0 表示秒(第 0 秒),第二位 0 表示分(第 0 分),第三位 2 表示时(凌晨 2 点),* 表示每天,? 表示不指定星期(与日期互斥)。因此该配置确实表示每天凌晨 2:00:00 执行。 与第 S20 题连考。
【知识关联】
- 同库关联:与 S16;@Scheduled / @EnableScheduling。
- 实现层:默认单线程调度器;fixedRate/fixedDelay/cron。
- 面试追问:① fixedRate 与 fixedDelay?② 多实例重复执行?
【拓展延伸】
- 变式问法:自定义线程池;分布式锁/xxl-job。
- 版本差异:注解稳定。
- 工程注意点:生产环境用分布式调度;注意时区;任务幂等。
S20 · 知识点:Actuator 健康检查
【题目】引入 spring-boot-starter-actuator 后,应用健康检查端点的默认访问路径是?
A. /actuator/health B. /health C. /actuator/actuator D. /monitor
答案:A
【考点】Actuator 端点体系与默认 Web 路径。
【结论】选 A。引入 spring-boot-starter-actuator 后,健康检查端点的默认访问路径为 /actuator/health。
【逐项辨析】 A. /actuator/health:正确。Spring Boot Actuator 的 Web 端点默认以 /actuator 为 base-path,health 端点映射在其下,返回 UP 或 DOWN 状态。 B. /health:错误。/health 是 Spring Boot 1.x 的默认路径,从 2.0 起已改为 /actuator/health。 C. /actuator/actuator:错误。该路径不符合 Actuator 端点映射规则,属于干扰项。 D. /monitor:错误。/monitor 并非 Spring Boot 内置端点,可能是某些第三方监控框架自定义的路径。
【知识点】Spring Boot Actuator 提供生产级的应用监控和管理端点,其核心架构由 Endpoint、WebEndpointExtension、EndpointFilter 等组成。健康检查端点(HealthEndpoint)聚合所有注册的 HealthIndicator,如 DataSourceHealthIndicator、DiskSpaceHealthIndicator、RabbitHealthIndicator 等,最终返回一个综合状态(Status)。在 Spring Boot 2.x 中,management.endpoints.web.base-path 默认为 /actuator,health 与 info 端点默认暴露;而从 Spring Boot 3.0 起,出于安全考虑,默认仅暴露 health 端点,info 端点也需显式配置 management.endpoints.web.exposure.include。该端点被广泛用于 Kubernetes Liveness/Readiness 探针、Spring Boot Admin、Prometheus 配套告警等场景。通过实现 HealthIndicator 接口,开发者可自定义业务级健康检查逻辑。
【记忆锚点】“Actuator 看健康,/actuator/health 是默认;UP 表示活,DOWN 就得查。”
【易混对比】
| 版本/配置 | 默认 base-path | 默认暴露端点 | 安全变化 |
|---|---|---|---|
| Spring Boot 1.5 | / | health、info 等多数 | 无特殊限制 |
| Spring Boot 2.x | /actuator | health、info(未在本机实测,本机只有 3.4.6) | 其余需显式暴露 |
| Spring Boot 3.0+ | /actuator | health(info 不再默认) | 默认更严格,需显式 include |
换问法:若题目改为“如何自定义 Actuator 健康检查的详细程度?”,则应回答:通过 management.endpoint.health.show-details 配置,可设置为 never(默认)、when-authorized 或 always,控制返回 JSON 中是否包含各 HealthIndicator 的详细明细。
【自测】某应用引入 Actuator 后访问 /actuator/info 返回 404,可能的原因是什么?如何解决?
答:可能原因有两个:(1)使用的是 Spring Boot 3.0+,info 端点默认不再暴露,需配置 management.endpoints.web.exposure.include=health,info;(2)未在 application.properties/yml 中配置 info.* 属性,导致端点无内容返回(但不会 404,会返回空 JSON)。若因未暴露导致 404,添加 exposure.include 配置即可。 与第 S19 题连考。
【知识关联】
- 同库关联:与 S08/S29;actuator 端点与健康检查。
- 实现层:HealthIndicator;暴露端点需配置 management.endpoints。
- 面试追问:① 如何暴露 health?② 安全如何控?
【拓展延伸】
- 变式问法:K8s readiness/liveness 映射。
- 版本差异:Boot2 端点安全模型变化;3.x 持续演进。
- 工程注意点:生产只暴露必要端点;加认证;与监控告警打通。