Skip to content

三、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 本类接口代理)再调用目标方法。

【自测】以下代码中事务是否会回滚?为什么?

java
@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 方法输出的日志顺序是什么?

java
@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 实例各有几个?

java
@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。

【自测】以下代码能否成功注入?若不能,应如何修改?

java
@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 容器时会发生什么?如何解决?

java
@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、ScheduledTaskRegistrarspring.task.scheduling.*
@EnableAsync + @Async异步方法执行AsyncTaskExecutor、ThreadPoolTaskExecutorspring.task.execution.*
@EnableCaching + @Cacheable缓存管理CacheManager、Cachespring.cache.*
@EnableTransactionManagement + @Transactional声明式事务PlatformTransactionManagerspring.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/actuatorhealth、info(未在本机实测,本机只有 3.4.6)其余需显式暴露
Spring Boot 3.0+/actuatorhealth(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 持续演进。
  • 工程注意点:生产只暴露必要端点;加认证;与监控告警打通。

持续学习,持续积累。