Skip to content

Day14 Spring 核心:IOC、AOP、Bean 生命周期、循环依赖 ​

📖 基础知识讲解【基础原理 + 高级深挖知识点】 ​

基础原理 ​

1. 全景:没有 Spring 的世界长什么样 ​

面试问「Spring 解决了什么问题」,背「解耦」三个字是不够的。真正要讲清的是:手动 new 对象时,到底出现了哪三类无法消除的痛点。

text
【无 Spring 时代】
UserController
   │  new UserServiceImpl()            ← 痛点1:编译期写死实现类,换实现要改代码
   ▼
UserServiceImpl
   │  new UserDaoImpl(new DataSource(url, user, pwd))
   ▼                                   ← 痛点2:依赖链越深,装配代码越长
UserDaoImpl
   │  手写 try/catch + conn.commit() + conn.rollback()
   ▼                                   ← 痛点3:事务/日志/权限代码散落在每个方法里

【有 Spring 时代】
          Spring 容器(IOC Container)
                │ 读配置 → 生成 BeanDefinition → 实例化 → 注入 → 初始化
                ▼
   ┌ 负责"造对象 + 连对象":IOC / DI 解决痛点 1、2
   └ 负责"给对象加能力":AOP 解决痛点 3
                │
                ▼
UserController ← 注入 ← UserService 代理 ← 注入 ← UserDao

一句话总结:Spring 核心就两件事 —— IOC 管对象的"生与连",AOP 管对象的"增与强"。Bean 生命周期是 IOC 的执行细节,循环依赖是 IOC 的疑难杂症,事务是 AOP 最典型的落地产物。

为什么面试必考这四块? 因为它们串成一条完整因果链:

知识点在链路中的位置不懂它会在线上踩什么坑
IOC / DI对象从哪来、怎么连上注入了接口的错误实现、@Autowired 歧义启动失败
Bean 生命周期对象从创建到可用中间经过哪些钩子在构造器里用注入的字段 → NPE;初始化顺序错乱
AOP对象被容器悄悄换成了代理自调用事务失效、切面不生效、代理类型转换异常
循环依赖IOC 装配顺序打结时容器怎么自救Spring Boot 2.6+ 启动直接报 BeanCurrentlyInCreationException

2. IOC 控制反转:到底"反转"了什么 ​

很多人把 IOC 和 DI 混为一谈。准确说法是:IOC 是一种设计思想(目标),DI 是实现这种思想的手段。

📌 单独澄清:反转的是"控制权",不是"创建动作"

对象终究还是要被 new 出来的,Spring 底层也是走反射调构造器。IOC 反转的是**"谁来决定什么时候 new、new 哪个实现、new 完连给谁"**这个决策权。

维度传统方式(控制权在业务代码)IOC 方式(控制权在容器)
谁决定创建时机业务代码执行到 new 那一行容器启动阶段(单例)或 getBean 时(多例)
谁决定用哪个实现类代码里写死 new UserServiceImpl()配置 / 注解 / 条件装配决定
谁负责装配依赖调用方手动一层层传进去容器递归解析依赖图自动注入
换实现的成本改源码 + 重新编译改配置或换一个 @Service 实现
java
// ❌ 控制权在自己手上:UserController 必须"认识"具体实现类
public class UserController {
    // 编译期依赖 UserServiceImpl,换成 UserServiceMockImpl 就得改这一行
    private UserService userService = new UserServiceImpl(new UserDaoImpl());
}

// ✅ 控制权交给容器:UserController 只声明"我需要一个 UserService"
@RestController
public class UserController {
    private final UserService userService;           // 只依赖接口
    public UserController(UserService userService) { // 容器在实例化时把实现塞进来
        this.userService = userService;
    }
}

DI 依赖注入的三种形态(下一节细讲差异):

text
构造器注入  → 依赖作为构造参数传入,对象"出生即完整"
setter 注入 → 先无参造出对象,再调 setXxx() 填充
字段注入    → 直接反射写字段(@Autowired 打在 field 上),无需 setter

DL 依赖查找(Dependency Lookup)也属于 IOC:主动调 context.getBean(XxxService.class) 去"捞"对象。它和 DI 的区别是——DI 是"等别人喂",DL 是"自己伸手拿"。业务代码优先用 DI,只有在工厂/策略路由这类运行时才知道类名的场景才用 DL。

3. 容器体系:BeanFactory、ApplicationContext、FactoryBean ​

这三个名字长得像,是面试故意设的坑,必须一次分清。

text
BeanFactory(IOC 容器的顶层接口,只定义"能取 Bean"这件事)
   │
   ├─ ListableBeanFactory      → 能按类型批量取:getBeansOfType()
   ├─ HierarchicalBeanFactory  → 支持父子容器:getParentBeanFactory()
   ├─ AutowireCapableBeanFactory → 能给"容器外的对象"做依赖注入
   │
   └─ ApplicationContext(子接口,在 BeanFactory 之上叠企业级能力)
         ├─ MessageSource              → 国际化
         ├─ ApplicationEventPublisher  → 事件发布
         ├─ ResourcePatternResolver    → 资源加载(classpath*: 通配)
         └─ EnvironmentCapable         → 环境 / Profile / 配置属性
              │
              ├─ ClassPathXmlApplicationContext      (XML 配置)
              ├─ AnnotationConfigApplicationContext  (注解配置)
              └─ AnnotationConfigServletWebServerApplicationContext(Spring Boot Web)
对比项BeanFactoryApplicationContext
定位基础 IOC 容器,功能最小集BeanFactory 的超集,面向应用的"完整容器"
加载策略懒加载,getBean() 时才实例化启动时 refresh() 预实例化所有非懒加载单例
扩展点注册需手动调用注册 BeanPostProcessor自动发现并注册所有 BeanPostProcessor
附加能力无国际化、事件、资源、Environment、AOP 自动代理
错误暴露时机运行时取 Bean 才报错启动即报错(配置写错当场挂,线上更安全)
实际使用几乎只作为内部引擎(DefaultListableBeanFactory)业务开发实际接触的就是它

关键认知:ApplicationContext 并不自己存 Bean,它内部组合了一个 DefaultListableBeanFactory 做真正的仓库,自己只做"门面 + 增强"。所以说 ApplicationContext「继承 BeanFactory 接口、组合 BeanFactory 实现」。

📌 单独澄清:FactoryBean 和 BeanFactory 完全是两回事

维度BeanFactoryFactoryBean
它是什么容器本身容器里的一个普通 Bean,但它是个"生产其他 Bean 的工厂"
解决什么问题统一管理所有 Bean某个 Bean 创建过程很复杂(需大量逻辑),交给工厂定制
取值行为getBean("a") 返回 agetBean("myFb") 返回 getObject() 的结果;加 & 前缀 getBean("&myFb") 才返回工厂自己
典型使用者Spring 内核MyBatis 的 MapperFactoryBean、Spring 的 ProxyFactoryBean
java
// FactoryBean 实战:MyBatis 的 Mapper 接口没有实现类,靠 FactoryBean 产出代理
@Component("userMapperFactory")
public class UserMapperFactoryBean implements FactoryBean<UserMapper> {
    @Override
    public UserMapper getObject() {
        // 这里可以写任意复杂的创建逻辑:动态代理、读远端配置、按环境切换实现
        return (UserMapper) Proxy.newProxyInstance(
                getClass().getClassLoader(),
                new Class[]{UserMapper.class},
                (proxy, method, args) -> executeSql(method, args));
    }
    @Override
    public Class<?> getObjectType() { return UserMapper.class; }
    @Override
    public boolean isSingleton() { return true; }
    private Object executeSql(Method m, Object[] args) { /* 省略 */ return null; }
}

// 使用方感知不到工厂的存在,拿到的直接是 UserMapper
@Autowired private UserMapper userMapper;              // ✅ 实际是 getObject() 的返回值
@Autowired @Qualifier("&userMapperFactory")
private FactoryBean<UserMapper> rawFactory;            // ✅ 加 & 才拿到工厂本体

4. BeanDefinition:Bean 在容器里的"身份证" ​

Spring 不是直接"读到注解就造对象",中间一定有一层元数据描述。这层就是 BeanDefinition,它是理解 Spring 启动流程的关键抽象。

text
配置来源(三种殊途同归)
   ├─ XML: <bean id="userService" class="..."/>
   ├─ 注解:@Component / @Service / @Bean
   └─ 编程式:registry.registerBeanDefinition(...)
          │
          ▼  各类 BeanDefinitionReader / ClassPathBeanDefinitionScanner 解析
   BeanDefinition(一个 Bean 的"配置清单")
          ├─ beanClass        要造哪个类
          ├─ scope            singleton / prototype / request...
          ├─ lazyInit         是否懒加载
          ├─ primary          是否为同类型首选
          ├─ dependsOn        必须先造好哪些 Bean
          ├─ initMethodName / destroyMethodName
          ├─ constructorArgumentValues  构造参数
          └─ propertyValues   属性值
          │
          ▼  注册进 beanDefinitionMap(Map<String, BeanDefinition>)
   DefaultListableBeanFactory
          │
          ▼  finishBeanFactoryInitialization() 遍历,按清单造对象
   真正的 Bean 实例 → singletonObjects(单例池)

记住这条分界线:BeanDefinition 注册完成 ≠ Bean 创建完成。BeanFactoryPostProcessor 作用在"清单阶段"(还能改配置),BeanPostProcessor 作用在"实例阶段"(只能改对象)。这是高级深挖里最常考的区分点。

java
// 编程式注册 BeanDefinition:Spring Boot 很多自动配置就是这么干的
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata meta,
                                        BeanDefinitionRegistry registry) {
        AbstractBeanDefinition bd = BeanDefinitionBuilder
                .genericBeanDefinition(UserServiceImpl.class)
                .setScope(BeanDefinition.SCOPE_SINGLETON)
                .setLazyInit(false)
                .addPropertyValue("timeout", 3000)
                .getBeanDefinition();
        registry.registerBeanDefinition("userService", bd);
    }
}

5. 依赖注入三种方式:为什么官方只推荐构造器注入 ​

注入方式写法能否 final循环依赖单测友好度Spring 官方态度
构造器注入构造参数(单构造器可省 @Autowired)✅ 可以❌ 无法解决,启动报错✅ 直接 new 传 mock强烈推荐
setter 注入@Autowired 打在 setXxx() 上❌ 不行✅ 三级缓存可解✅ 可手动 set可选依赖时推荐
字段注入@Autowired 直接打在字段上❌ 不行✅ 三级缓存可解❌ 需反射或 Spring 上下文不推荐(IDEA 会警告)
java
// ✅ 推荐:构造器注入 —— 依赖不可变、不可为 null、易测试
@Service
public class OrderService {
    private final UserService userService;
    private final StockService stockService;
    // Spring 4.3+ 只有一个构造器时可以不写 @Autowired
    public OrderService(UserService userService, StockService stockService) {
        this.userService = userService;
        this.stockService = stockService;
    }
}

//❌ 不推荐:字段注入 —— 三个隐患
@Service
public class OrderServiceBad {
    @Autowired private UserService userService;  // 1. 不能 final,运行期可被反射改
    @Autowired private StockService stockService;// 2. 脱离 Spring 无法 new 出可用对象
    // 3. 依赖可以无限加而代码看不出"这个类太重了",掩盖了设计问题
}

📌 单独澄清:构造器注入"解决不了"循环依赖,恰恰是它的优点

问题字段 / setter 注入构造器注入
遇到循环依赖容器用三级缓存悄悄兜住,程序能跑起来启动直接失败并打印依赖环
后果环一直在,某天加个 AOP / @Async 就爆炸第一天就被迫重新设计,环被消灭在源头
口诀"能跑但有病""跑不起来但健康"
java
// ❌ 这种环在字段注入下能启动成功,但本质是设计问题
@Service class A { @Autowired B b; }
@Service class B { @Autowired A a; }

// ✅ 正确做法不是换注入方式,而是抽出公共逻辑打破环
@Service class A { private final C c; A(C c){ this.c = c; } }
@Service class B { private final C c; B(C c){ this.c = c; } }
@Service class C { /* A、B 共用的那部分能力放这里 */ }

6. 自动装配与歧义解决:一个接口多个实现怎么办 ​

容器按类型找 Bean 时,如果找到多个候选者,会抛 NoUniqueBeanDefinitionException。解决手段有五种,优先级要记清。

text
@Autowired 解析流程
  1. 按类型 byType 找候选集合
  2. 候选只有 1 个  → 直接注入
  3. 候选 > 1 个    → 依次尝试:
        ① @Primary 标记的候选
        ② @Priority(数字小的优先)
        ③ 字段名 / 参数名 与 beanName 匹配(byName 兜底)
        ④ 都失败 → 抛 NoUniqueBeanDefinitionException
  4. 候选 0 个       → required=true 抛 NoSuchBeanDefinitionException
                      required=false 或 Optional<T> 注入 null / empty
手段作用适用场景
@Primary标记默认实现有一个明确的"主力实现"
@Qualifier("beanName")注入点指定具体 Bean不同调用方要不同实现
字段名与 beanName 一致利用 byName 兜底临时解法,可读性差,不推荐依赖
@Resource(name = "xxx")JSR-250 注解,默认按名称遗留代码常见
注入 List<T> / Map<String,T>一次性拿到全部实现策略模式 / 责任链
java
public interface PayService { String channel(); void pay(long amount); }

@Service("aliPay") @Primary
class AliPayService implements PayService { public String channel(){ return "ALI"; } ... }

@Service("wxPay")
class WxPayService implements PayService { public String channel(){ return "WX"; } ... }

// ✅ 场景1:要默认实现 → 直接注入,@Primary 生效,拿到 aliPay
@Service
class OrderA { private final PayService payService;
    OrderA(PayService payService){ this.payService = payService; } }

// ✅ 场景2:要指定实现 → @Qualifier 精确点名
@Service
class OrderB { private final PayService payService;
    OrderB(@Qualifier("wxPay") PayService payService){ this.payService = payService; } }

// ✅ 场景3:策略路由 → 注入 Map,key 自动是 beanName,彻底消灭 if-else
@Service
class PayRouter {
    private final Map<String, PayService> strategies;   // {aliPay=..., wxPay=...}
    PayRouter(Map<String, PayService> strategies){ this.strategies = strategies; }
    void pay(String channel, long amount) {
        strategies.values().stream()
                .filter(s -> s.channel().equals(channel))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("未知渠道"))
                .pay(amount);
    }
}

@Autowired vs @Resource 必考对比

维度@Autowired@Resource
来源Spring 自有注解JSR-250 标准(JDK 自带)
默认装配方式byType,歧义时才退化 byNamebyName,找不到名字再 byType
指定名称配合 @Qualifiername 属性直接写
支持 required有 required = false无此属性
可用位置构造器 / setter / 字段 / 方法参数字段 / setter(不支持构造器)
处理器AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor

7. AOP 基础:七大术语 + 五种通知 + 执行顺序 ​

AOP 的术语密度极高,背散装定义很容易混。把它们放进一次方法调用的时间轴上就清楚了。

text
目标对象 TargetObject:UserService 真身(被增强的那个对象)
   │
   ▼  容器用 Weaving 织入,产出 →
代理对象 Proxy:UserService$$Proxy(调用方实际拿到的东西)
   │
   ▼  调用 proxy.createUser()
JoinPoint 连接点:程序执行中"可以被拦截"的那个点(Spring AOP 里只有"方法调用")
   │
   ▼  Pointcut 切点:一个表达式,筛选出"我要拦哪些 JoinPoint"
Advice 通知:筛中以后"做什么"(下面五种)
   │
Aspect 切面 = Pointcut(拦哪里)+ Advice(做什么),打包成一个类
   │
Introduction 引介:给目标类动态加新接口/新方法(极少用)
通知类型注解执行时机能否阻止目标方法执行能否改返回值
前置通知@Before目标方法执行前❌(只能抛异常中断)❌
后置返回通知@AfterReturning目标方法正常返回后❌❌(只能读)
异常通知@AfterThrowing目标方法抛异常后❌❌
最终通知@After无论正常还是异常,一定执行❌❌
环绕通知@Around包裹整个调用,前后都能插代码✅ 不调 proceed() 即可✅

单切面执行顺序(Spring 5.2.7 之后已修正为符合直觉的顺序):

text
进入代理方法
  ├─ @Around 前半段(proceed() 之前的代码)
  ├─ @Before
  ├─── 执行目标方法 ───┐
  │                    ├─ 正常返回 → @AfterReturning
  │                    └─ 抛出异常 → @AfterThrowing
  ├─ @After(finally 语义,必执行)
  └─ @Around 后半段(proceed() 之后的代码)
退出代理方法

多切面顺序:用 @Order 控制,数字小的"外层",形成责任链。

text
@Order(1) 日志切面   ── 进 ──┐
  @Order(2) 事务切面 ── 进 ──┐│
    @Order(3) 缓存切面 ─ 进 ─┐││
      目标方法                │││
    缓存切面 ── 出 ───────────┘││
  事务切面 ── 出 ─────────────┘│
日志切面 ── 出 ────────────────┘

切点表达式常用写法(面试会让你现场写):

java
// execution:最常用,按方法签名匹配
@Pointcut("execution(public * com.xxx.service..*.*(..))")   // service 包及子包所有公有方法
@Pointcut("execution(* com.xxx.service.UserService.get*(..))") // 以 get 开头的方法

// @annotation:按方法上的注解匹配(自定义注解切面的标配)
@Pointcut("@annotation(com.xxx.annotation.TimeLog)")

// within / @within:按类匹配 / 按类上的注解匹配
@Pointcut("within(com.xxx.service..*)")
@Pointcut("@within(org.springframework.stereotype.Service)")

// args / @args:按参数类型 / 参数类上的注解匹配
@Pointcut("args(java.lang.Long)")

// 组合:&& || !
@Pointcut("execution(* com.xxx.service..*(..)) && !@annotation(com.xxx.annotation.NoLog)")

8. 动态代理:JDK Proxy 与 CGLIB 的真实差异 ​

Spring AOP 的本质是运行期动态代理,这是和 AspectJ(编译期/类加载期织入)最大的区别。

text
Bean 创建到一半(属性填充 + 初始化完成)
        │
        ▼ AbstractAutoProxyCreator#postProcessAfterInitialization
   判断这个 Bean 有没有匹配的 Advisor(切面)
        ├─ 没有 → 原样返回真身
        └─ 有   → 进入 DefaultAopProxyFactory#createAopProxy
                     ├─ proxyTargetClass=true           → CGLIB
                     ├─ 目标类没有实现任何接口           → CGLIB
                     ├─ 目标本身就是接口 / Proxy 类      → JDK
                     └─ 其余(实现了接口且未强制 CGLIB)  → JDK
        │
        ▼
   放进单例池的是【代理对象】,不是原始对象
对比项JDK 动态代理CGLIB
实现原理运行期生成实现了目标接口的 $Proxy0 类ASM 字节码生成目标类的子类并覆写方法
前提条件目标类必须实现接口目标类不能是 final,方法不能 final/private/static
核心 APIProxy.newProxyInstance + InvocationHandlerEnhancer + MethodInterceptor
调用性能反射调用,JDK8+ 后已很接近早期更快(FastClass 索引直调),现代 JDK 差距很小
创建性能更快(无需生成完整字节码)更慢(要生成子类)
能代理的方法仅接口中声明的方法所有可覆写的 public / protected 方法
构造器要求无需要能被调用(Spring 用 Objenesis 绕过)
Spring Boot 默认—默认 proxyTargetClass=true,统一用 CGLIB

为什么 Spring Boot 默认改成 CGLIB? 因为 JDK 代理只暴露接口方法,常出现「注入时按实现类声明 → 类型转换失败」。统一 CGLIB 后 @Autowired UserServiceImpl 也能注入,减少踩坑。代价是不能代理 final 类/方法。

📌 单独澄清:Spring AOP 和 AspectJ 不是一回事

维度Spring AOPAspectJ
织入时机运行期生成代理对象编译期(ajc)或类加载期(LTW)改字节码
拦截粒度只能拦方法调用(且必须走代理)方法、构造器、字段读写、静态块都能拦
自调用❌ 失效(this 不走代理)✅ 生效(字节码已被改写)
依赖只借用 AspectJ 的注解和表达式需要专门的编译器 / Java Agent
性能每次调用有代理开销零运行时开销

关键认知:项目里 @Aspect、@Around、execution(...) 这些注解和语法来自 AspectJ,但执行引擎是 Spring 自己的动态代理。所以才有「用了 AspectJ 注解,却还是自调用失效」这个经典困惑。

9. Bean 生命周期:完整链路逐步拆解 ​

这是 Spring 面试的"终极大题"。能把下面这条链路背全并说清每步为什么存在,基本就稳了。

text
① 实例化前置拦截   InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation
      └─ 若返回非 null,直接短路(AOP 的 TargetSource、Mock 框架会用)
② 实例化 createBeanInstance
      └─ 推断构造器(@Autowired 构造器 / 唯一有参构造器 / 无参构造器)→ 反射 newInstance
      └─ 此时对象已在堆上,但所有字段都是默认值(null / 0)
③ 合并 BeanDefinition  MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition
      └─ 扫描 @Autowired / @Resource / @Value 元数据并缓存
④ 提前暴露引用 addSingletonFactory
      └─ 把"半成品"包成 ObjectFactory 丢进三级缓存,为循环依赖做准备
⑤ 属性填充 populateBean
      ├─ postProcessAfterInstantiation(返回 false 可跳过填充)
      └─ AutowiredAnnotationBeanPostProcessor 反射注入依赖
⑥ Aware 回调 invokeAwareMethods
      ├─ BeanNameAware#setBeanName
      ├─ BeanClassLoaderAware#setBeanClassLoader
      └─ BeanFactoryAware#setBeanFactory
⑦ 初始化前 BeanPostProcessor#postProcessBeforeInitialization
      ├─ ApplicationContextAwareProcessor → 补齐 ApplicationContextAware 等回调
      └─ CommonAnnotationBeanPostProcessor → 执行 @PostConstruct
⑧ 初始化
      ├─ InitializingBean#afterPropertiesSet
      └─ 自定义 init-method / @Bean(initMethod = "xxx")
⑨ 初始化后 BeanPostProcessor#postProcessAfterInitialization
      └─ ★ AOP 代理在这里产生:AnnotationAwareAspectJAutoProxyCreator 返回代理对象
⑩ 注册销毁回调 registerDisposableBean(仅 singleton)
⑪ 放入一级缓存 singletonObjects,Bean 可用
⑫ 容器关闭
      ├─ @PreDestroy
      ├─ DisposableBean#destroy
      └─ 自定义 destroy-method

三组初始化钩子的执行顺序与取舍:

钩子归属顺序优点缺点
@PostConstructJSR-250 标准1无侵入、与 Spring 解耦依赖 javax/jakarta.annotation(JDK11+ 需引包)
InitializingBean#afterPropertiesSetSpring 接口2无反射,性能略高业务类被迫实现 Spring 接口
init-method / @Bean(initMethod)配置声明3完全解耦,适合第三方类方法名字符串,重构不安全

销毁顺序对称:@PreDestroy → DisposableBean#destroy → destroy-method。

📌 单独澄清:构造器里拿不到注入的字段,是"时序"而不是"bug"

java
@Service
public class ReportService {
    @Autowired private UserDao userDao;

    // ❌ 构造器在"②实例化"阶段执行,此时"⑤属性填充"还没发生
    public ReportService() {
        userDao.count();   // NPE!userDao 还是 null
    }

    // ✅ 方案 A:构造器注入,依赖随构造参数一起进来
    // ✅ 方案 B:把初始化逻辑挪到 @PostConstruct(⑦ 阶段,字段已注入完毕)
    @PostConstruct
    public void init() {
        log.info("当前用户数 = {}", userDao.count());  // 安全
    }
}

判断口诀:问自己一句「这段代码跑在第几步?」——构造器在第 ②步,字段注入在第 ⑤步,@PostConstruct 在第 ⑦步。任何依赖其他 Bean 的初始化逻辑,都不能早于第 ⑤步。

10. Bean 作用域与单例的线程安全 ​

作用域生命周期容器是否管销毁典型用途
singleton容器启动创建,容器关闭销毁(默认)✅无状态 Service / DAO / 工具类
prototype每次 getBean 新建❌ 容器不管销毁有状态对象、线程不安全对象
request一次 HTTP 请求✅请求级上下文(Web 环境)
session一个 HTTP 会话✅用户会话数据
application整个 ServletContext✅全局共享配置
websocket一次 WebSocket 会话✅连接级状态

📌 单独澄清:单例 Bean 线程不安全的根源是"实例变量",不是"单例"本身

场景线程安全?原因
单例 Bean + 只有方法局部变量✅ 安全局部变量在各线程自己的虚拟机栈上,天然隔离
单例 Bean + 可变实例字段❌ 不安全所有线程共享同一个对象的同一块堆内存
单例 Bean + 不可变字段(final 且引用的对象不可变)✅ 安全没有写操作就没有竞态
单例 Bean + ThreadLocal 字段✅ 安全每个线程读写自己的副本(注意要 remove)
java
// ❌ 典型翻车:把请求级状态塞进单例 Bean 的实例字段
@Service
public class BadOrderService {
    private Long currentUserId;              // 多线程互相覆盖
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // SDF 本身非线程安全

    public void handle(Long userId) {
        this.currentUserId = userId;         // 线程 A 刚写完,线程 B 就覆盖了
        doSomething();                       // doSomething 读到的可能是 B 的 userId
    }
}

// ✅ 正确:状态随参数传递;非线程安全工具改用线程安全版本或局部创建
@Service
public class GoodOrderService {
    private static final DateTimeFormatter FMT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd");   // 不可变,线程安全

    public void handle(Long userId) {
        doSomething(userId);                 // 状态走参数,活在调用线程的栈帧里
    }
}

📌 prototype 的两个经典陷阱

java
// 陷阱 1:容器不管 prototype 的销毁回调
@Scope("prototype")
@Component
public class Conn {
    @PreDestroy public void close(){ /* 永远不会被容器调用! */ }
}
// → 需要自己 try-with-resources 或手动调 beanFactory.destroyBean(obj)

// 陷阱 2:单例里注入 prototype,拿到的永远是同一个实例
@Service
public class SingletonHolder {
    @Autowired private Conn conn;           // ❌ 只在创建 SingletonHolder 时注入一次

    // ✅ 解法 A:注入 ObjectProvider,每次调用现取
    @Autowired private ObjectProvider<Conn> connProvider;
    public void work(){ Conn c = connProvider.getObject(); }

    // ✅ 解法 B:@Lookup 方法注入,容器会重写该方法
    @Lookup public Conn createConn(){ return null; }   // 方法体会被 CGLIB 覆盖

    // ✅ 解法 C:给 prototype Bean 加 @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
}

11. 循环依赖与三级缓存 ​

什么是循环依赖:A 依赖 B,B 又依赖 A(也可能是 A→B→C→A 的长环,甚至 A→A 的自依赖)。容器创建 A 时要先造 B,造 B 时又要回头拿 A,形成死锁式递归。

哪些能解、哪些不能解:

场景能否解决原因
单例 + setter/字段注入✅实例化与属性填充分离,可提前暴露半成品
单例 + 构造器注入❌实例化阶段就要依赖对象,没有"半成品"可暴露
单例 + 构造器注入 + @Lazy✅@Lazy 注入的是代理,真正调用时才去容器取真身
prototype 作用域❌多例不进缓存,无从暴露;直接抛 BeanCurrentlyInCreationException
@Async / 部分 AOP 场景⚠️ 部分失败代理在初始化后才生成,早期暴露的引用与最终代理不一致 → 抛错
Spring Boot 2.6+ 默认配置❌ 默认禁止需显式开 spring.main.allow-circular-references=true

三级缓存结构(都在 DefaultSingletonBeanRegistry 里):

缓存级别类型存什么
singletonObjects一级Map<String, Object>完整可用的单例 Bean
earlySingletonObjects二级Map<String, Object>提前暴露的半成品(可能已是代理)
singletonFactories三级Map<String, ObjectFactory<?>>生产半成品的工厂(决定给真身还是代理)

查找顺序永远是:一级 → 二级 → 三级(getSingleton 方法里逐级下探,三级命中后把结果升到二级并移除三级)。

完整推演:A ⇄ B 字段注入

text
getBean(A)
 ├ 一/二/三级缓存都没有 A
 ├ 标记 A 为"创建中":singletonsCurrentlyInCreation.add("a")
 ├ ② 实例化 A(堆上有了 a 对象,字段 b = null)
 ├ ④ 把 ObjectFactory 放进三级缓存:singletonFactories["a"] = () -> getEarlyBeanReference(a)
 └ ⑤ 填充 A 的属性,发现需要 B → getBean(B)
       ├ 缓存都没有 B
       ├ 标记 B 为"创建中"
       ├ ② 实例化 B(字段 a = null)
       ├ ④ singletonFactories["b"] = () -> getEarlyBeanReference(b)
       └ ⑤ 填充 B 的属性,发现需要 A → getBean(A)
             ├ 一级没有 → 二级没有 → 三级命中!
             ├ 执行工厂 getEarlyBeanReference(a)
             │    ├ 若 A 需要 AOP → 此刻提前创建代理,返回 proxyA
             │    └ 否则直接返回原始 a
             ├ 把结果放入二级缓存,从三级移除(保证多次取到同一个对象)
             └ 返回半成品 A 给 B
       ├ B 拿到 A 的引用,属性填充完成
       ├ ⑧⑨ B 初始化 + 可能的 AOP 代理
       └ ⑪ B 完整,放入一级缓存,清掉二三级
 ├ A 拿到完整的 B,属性填充完成
 ├ ⑧⑨ A 初始化;若之前已提前代理过,这里直接复用那个 proxyA(不会再造一个)
 └ ⑪ A 完整,放入一级缓存

📌 单独澄清:为什么必须是三级,两级不够吗?

这是本节最高频的追问。关键在于**"要不要代理"这个决定必须延迟到真正有人来取的那一刻**。

方案做法问题
只要一级半成品和成品都放 singletonObjects别人可能拿到字段还没填完的 Bean,无法区分完整与否
两级实例化后立即创建代理放进二级缓存所有 Bean 不管有没有循环依赖,都被迫提前代理,破坏了"AOP 在初始化后织入"的生命周期约定
三级(现方案)只放工厂,有人来取时才决定返回真身还是代理无循环依赖时工厂永远不被调用,零额外开销;有循环依赖时才提前代理
java
// 三级缓存里放的工厂,本质是这一行(AbstractAutowireCapableBeanFactory)
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

// getEarlyBeanReference 的核心逻辑
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof SmartInstantiationAwareBeanPostProcessor sibp) {
            // AOP 的 AbstractAutoProxyCreator 在这里提前把代理造出来
            exposedObject = sibp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;   // 没有切面匹配时,原样返回真身
}

那二级缓存是干嘛的? 两个作用,缺一不可:

  1. 保证"同一性":如果只有一三级,B 和 C 都依赖 A 时,工厂会被调用两次 → 产生两个不同的代理对象,单例语义被破坏。命中三级后立刻升到二级,后续全部走二级,保证只有一个。
  2. 避免重复执行工厂逻辑:代理创建是有成本的,没必要重复做。

一句话背诵版:一级存成品、二级存半成品、三级存工厂;三级解决"代理对象的提前生成",二级解决"提前生成的对象只能有一份"。

📌 为什么 Spring Boot 2.6 之后默认禁止循环依赖?

text
Spring Boot 2.6 起:spring.main.allow-circular-references 默认 false
启动时若检测到环,直接抛:
  BeanCurrentlyInCreationException: Requested bean is currently in creation:
  Is there an unresolvable circular reference?

官方理由:
  ① 循环依赖几乎总是设计问题(职责划分不清)
  ② 三级缓存并非万能:@Async、部分 @Transactional、
     被 @Order 影响的 BeanPostProcessor 顺序都可能让它失效
  ③ 失效时报的错非常隐晦(BeanCurrentlyInCreationException / 代理不一致)
     不如启动就暴露

四种破环手段,按推荐度排序:

java
// ✅ 首选:重构,抽出公共依赖 C,把环变成树(彻底根治)

// ✅ 次选:@Lazy —— 注入一个代理,首次调用方法时才去容器取真身
@Service
public class A {
    private final B b;
    public A(@Lazy B b) { this.b = b; }   // 构造器注入也能破环
}

// ⚠️ 可用:改为 setter / 字段注入,让三级缓存兜住(治标不治本)
@Service
public class A2 {
    private B b;
    @Autowired public void setB(B b) { this.b = b; }
}

// ⚠️ 下策:运行时用 ApplicationContextAware 主动查找
@Service
public class A3 implements ApplicationContextAware {
    private ApplicationContext ctx;
    public void setApplicationContext(ApplicationContext ctx) { this.ctx = ctx; }
    public void work() { ctx.getBean(B.class).doSomething(); }
}

// ❌ 最差:spring.main.allow-circular-references=true 全局放开(把警报关掉而已)

✨ 高级深挖知识点 ​

1. refresh() 十二步:容器启动的骨架 ​

AbstractApplicationContext#refresh() 是整个 Spring 的"主干方法"。能说出这十二步,面试官基本认定你读过源码。

text
refresh()
 ① prepareRefresh()                  准备:记录启动时间、设置 active 标志、初始化 Environment
 ② obtainFreshBeanFactory()          创建 DefaultListableBeanFactory,加载 BeanDefinition
 ③ prepareBeanFactory(bf)            设置类加载器、注册 ApplicationContextAwareProcessor、
                                     注册可解析依赖(BeanFactory/ApplicationContext 自身)
 ④ postProcessBeanFactory(bf)        子类扩展钩子(Web 容器在这里注册 request/session 作用域)
 ⑤ invokeBeanFactoryPostProcessors() ★ 执行 BFPP:ConfigurationClassPostProcessor 在这里
                                       解析 @Configuration、@ComponentScan、@Import,
                                       把扫描到的类全部注册成 BeanDefinition
 ⑥ registerBeanPostProcessors()      ★ 把所有 BPP 实例化并排序注册(AOP 代理创建器在此就位)
 ⑦ initMessageSource()               国际化
 ⑧ initApplicationEventMulticaster() 事件广播器
 ⑨ onRefresh()                       子类扩展(Spring Boot 在这里创建并启动内嵌 Tomcat)
 ⑩ registerListeners()               注册 ApplicationListener
 ⑪ finishBeanFactoryInitialization() ★ 实例化所有非懒加载单例(Bean 生命周期在这里批量跑)
 ⑫ finishRefresh()                   清缓存、发布 ContextRefreshedEvent、
                                     回调 SmartInitializingSingleton / LifecycleProcessor

三个 ★ 是全部考点的来源:第⑤步是 BeanFactoryPostProcessor 的舞台,第⑥步决定了 AOP 能不能生效,第⑪步是 Bean 生命周期十二步的入口。

2. BeanFactoryPostProcessor vs BeanPostProcessor ​

维度BeanFactoryPostProcessor (BFPP)BeanPostProcessor (BPP)
作用对象BeanDefinition(配置清单)Bean 实例(对象)
执行时机refresh 第 ⑤ 步,所有 Bean 实例化之前每个 Bean 创建过程中(初始化前/后)
执行次数整个容器只执行一次每个 Bean 都会过一遍
能做什么改 scope、改属性值、新增/删除 BeanDefinition改对象字段、返回代理对象替换原对象
典型实现ConfigurationClassPostProcessor(解析注解配置)、PropertySourcesPlaceholderConfigurer(替换 ${})、MyBatis MapperScannerConfigurerAutowiredAnnotationBeanPostProcessor(@Autowired 注入)、AnnotationAwareAspectJAutoProxyCreator(AOP)、ApplicationContextAwareProcessor
java
// BFPP:在 Bean 还没造出来之前,偷偷把所有 Service 改成懒加载
@Component
public class LazyAllServices implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
        for (String name : bf.getBeanDefinitionNames()) {
            BeanDefinition bd = bf.getBeanDefinition(name);
            if (name.endsWith("Service")) {
                bd.setLazyInit(true);          // 改的是"清单",对象还不存在
            }
        }
    }
}

// BPP:在对象已经造好后替换成代理
@Component
public class LogProxyProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String name) {
        if (!(bean instanceof PayService)) return bean;
        return Proxy.newProxyInstance(bean.getClass().getClassLoader(),
                bean.getClass().getInterfaces(),
                (p, m, args) -> {
                    System.out.println("调用 " + m.getName());
                    return m.invoke(bean, args);   // 注意这里用的是真身 bean
                });
    }
}

BeanDefinitionRegistryPostProcessor 是 BFPP 的子接口,多了 postProcessBeanDefinitionRegistry,执行更早,专门用于注册新的 BeanDefinition(BFPP 只适合改已有的)。ConfigurationClassPostProcessor 实现的就是它。

3. @Configuration 与 proxyBeanMethods:为什么 @Bean 方法调两次只有一个实例 ​

java
@Configuration   // 默认 proxyBeanMethods = true
public class AppConfig {
    @Bean public A a() { return new A(b()); }   // 这里调了 b()
    @Bean public B b() { return new B(); }      // 容器也会调一次 b()
}

问题:b() 被调用两次,为什么容器里只有一个 B 实例?

text
proxyBeanMethods = true(Full 模式,默认)
   └─ ConfigurationClassEnhancer 用 CGLIB 给 AppConfig 生成子类
        └─ 覆写每个 @Bean 方法,拦截逻辑:
             先查容器里有没有这个 beanName
               ├─ 有 → 直接返回容器里的(保证单例语义)
               └─ 没有 → 调用父类原方法创建并放入容器
   → 代价:启动时多一次 CGLIB 生成 + 每次调用走拦截

proxyBeanMethods = false(Lite 模式)
   └─ 不生成 CGLIB 子类,@Bean 方法就是普通方法
   → 直接调 b() 会真的 new 一个新 B(不进容器!)
   → 优点:启动更快,Spring Boot 内部大量自动配置类都用 false
java
// ✅ Lite 模式下的正确写法:依赖通过方法参数声明,由容器注入
@Configuration(proxyBeanMethods = false)
public class AppConfigLite {
    @Bean public B b() { return new B(); }
    @Bean public A a(B b) { return new A(b); }   // 参数由容器解析,不要手动调 b()
}

结论:@Bean 方法之间不要互相直接调用。要依赖就声明成方法参数,这样两种模式都正确,也更能表达依赖关系。

4. AOP 代理的创建时机与 @EnableAspectJAutoProxy ​

text
@EnableAspectJAutoProxy
   └─ @Import(AspectJAutoProxyRegistrar.class)
        └─ 注册 AnnotationAwareAspectJAutoProxyCreator(一个 BeanPostProcessor)
             │
             ├─ 它实现 SmartInstantiationAwareBeanPostProcessor
             │     → 提供 getEarlyBeanReference(循环依赖时提前代理的入口)
             │
             └─ postProcessAfterInitialization(Bean 生命周期第 ⑨ 步)
                   ├─ 收集容器中所有 Advisor(@Aspect 类解析成 Advisor 列表)
                   ├─ 用 Pointcut 逐个匹配当前 Bean 的方法
                   ├─ 一个都不匹配 → 返回原对象
                   └─ 有匹配 → ProxyFactory.getProxy() 生成 JDK / CGLIB 代理

Spring Boot 中 AopAutoConfiguration 会自动开启,所以不用手写 @EnableAspectJAutoProxy。两个关键属性:

属性作用Spring Boot 默认
proxyTargetClass强制使用 CGLIBtrue
exposeProxy把当前代理暴露到 AopContext.currentProxy()false
java
// exposeProxy = true 后,可以在类内部拿到自己的代理,解决自调用问题
@EnableAspectJAutoProxy(exposeProxy = true)
@Service
public class OrderService {
    @Transactional
    public void inner() { /* 事务逻辑 */ }

    public void outer() {
        // ❌ this.inner();  不走代理,事务失效
        ((OrderService) AopContext.currentProxy()).inner();  // ✅ 走代理
    }
}

5. @Transactional 失效的七种场景 ​

事务是 AOP 最典型的落地,失效原因基本都能归到"没走代理"或"异常没被识别"两类。

失效场景根因修复方式
① 同类内部 this.method() 自调用没经过代理对象注入自己 / AopContext.currentProxy() / 拆到另一个类
② 方法不是 publicSpring 默认只拦 public(CGLIB 无法覆写 private)改为 public
③ 方法或类是 final / staticCGLIB 无法覆写去掉 final,改为实例方法
④ 异常被 try/catch 吞掉代理层感知不到异常,照常 commit抛出异常,或手动 setRollbackOnly()
⑤ 抛出的是受检异常(Exception)默认只对 RuntimeException + Error 回滚@Transactional(rollbackFor = Exception.class)
⑥ 数据库引擎不支持事务(MyISAM)存储引擎层面没有事务改 InnoDB
⑦ 多线程 / 异步里执行 DB 操作事务绑定在 ThreadLocal,子线程拿不到连接子线程内部自己开事务,或改用消息/补偿
java
// ❌ 场景①+④ 双重失效的经典写法
@Service
public class OrderServiceBad {
    public void createOrder(Order o) {
        try {
            this.saveAndDeduct(o);      // ① this 调用,事务注解完全无效
        } catch (Exception e) {
            log.error("失败", e);        // ④ 即使走了代理,异常被吞也不会回滚
        }
    }
    @Transactional
    public void saveAndDeduct(Order o) { orderDao.insert(o); stockDao.deduct(o); }
}

// ✅ 正确:事务入口在外层、让异常往外抛、跨类调用走代理
@Service
public class OrderServiceGood {
    private final OrderTxService txService;     // 拆到另一个 Bean
    public OrderServiceGood(OrderTxService txService) { this.txService = txService; }

    public void createOrder(Order o) {
        txService.saveAndDeduct(o);   // 跨 Bean 调用→ 走代理 → 事务生效
    }
}

@Service
public class OrderTxService {
    @Transactional(rollbackFor = Exception.class)   // 受检异常也回滚
    public void saveAndDeduct(Order o) throws BizException {
        orderDao.insert(o);
        stockDao.deduct(o);           // 这里抛异常,上面的 insert 会回滚
    }
}

6. 事务传播行为:七种取值与实际效果 ​

传播行为外层有事务时外层无事务时典型用途
REQUIRED(默认)加入外层事务新建事务绝大多数业务
REQUIRES_NEW挂起外层,自己开新事务新建事务日志记录,主业务回滚也要留
NESTED在外层事务里开 Savepoint新建事务子操作失败只回滚子操作
SUPPORTS加入不开事务只读查询
NOT_SUPPORTED挂起外层,自己不开不开事务耗时的非事务操作
MANDATORY加入抛异常强制要求被包在事务里调用
NEVER抛异常不开事务强制禁止在事务中调用

📌 单独澄清:REQUIRES_NEW 与 NESTED 的差别在"连接"和"回滚粒度"

维度REQUIRES_NEWNESTED
底层机制挂起外层,另开一个数据库连接同一连接,打 Savepoint
内层回滚只回滚内层,外层不受影响只回滚到 Savepoint,外层不受影响
外层回滚内层已提交的数据保留内层跟着一起回滚
连接占用占两个连接(高并发时可能连接池耗尽)一个连接
死锁风险内外层操作同一行时可能互锁无
java
@Service
public class PayService {
    @Transactional(rollbackFor = Exception.class)        // 外层 REQUIRED
    public void pay(Long orderId) {
        orderDao.updateStatus(orderId, "PAID");
        logService.record(orderId);     // REQUIRES_NEW:这条日志无论主流程成败都留下
        accountDao.deduct(orderId);     // 若这里抛异常,上面的 updateStatus 回滚,日志仍在
    }
}

@Service
public class LogService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record(Long orderId) { logDao.insert(orderId); }
}

7. Aware 接口家族:容器能力的"后门" ​

Bean 默认是"无感知"的,但有时业务确实需要容器能力。Aware 系列就是这些后门,执行顺序在属性填充之后、初始化之前。

接口注入的东西常见用途
BeanNameAware自己的 beanName日志打印、多实例区分
BeanClassLoaderAware类加载器动态加载类、SPI
BeanFactoryAwareBeanFactory运行时按名查找 Bean
ApplicationContextAwareApplicationContext最常用,发事件 / 取 Bean 工具类
EnvironmentAwareEnvironment读配置、判断 Profile
ResourceLoaderAwareResourceLoader加载 classpath 资源
ApplicationEventPublisherAware事件发布器发布领域事件
EmbeddedValueResolverAware占位符解析器手动解析 ${} / #{}

执行机制分两处:BeanNameAware / BeanClassLoaderAware / BeanFactoryAware 由 invokeAwareMethods 直接调用(第 ⑥ 步);其余由 ApplicationContextAwareProcessor 这个 BPP 在 postProcessBeforeInitialization(第 ⑦ 步)里调用。

java
// 最经典用法:静态工具类拿容器,供非 Spring 管理的代码使用
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext ctx;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        ctx = applicationContext;
    }
    public static <T> T getBean(Class<T> clazz) { return ctx.getBean(clazz); }
    public static <T> T getBean(String name, Class<T> clazz) { return ctx.getBean(name, clazz); }
}

8. Spring 事件机制:容器内的发布订阅 ​

事件机制让模块之间不用互相持有引用也能通信,是解耦循环依赖的常用手段。

text
publisher.publishEvent(event)
   └─ ApplicationEventMulticaster#multicastEvent
        ├─ 按事件类型筛选 ApplicationListener(支持泛型精确匹配)
        ├─ taskExecutor == null → **同步**,在发布者线程里逐个调用
        └─ taskExecutor != null → 异步,线程池里执行
容器内置事件触发时机
ContextRefreshedEventrefresh 第 ⑫ 步,容器就绪
ApplicationStartedEventSpring Boot 启动完成(Runner 之前)
ApplicationReadyEvent所有 Runner 执行完,应用可接流量
ContextClosedEvent容器开始关闭
java
// 1. 定义事件(Spring 4.2+ 不必继承 ApplicationEvent)
public record OrderPaidEvent(Long orderId, long amount) {}

// 2. 发布
@Service
public class PayService {
    private final ApplicationEventPublisher publisher;
    public PayService(ApplicationEventPublisher publisher) { this.publisher = publisher; }

    @Transactional
    public void pay(Long orderId) {
        orderDao.updateStatus(orderId, "PAID");
        publisher.publishEvent(new OrderPaidEvent(orderId, 100L));  // 默认同步
    }
}

// 3. 监听
@Component
public class OrderListeners {
    // ⚠️ 默认同步且在同一事务里:监听器抛异常会导致 pay() 回滚
    @EventListener
    public void onPaid(OrderPaidEvent e) { log.info("订单已支付 {}", e.orderId()); }

    // ✅ 异步:需配合 @EnableAsync,脱离发布者线程和事务
    @Async
    @EventListener
    public void sendSms(OrderPaidEvent e) { smsClient.send(e.orderId()); }

    // ✅ 事务事件:只在事务真正提交后才执行,避免"事务还没提交就发了短信"
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void pushMq(OrderPaidEvent e) { mqProducer.send(e); }
}

📌 单独澄清:@EventListener 默认是同步的,不是"消息队列"

维度@EventListener(默认)@Async @EventListener真正的 MQ
执行线程发布者线程线程池线程消费者进程
是否在同一事务✅ 在(异常会连带回滚)❌ 不在❌ 不在
进程重启后丢失丢失持久化、可重试
适用场景同事务内的强一致副作用进程内解耦的非关键逻辑跨服务、要求可靠投递

9. Bean 加载顺序的四种控制手段 ​

手段控制什么注意点
@DependsOn("beanA")强制 beanA 先初始化完成只影响初始化顺序,不产生依赖注入
@Order / Ordered同类型多个 Bean 的集合排序❌ 不影响 Bean 创建顺序,只影响 List<T> 注入顺序和切面链顺序
@Priority自动注入时的候选优先级JSR-250,比 @Order 少用
@AutoConfigureBefore/After自动配置类之间的顺序仅对 spring.factories / AutoConfiguration.imports 里的类有效,对 @Component 无效
java
// ❌ 常见误解:以为 @Order 能让 A 比 B 先创建
@Order(1) @Component class A {}
@Order(2) @Component class B {}  // A 不一定先于 B 被实例化!

// ✅ 想控制"谁先初始化好"用 @DependsOn
@Component
@DependsOn("dataSourceInitializer")   // 保证表结构初始化完才创建这个 Bean
public class UserCacheWarmer {
    @PostConstruct public void warmUp(){ /* 需要表已存在 */ }
}

// ✅ @Order 真正的用武之地:控制集合注入顺序与责任链顺序
@Component @Order(1) class AuthFilter implements BizFilter {}
@Component @Order(2) class LogFilter  implements BizFilter {}

@Service
class FilterChainRunner {
    private final List<BizFilter> filters;   // 注入时已按 @Order 升序排好
    FilterChainRunner(List<BizFilter> filters){ this.filters = filters; }
}

10. SmartInitializingSingleton 与启动回调时机对比 ​

回调时机能否保证"所有 Bean 都就绪"
@PostConstruct当前 Bean 初始化时❌ 其他 Bean 可能还没创建
InitializingBean当前 Bean 初始化时❌ 同上
SmartInitializingSingleton#afterSingletonsInstantiated所有单例实例化完毕后✅
ApplicationListener<ContextRefreshedEvent>容器 refresh 结束✅(但父子容器会触发多次)
ApplicationRunner / CommandLineRunnerSpring Boot 启动最后✅(仅 SpringBoot)
java
// 场景:要收集全部策略实现并建索引 —— 必须等所有 Bean 都创建完
@Component
public class StrategyRegistry implements SmartInitializingSingleton, ApplicationContextAware {
    private ApplicationContext ctx;
    private final Map<String, PayService> index = new HashMap<>();

    @Override public void setApplicationContext(ApplicationContext ctx) { this.ctx = ctx; }

    @Override
    public void afterSingletonsInstantiated() {
        ctx.getBeansOfType(PayService.class).values()
           .forEach(s -> index.put(s.channel(), s));   // 此刻一定全都在了
    }
}

11. @Async 与循环依赖、代理的三个坑 ​

java
// 坑 1:@Async 方法同类自调用 → 和 @Transactional 一样失效(不走代理)
// 坑 2:@Async + 循环依赖 → 启动直接报错
//        原因:AsyncAnnotationBeanPostProcessor 不是 SmartInstantiationAware,
//              无法参与"提前代理",三级缓存给出的半成品与最终代理不一致
//        报错:BeanCurrentlyInCreationException / 
//              "Bean with name 'a' has been injected into other beans [b] in its raw version"
// 坑 3:@Async 默认线程池是 SimpleAsyncTaskExecutor —— 每次来任务新建线程,不复用!
java
// ✅ 必须自定义线程池,否则高并发下线程数失控
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(32);
        executor.setQueueCapacity(1000);
        executor.setThreadNamePrefix("biz-async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        // void 返回值的异步方法抛异常会被"静默吞掉",必须接这个处理器
        return (ex, method, params) -> log.error("异步方法 {} 异常", method.getName(), ex);
    }
}

12. 条件装配:@Conditional 家族 ​

Spring Boot 自动配置的"大脑",面试常问「自动配置怎么做到按需生效」。

注解生效条件
@ConditionalOnClassclasspath 存在某个类
@ConditionalOnMissingClassclasspath 不存在某个类
@ConditionalOnBean容器中已有某个 Bean
@ConditionalOnMissingBean容器中没有某个 Bean(用户自定义优先的关键)
@ConditionalOnProperty配置项存在且匹配指定值
@ConditionalOnWebApplication当前是 Web 环境
@ConditionalOnExpressionSpEL 表达式为 true
java
// 自定义条件:只有配置了国际化开关才注册这个 Bean
@Configuration
public class I18nConfig {
    @Bean
    @ConditionalOnProperty(name = "app.i18n.enabled", havingValue = "true")
    @ConditionalOnMissingBean(MessageSource.class)   // 用户自己定义了就不用我的
    public MessageSource messageSource() {
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        ms.setBasename("i18n/messages");
        ms.setDefaultEncoding("UTF-8");
        return ms;
    }
}

@ConditionalOnMissingBean 的顺序陷阱:它判断的是「判断那一刻容器里有没有」,所以依赖 BeanDefinition 注册顺序。这就是自动配置类必须用 @AutoConfigureAfter 保证「在用户配置之后才评估」的原因;在普通 @Component 上用它结果不可预测。

13. 单例 Bean 中安全使用请求级状态:ThreadLocal 与作用域代理 ​

java
// 方案 A:ThreadLocal —— 自己管,务必 finally remove,否则线程池复用导致脏数据 + 泄漏
public class UserContext {
    private static final ThreadLocal<Long> HOLDER = new ThreadLocal<>();
    public static void set(Long id) { HOLDER.set(id); }
    public static Long get() { return HOLDER.get(); }
    public static void clear() { HOLDER.remove(); }      // ★ 必须调
}

@Component
public class UserInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object h) {
        UserContext.set(Long.valueOf(req.getHeader("uid")));
        return true;
    }
    public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
                                Object h, Exception e) {
        UserContext.clear();        // 不清就会被下一个请求读到上一个用户的 id
    }
}

// 方案 B:request 作用域 + 作用域代理 —— 交给容器管,单例里可直接注入
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
       proxyMode = ScopedProxyMode.TARGET_CLASS)   // ★ 没有这行,单例无法注入 request Bean
public class RequestContext {
    private Long userId;
    public Long getUserId(){ return userId; }
    public void setUserId(Long id){ this.userId = id; }
}

@Service
public class ReportService {
    private final RequestContext requestContext;   // 注入的是代理,每次调用路由到当前请求的实例
    public ReportService(RequestContext rc){ this.requestContext = rc; }
}

proxyMode 为什么必需? 单例 Bean 在容器启动时就创建,那一刻根本没有 HTTP 请求,无法注入 request 作用域的真实实例。加了作用域代理后,注入的是一个空壳代理,方法调用时才去 RequestContextHolder 找当前线程绑定的实例。

14. 父子容器:Spring MVC 的经典两层结构 ​

text
Root WebApplicationContext(父,由 ContextLoaderListener 创建)
   ├─ Service、DAO、事务、数据源
   │
   └─ Servlet WebApplicationContext(子,由 DispatcherServlet 创建)
         ├─ Controller、HandlerMapping、ViewResolver
         └─ 可以访问父容器的 Bean;父容器 ❌ 不能访问子容器的 Bean
规则说明
查找方向子 → 父 单向;先查自己,没有再问父容器
同名 Bean子容器的覆盖父容器(对子容器可见范围而言)
父容器的切面/事务❌ 不会作用于子容器的 Bean(切面要在哪一层生效就在哪一层声明)
经典坑事务配置在子容器、Service 在父容器 → 事务完全不生效

Spring Boot 默认只有一个容器(AnnotationConfigServletWebServerApplicationContext),所以这类坑少了很多。但 Spring Cloud Feign 的 FeignClient 配置、Dubbo 的部分场景仍会用到子容器隔离。

15. Spring 6 / Boot 3 的变化:AOT 与反射注入 ​

text
传统运行方式:反射 + 运行期动态代理 + 运行期 classpath 扫描
   └─ 启动慢(扫描 + 代理生成)、内存占用高、无法 GraalVM 原生镜像

Spring 6 AOT(Ahead-Of-Time):
   ├─ 构建期把 BeanDefinition "固化"成 Java 代码(BeanFactoryInitializationAotContribution)
   ├─ 构建期生成反射/代理/资源的注册元数据(reflect-config.json 等)
   └─ 运行期不再扫描、不再动态生成 → 启动毫秒级,可编成原生镜像
变化点影响
最低 JDK 17可用 record / sealed / 文本块写 Bean 和配置
javax.* → jakarta.*@Resource、@PostConstruct 包名全变,升级必改
AOT / 原生镜像运行期动态注册 BeanDefinition、运行期生成代理的"黑魔法"会失效
可观测性内置 Micrometer Observation API,替代手写 AOP 埋点
@HttpExchange声明式 HTTP 客户端进入 Spring 核心(类似 Feign)

面试加分表述:「AOT 的本质是把 Spring 启动期做的决策提前到编译期。代价是放弃运行期的动态性——所以我们项目里没有使用那种 if (环境==X) registry.registerBeanDefinition(...) 的运行期动态注册写法,全部改成 @Conditional 条件装配,保证将来能平滑迁到原生镜像。」

16. 配置属性注入:@Value、Environment 与 @ConfigurationProperties ​

依赖注入有两条线:注入 Bean(@Autowired)和注入配置值(@Value)。后者在面试里经常被忽略,但它是 IOC 容器的另一半能力。

text
Environment(ConfigurableEnvironment)
   └─ MutablePropertySources   有序的属性源列表,查找时「从前往后,第一个命中即返回」
        ① commandLineArgs            --server.port=8081
        ② SPRING_APPLICATION_JSON
        ③ systemProperties           -Dkey=value(JVM 参数)
        ④ systemEnvironment          操作系统环境变量
        ⑤ application-{profile}.yml  指定 profile 的配置
        ⑥ application.yml            主配置
        ⑦ @PropertySource            配置类上手动引入
        ⑧ SpringApplication.setDefaultProperties  代码兜底默认值

注入链路:
PropertySourcesPlaceholderConfigurer(一个 BFPP)
   └─ 向 BeanFactory 注册 embeddedValueResolver(底层就是 Environment)
        └─ AutowiredAnnotationBeanPostProcessor 填充 @Value 字段时
             ├─ resolveEmbeddedValue("${db.url}")     → 字符串替换
             ├─ BeanExpressionResolver 解析 "#{...}"  → SpEL 求值
             └─ ConversionService 类型转换            → String 转 int/List/Duration

${} 与 #{} 是两套完全不同的机制

写法机制求值时机典型用途
${a.b}属性占位符替换注入前,纯文本读配置文件
${a.b:默认值}占位符 + 冒号默认值同上配置缺失时兜底,强烈建议都写默认值
#{1 + 2}SpEL 表达式注入时求值计算、调用方法、引用其他 Bean
#{@otherBean.getName()}SpEL 引用 Bean注入时求值跨 Bean 取值
#{'${a.b}'.split(',')}先 ${} 再 #{}两阶段把逗号字符串切成数组(注意单引号必须有)

@Value vs @ConfigurationProperties

维度@Value@ConfigurationProperties
粒度单个值,散落在各处一整棵配置树绑定到一个对象
松散绑定❌ 不支持,db-url 和 dbUrl 不通✅ 支持 db-url / db_url / DB_URL / dbUrl
SpEL✅ 支持 #{}❌ 不支持
校验无✅ 可配 @Validated + JSR-303 做启动期校验
复杂类型需手写切割✅ 原生支持 List / Map / 嵌套对象 / Duration
元数据提示无✅ IDE 自动补全(spring-configuration-processor)
适用场景一两个零散开关一个模块的配置集合(推荐默认选它)
java
// ✅ 推荐:配置类 + 构造器绑定 + 启动期校验,配置写错启动就失败
@Validated
@ConfigurationProperties(prefix = "order.pay")
public record PayProps(
        @NotBlank String appId,
        @Min(1) int retryTimes,
        Duration timeout,          // 直接写 "3s" / "500ms",自动转换
        List<String> whiteList) {} // 直接写 order.pay.white-list[0]=...

// 注册方式二选一:配置类上 @EnableConfigurationProperties(PayProps.class)
//              或 类上加 @ConfigurationPropertiesScan 扫描

// ❌ 三个高频错误写法
@Component
public class Wrong {
    @Value("${db.url}")
    private static String url;            // ① static 字段注入不生效,永远是 null

    @Value("${db.port}")
    private int port;                     // ② 配置缺失直接启动失败,应写 ${db.port:3306}

    @Value("${db.hosts}")
    private List<String> hosts;           // ③ @Value 不会切割逗号,须 #{'${db.hosts}'.split(',')}
}

面试加分表述:「配置注入我们团队统一用 @ConfigurationProperties + @Validated + 构造器绑定:一是配置写错在启动期就失败,不会等到半夜调用才报 NPE;二是配置项是不可变的,不会被运行期误改;三是松散绑定让 K8s 环境变量(ORDER_PAY_APP_ID)和 yml(order.pay.app-id)能共用同一套字段。@Value 只保留在少数临时开关上。」

17. 组件扫描与注解元模型:@ComponentScan 到底怎么找到你的 Bean ​

@Component 能被扫到不是魔法,而是一条可以完整说清的链路。能讲清这条链路,才解释得了「为什么自定义组合注解能生效」「为什么注解加在父类上子类没继承到」。

text
ConfigurationClassPostProcessor(一个 BeanDefinitionRegistryPostProcessor)
 └─ ComponentScanAnnotationParser 解析 @ComponentScan
     └─ ClassPathBeanDefinitionScanner#doScan(basePackages)
         ① 用 PathMatchingResourcePatternResolver 把包名转成 classpath*:com/x/**/*.class
         ② 对每个 .class 用 ASM 直接读字节码(MetadataReader)
            → 关键:只读元数据,不加载类、不触发静态初始化,所以扫描很快且无副作用
         ③ excludeFilters 先判断,再判断 includeFilters(默认 include = @Component 及其派生)
         ④ isCandidateComponent:必须是独立的具体类(非接口/非抽象,或有 @Lookup 方法)
         ⑤ 生成 ScannedGenericBeanDefinition
         ⑥ BeanNameGenerator 生成 beanName(默认类名首字母小写;内部类是 Outer.Inner)
         ⑦ registry.registerBeanDefinition(name, bd)
过滤器类型含义示例
ANNOTATION(默认)标了某注解@ComponentScan(includeFilters = @Filter(MyMark.class))
ASSIGNABLE_TYPE是某类/接口的子类型扫描所有 BaseHandler 实现
ASPECTJ / REGEX表达式匹配类名@Filter(type = REGEX, pattern = "com\\.x\\..*Impl")
CUSTOM自己实现 TypeFilter拿到 MetadataReader 做任意判断

注解元模型:组合注解为什么能生效

Spring 不用 JDK 原生的 getAnnotation,而是用自己的 MergedAnnotations,会递归向上搜索元注解,并支持 @AliasFor 做属性互映射。

java
// @Service 本身并没有被扫描逻辑硬编码,它只是「标了 @Component 的注解」
@Component            // ← 元注解,Spring 递归搜索到它就认定是组件
public @interface Service { @AliasFor(annotation = Component.class) String value() default ""; }

// ✅ 所以可以自定义一个「带默认事务 + 带监控」的组合注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service                                       // 继承组件语义
@Transactional(rollbackFor = Exception.class)   // 继承事务语义
public @interface TxService {
    @AliasFor(annotation = Service.class)
    String value() default "";
}

@TxService("orderBiz")   // 等价于同时标了 @Service("orderBiz") + @Transactional
public class OrderBiz { }

三个最容易答错的边界

现象真相
父类标了 @Component,子类没标,子类没被注册类上的注解默认不被继承(@Component 没有 @Inherited)。扫描是逐个 class 文件判断的
父类方法标了 @Transactional,子类重写后失效方法注解从不继承。Spring 事务靠 AnnotationTransactionAttributeSource 主动向父类/接口查找,但 @Transactional 写在接口上对 CGLIB 代理不可靠,所以一律标在实现类的方法上
同一个类被两次扫到(如 @ComponentScan 包含了配置类所在包)第二次注册会走 isCompatible 判断,同名同源会静默跳过;但若两个不同类生成了同名 beanName,默认直接抛 ConflictingBeanDefinitionException

面试加分表述:「我们内部 Starter 里把 @Service + @Transactional + @Validated 合成了一个 @BizService 组合注解,靠的就是 Spring 的 MergedAnnotations 会递归搜元注解这个特性。另外排查过一个坑:有同学把 @Component 加在抽象基类上,以为所有子类都会被注册——实际上类注解不会继承,扫描器也会跳过抽象类,正确做法是在每个实现类上标注,或用 ASSIGNABLE_TYPE 过滤器批量纳入。」

18. Spring 里的设计模式全景:每个模式都能指到具体类 ​

「Spring 用了哪些设计模式」是必问题,但只背名字会被追问到露底。正确答法是每个模式都对应到一个能说出细节的类。

设计模式Spring 中的落点关键细节(追问时答这一列)
工厂方法BeanFactory#getBean、FactoryBean#getObjectBeanFactory 是容器本身的工厂;FactoryBean 是某个 Bean 的工厂,&name 才能拿到工厂自身
单例(注册表)DefaultSingletonBeanRegistry 的 singletonObjects不是经典单例(私有构造器),而是Map 缓存 + 双重检查,作用域是容器而非 JVM
模板方法AbstractApplicationContext#refreshrefresh() 定好十二步骨架,obtainFreshBeanFactory、onRefresh 留给子类实现
代理JdkDynamicAopProxy / CglibAopProxyAOP、@Transactional、@Async 全靠它;自调用失效的根因就在这里
责任链ReflectiveMethodInvocation#proceed用 currentInterceptorIndex 递归推进,所以它有状态、不可复用
观察者ApplicationEventMulticaster + @EventListener默认同步执行,加 @Async 才异步;@TransactionalEventListener 挂在事务钩子上
适配器AdvisorAdapter、MVC 的 HandlerAdapter把 MethodBeforeAdvice 等不同通知统一适配成 MethodInterceptor
装饰器BeanWrapper、*Decorator、DelegatingFilterProxyBeanPostProcessor 返回包装对象本质也是装饰
策略InstantiationStrategy、BeanNameGenerator反射实例化 vs CGLIB 实例化(有 replace-method 时)由策略决定
建造者BeanDefinitionBuilder、UriComponentsBuilder编程式注册 BeanDefinition 时常用
桥接JdbcTemplate 等 *Template抽象(模板)与实现(具体 DB 驱动/异常翻译)分离
text
一句话串起来(面试口述模板):
容器启动是「模板方法」定骨架(refresh 十二步),
Bean 的产出靠「工厂」(BeanFactory / FactoryBean),
单例靠「注册表式单例」(三级缓存 Map),
扩展靠「观察者」(事件)和「装饰」(BeanPostProcessor 包装),
增强靠「代理」(JDK / CGLIB),
多个增强串成「责任链」(MethodInvocation),
不同类型的通知靠「适配器」统一成 MethodInterceptor。

面试加分表述:「我更愿意讲这些模式解决了什么问题:模板方法让 Spring 的启动流程既固定又可扩展,所以 Boot 只需实现 onRefresh 就能把内嵌 Tomcat 塞进去;责任链让通知可以任意叠加而互不感知,但代价是 MethodInvocation 有状态不能复用——我们自己写拦截器链时就踩过这个坑,把 invocation 对象缓存起来复用,结果并发下索引错乱。」

❓ 高频面试题【基础简答 + 连环追问链 + 精简口述答案 + 项目关联话术】 ​

题 1:什么是 IOC 和 DI?BeanFactory 和 ApplicationContext 有什么区别? ​

精简口述答案(90 秒版)

IOC 控制反转是一种设计思想,反转的是"对象创建时机、实现类选择、依赖装配"这三项控制权,从业务代码交给容器。注意它反转的是控制权,不是取消创建动作,底层仍然是反射调构造器。

DI 依赖注入是 IOC 最主流的实现手段,分构造器、setter、字段三种;另一种实现手段是 DL 依赖查找,也就是主动 getBean。

BeanFactory 是 IOC 容器的顶层接口,只提供最基础的「按名/按类型取 Bean」,而且是懒加载,getBean 时才实例化。ApplicationContext 是它的子接口,在此之上叠了国际化、事件发布、资源加载、Environment 四大企业级能力,并且启动时就预实例化所有非懒加载单例,配置写错在启动阶段就暴露,线上更安全。

实际结构上,ApplicationContext 并不自己存 Bean,而是组合了一个 DefaultListableBeanFactory 当仓库,自己做门面和增强。

连环追问链

追问答题要点
FactoryBean 和 BeanFactory 的区别?BeanFactory 是容器本身;FactoryBean 是容器里的一个普通 Bean,但它负责生产别的 Bean。getBean("x") 拿到的是 getObject() 结果,加 & 前缀才拿到工厂本体。MyBatis 的 Mapper 代理就靠它
ApplicationContext 有哪些常用实现?ClassPathXmlApplicationContext(XML)、AnnotationConfigApplicationContext(注解)、Spring Boot 的 AnnotationConfigServletWebServerApplicationContext
容器启动流程大致几步?refresh() 十二步,关键三步是:⑤ invokeBeanFactoryPostProcessors 解析配置注册 BeanDefinition、⑥ registerBeanPostProcessors 让 AOP 代理器就位、⑪ finishBeanFactoryInitialization 批量创建单例
BeanDefinition 是什么?Bean 的元数据清单(beanClass、scope、lazyInit、primary、dependsOn、构造参数等)。注册 BeanDefinition ≠ 创建 Bean,这条分界线决定了 BFPP 和 BPP 的职责划分
为什么推荐构造器注入?依赖可 final 不可变、不可能为 null、单测能直接 new 传 mock、构造参数一多就能暴露"这个类职责过重";而且构造器注入遇到循环依赖会启动失败,等于强制你在第一天修正设计

✅ 项目关联话术

「我们项目所有 Service 统一用构造器注入 + Lombok 的 @RequiredArgsConstructor,字段全部 final。好处很直接:有一次新同学给一个 Service 加了第 9 个依赖,Code Review 时构造参数列表一眼就看出职责过重,顺势拆成了两个 Service。另外多实现类的场景(比如支付渠道)我们注入 Map<String, PayService>,key 就是 beanName,彻底消灭了渠道路由的 if-else,新增渠道只要加一个 @Service 实现类。」

题 2:Spring AOP 的底层原理?JDK 动态代理和 CGLIB 有什么区别?哪些场景 AOP 会失效? ​

精简口述答案(90 秒版)

Spring AOP 的本质是运行期动态代理。容器在 Bean 生命周期的「初始化后」这一步,由 AnnotationAwareAspectJAutoProxyCreator 这个 BeanPostProcessor 收集所有 Advisor,用切点表达式匹配当前 Bean 的方法;匹配上就生成代理对象,放进单例池的是代理,不是真身。

代理方式二选一:目标类实现了接口且没强制 CGLIB,就用 JDK 动态代理,运行期生成实现同样接口的 $Proxy0;没有接口或 proxyTargetClass=true,就用 CGLIB,用 ASM 生成目标类的子类并覆写方法。Spring Boot 默认 proxyTargetClass=true 统一走 CGLIB,避免「按实现类注入时类型转换失败」。

AOP 失效归根到底就一句:没走代理对象。具体有四类——同类内部 this.xxx() 自调用、private 方法、static 方法、final 类或方法。

连环追问链

追问答题要点
Spring AOP 和 AspectJ 的区别?Spring AOP 运行期代理,只能拦方法调用,自调用失效;AspectJ 编译期/类加载期改字节码,能拦字段、构造器,自调用也生效。我们用的 @Aspect、execution() 只是借用 AspectJ 的注解和表达式语法,执行引擎还是 Spring 自己的代理
五种通知的执行顺序?@Around 前半段 → @Before → 目标方法 → (正常)@AfterReturning / (异常)@AfterThrowing → @After → @Around 后半段。多切面用 @Order,数字小的在外层,形成责任链
自调用失效怎么解决?① 拆到另一个 Bean(最推荐,顺带理清职责);② 注入自己(Spring 4.3+ 允许自注入);③ @EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy();④ 换 AspectJ 编译期织入
CGLIB 代理的对象,构造器会执行几次?两次。父类(真身)构造一次,CGLIB 子类构造一次。所以构造器里有副作用(埋点、注册监听)的类用 CGLIB 要特别小心;Spring 内部用 Objenesis 可以绕过构造器
@Around 的 ProceedingJoinPoint 和 JoinPoint 区别?ProceedingJoinPoint 是 JoinPoint 的子接口,多了 proceed(),只有 @Around 能用。不调 proceed() 目标方法就不执行,可以用来做熔断、缓存短路

✅ 项目关联话术

「我们用自定义注解 + @Around 做了三个切面:接口耗时监控(超过 500ms 打 WARN 并上报)、操作审计日志、接口幂等(基于 Redis setnx)。业务代码里一行埋点都不用写,只打一个 @TimeLog 注解。踩过一个坑:有个同事把幂等注解打在了 private 方法上,上线后发现完全没生效;后来我们在 CI 加了一条 ArchUnit 规则,自定义注解只允许出现在 public 方法上,从机制上杜绝了这类问题。」

题 3:Spring 怎么解决循环依赖?三级缓存分别是什么?为什么是三级而不是两级? ​

精简口述答案(90 秒版)

三级缓存都在 DefaultSingletonBeanRegistry 里:一级 singletonObjects 存完整成品;二级 earlySingletonObjects 存提前暴露的半成品(可能已是代理);三级 singletonFactories 存生产半成品的 ObjectFactory。查找顺序永远是一级 → 二级 → 三级。

流程:A 实例化后立刻把 ObjectFactory 放进三级缓存;A 填充属性时发现要 B,去创建 B;B 填充属性时发现要 A,一二级都没有,三级命中,执行工厂拿到半成品 A(如果 A 需要 AOP,这一刻就提前把代理造出来),同时把结果升到二级并移除三级;B 完成后进一级缓存;A 拿到完整的 B,完成后也进一级缓存。

为什么要三级? 关键是「要不要代理」这个决定必须延迟到真正有人来取的那一刻。如果只有两级,就得在实例化后立刻给所有 Bean 创建代理,这破坏了「AOP 在初始化后织入」的生命周期约定,也给没有循环依赖的 Bean 带来无谓开销。三级只放工厂,没人取就永远不执行。

那二级缓存的作用是什么? 保证同一性。如果 B 和 C 都依赖 A,没有二级缓存,工厂会被执行两次,产生两个不同的代理对象,单例语义就破了。

连环追问链

追问答题要点
构造器注入的循环依赖能解决吗?不能。实例化阶段就需要依赖对象,连"半成品"都造不出来,无从暴露。只能靠 @Lazy 注入代理来绕过,或者重构打破环
prototype 的循环依赖能解决吗?不能。多例 Bean 不进单例缓存,没有提前暴露的机制,直接抛 BeanCurrentlyInCreationException
三级缓存是万能的吗?不是。@Async 的 AsyncAnnotationBeanPostProcessor 不是 SmartInstantiationAwareBeanPostProcessor,无法参与提前代理,导致早期暴露的引用和最终代理不一致,会报 "has been injected into other beans in its raw version"。这正是 Spring Boot 2.6+ 默认禁止循环依赖的原因
Spring Boot 2.6 以后为什么默认禁止?三点:① 循环依赖几乎总是设计问题;② 三级缓存并非万能(见上条);③ 失效时报错非常隐晦,不如启动就暴露。要恢复得显式配 spring.main.allow-circular-references=true
怎么破环?按推荐度排序① 重构抽公共依赖,把环变成树(根治);② @Lazy 注入代理;③ 改 setter/字段注入让三级缓存兜住(治标);④ ApplicationContextAware 运行时查找;⑤ 全局放开配置(最差,等于把警报关掉)

✅ 项目关联话术

「我们从 Spring Boot 2.3 升到 2.7 时,启动直接挂了,报了三处循环依赖。我们没有图省事去开 allow-circular-references,而是逐个分析:其中两处是 UserService 和 OrderService 互相调用,我们把两边共用的「用户额度校验」抽成了独立的 QuotaService,环就断了;第三处是一个历史遗留的通知模块,短期先用 @Lazy 兜住并打了 TODO,下个迭代重构掉。现在我们团队的规范是:构造器注入 + 不允许全局放开循环依赖,相当于把设计问题的检测左移到了启动阶段。」

题 4:完整讲一下 Spring Bean 的生命周期,有哪些扩展点可以介入? ​

精简口述答案(90 秒版)

大致分四大阶段、十二小步:

① 实例化前:InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation,返回非 null 会直接短路。 ② 实例化:推断构造器 → 反射 newInstance,此时字段全是默认值;随后把 ObjectFactory 放入三级缓存提前暴露。 ③ 属性填充与初始化:populateBean 反射注入依赖 → Aware 回调(BeanName / BeanClassLoader / BeanFactory)→ BeanPostProcessor 前置(这里执行 @PostConstruct、补齐 ApplicationContextAware)→ InitializingBean#afterPropertiesSet → init-method → BeanPostProcessor 后置(AOP 代理在这里产生)。 ④ 注册与销毁:注册销毁回调 → 放入一级缓存可用 → 容器关闭时 @PreDestroy → DisposableBean#destroy → destroy-method。

最重要的扩展点有两类:BeanFactoryPostProcessor 作用在 BeanDefinition 清单阶段(整个容器只执行一次,能改配置、能加删 Bean 定义);BeanPostProcessor 作用在实例阶段(每个 Bean 都过一遍,能改对象、能返回代理替换原对象)。AOP 和 @Autowired 注入都是 BPP 实现的。

连环追问链

追问答题要点
三种初始化回调的顺序?@PostConstruct(JSR-250,由 CommonAnnotationBeanPostProcessor 执行)→ InitializingBean#afterPropertiesSet → init-method。销毁顺序对称:@PreDestroy → DisposableBean#destroy → destroy-method
构造器里访问 @Autowired 字段为什么 NPE?时序问题:构造器在第②步执行,字段注入在第⑤步。初始化逻辑要么改用构造器注入,要么挪到 @PostConstruct(第⑦步)
怎么保证"所有 Bean 都就绪后"再执行逻辑?@PostConstruct 不行(别的 Bean 可能还没建)。要用 SmartInitializingSingleton#afterSingletonsInstantiated、监听 ContextRefreshedEvent、或 Spring Boot 的 ApplicationRunner
prototype Bean 的销毁回调会执行吗?不会。容器创建完就不再持有引用,@PreDestroy 永远不被调用。需要自己 try-with-resources 或手动调 beanFactory.destroyBean(obj)
BeanDefinitionRegistryPostProcessor 是什么?BFPP 的子接口,执行更早,专门用于注册新的 BeanDefinition。ConfigurationClassPostProcessor 实现的就是它,@ComponentScan、@Import 都在这里被解析

✅ 项目关联话术

「我们写过一个自定义 BeanPostProcessor 做敏感字段脱敏:Bean 初始化后扫描带 @Sensitive 注解的字段,给它包一层代理。还写过一个 BeanFactoryPostProcessor,在预发环境把所有外部 RPC 客户端的 BeanDefinition 换成 Mock 实现类,不用改一行业务代码就能做隔离测试。踩过一个生命周期的坑:有个缓存预热的 @PostConstruct 方法依赖另一个 Bean 已经把表建好,偶发失败,最后用 @DependsOn 显式声明顺序才稳定——这让我彻底搞清了 @Order 不控制创建顺序,只控制集合注入顺序。」

题 5:@Autowired 和 @Resource 有什么区别?一个接口多个实现时怎么注入? ​

精简口述答案(90 秒版)

@Autowired 是 Spring 自有注解,默认按类型装配,歧义时才退化为按名称;由 AutowiredAnnotationBeanPostProcessor 处理,支持 required = false,可用在构造器、setter、字段和方法参数上。

@Resource 是 JSR-250 标准注解,默认按名称装配,找不到名字再按类型;由 CommonAnnotationBeanPostProcessor 处理,不支持 required,也不能用在构造器上。注意 Spring Boot 3 / Spring 6 之后包名从 javax.annotation 变成了 jakarta.annotation。

一个接口多个实现时,@Autowired 的解析顺序是:按类型拿到候选集合 → 只有一个直接注入 → 多个则依次尝试 @Primary、@Priority、字段名与 beanName 匹配 → 都失败就抛 NoUniqueBeanDefinitionException。

实战上有三种解法:有明确主力实现用 @Primary;注入点要指定具体实现用 @Qualifier;需要全部实现做策略路由,就注入 Map<String, T> 或 List<T>,这是消灭 if-else 最优雅的方式。

连环追问链

追问答题要点
注入 List<T> 时顺序怎么定?用 @Order 或实现 Ordered 接口,数字小的在前。注意 @Order 只影响集合顺序和切面链顺序,不影响 Bean 创建顺序
注入 Map<String, T> 的 key 是什么?beanName。@Service("aliPay") 就是 aliPay,不写名字则是类名首字母小写
找不到 Bean 怎么优雅处理?@Autowired(required = false)、注入 Optional<T>、或注入 ObjectProvider<T> 后调 getIfAvailable()
单例里注入 prototype,为什么拿到的总是同一个?注入只在创建单例时发生一次。要每次都新建得用 ObjectProvider#getObject()、@Lookup 方法注入,或给 prototype Bean 加 @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
泛型注入能区分吗?可以。Spring 4+ 支持泛型作为限定条件,@Autowired Handler<OrderEvent> h 能精确匹配到 Handler<OrderEvent> 的实现,不会和 Handler<PayEvent> 混淆

✅ 项目关联话术

「我们支付模块有六个渠道实现,早期是一个 40 行的 switch。重构时改成注入 Map<String, PayService>,在 SmartInitializingSingleton 里按渠道码建索引,新增渠道只要加一个 @Service 实现类,路由代码零改动。另外我们有一个注入歧义引发的事故:两个模块各自定义了一个 RestTemplate Bean,启动就挂。排查后我们定了规范——所有基础设施类 Bean 必须显式命名 + 给默认实现打 @Primary,并且在自动配置里统一用 @ConditionalOnMissingBean 让业务侧可覆盖。」

题 6:@Transactional 在哪些情况下会失效?事务传播行为有哪几种? ​

精简口述答案(90 秒版)

失效原因归两大类:没走代理 和 异常没被识别。

没走代理的四种:① 同类内部 this.method() 自调用;② 方法不是 public;③ 方法或类是 final / static;④ 类根本没被 Spring 管理。

异常没被识别的两种:④ 异常被 try/catch 吞掉,代理层感知不到就照常提交;⑤ 抛的是受检异常,Spring 默认只对 RuntimeException 和 Error 回滚,必须显式写 rollbackFor = Exception.class。

另外还有两种环境因素:数据库引擎不支持事务(MyISAM);以及在子线程里执行 DB 操作——事务通过 ThreadLocal 绑定连接,子线程拿不到。

传播行为七种,最常考三个:REQUIRED 默认,有就加入、没有就新建;REQUIRES_NEW 挂起外层、另开一个数据库连接,外层回滚时内层已提交的数据保留;NESTED 同一连接打 Savepoint,外层回滚时内层跟着一起回滚。这个「外层回滚时内层数据是否保留」正是两者最本质的区别。

连环追问链

追问答题要点
自调用失效的根因和解法?this 是真身引用,不经过代理,切面逻辑完全没机会执行。解法按推荐度:拆到另一个 Bean > 自注入 > exposeProxy=true + AopContext.currentProxy() > 用 TransactionTemplate 编程式事务
REQUIRES_NEW 有什么风险?占两个数据库连接,高并发下可能耗尽连接池;内外层操作同一行时可能互相锁等待导致死锁
事务和其他切面的执行顺序怎么控制?@Transactional 对应的 TransactionInterceptor 默认 order 是 Integer.MAX_VALUE(最内层)。想让自定义切面在事务外层就给它一个更小的 @Order;比如分布式锁切面必须在事务外,否则锁释放早于事务提交,会出现并发问题
只读事务 readOnly = true 有什么用?给数据库和 ORM 优化提示:MySQL 可走只读副本、Hibernate 会跳过脏检查刷新。注意它不是"禁止写",写了可能直接报错也可能静默不生效,取决于驱动
事务里发消息怎么保证一致性?不要在事务方法里直发 MQ(事务可能回滚但消息已发出)。用 @TransactionalEventListener(phase = AFTER_COMMIT),或本地消息表 / 事务消息

✅ 项目关联话术

「我们线上出过一次典型的自调用事务失效:下单方法里 this.saveOrderAndDeductStock(),扣库存失败后订单却留下来了,产生了一批脏数据。定位后我们做了三件事:一是把事务方法统一拆到独立的 XxxTxService,二是规范所有 @Transactional 必须写 rollbackFor = Exception.class,三是在 CI 里加了静态检查,禁止在同类内部调用带 @Transactional 的方法。还有一个顺序问题:我们的分布式锁切面原来 order 比事务大,变成了锁在事务内层,锁释放时事务还没提交,并发下照样超卖;把锁切面的 @Order 调成 -1 放到事务外层才彻底解决。」

题 7:Bean 有哪些作用域?单例 Bean 是线程安全的吗? ​

精简口述答案(90 秒版)

作用域六种:singleton(默认,容器启动创建、关闭销毁)、prototype(每次 getBean 新建,容器不管销毁)、以及 Web 环境下的 request、session、application、websocket。

单例 Bean 本身不保证线程安全,但是否出问题取决于有没有可变的实例字段:

  • 只有方法局部变量 → 安全,局部变量在各线程自己的虚拟机栈上,天然隔离;
  • 有可变实例字段 → 不安全,所有线程共享同一块堆内存;
  • 字段不可变(final 且引用对象也不可变)→ 安全,没有写就没有竞态;
  • 用 ThreadLocal 存状态 → 安全,但必须在 finally 里 remove(),否则线程池复用会读到上一个请求的脏数据,还会内存泄漏。

所以结论是:Service / DAO 这类无状态组件用单例完全安全,前提是不要把请求级状态塞进实例字段。

连环追问链

追问答题要点
为什么单例是默认作用域?绝大多数 Service / DAO 是无状态的,单例省去重复创建开销、省内存,还能让 AOP 代理、连接池等资源复用
SimpleDateFormat 为什么不能做单例字段?它内部用 Calendar 实例字段保存解析中间状态,多线程并发会互相覆盖,可能解析出错误日期甚至抛异常。改用 DateTimeFormatter(不可变、线程安全)
request 作用域的 Bean 怎么注入到单例里?必须加 @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)。因为单例在容器启动时创建,那一刻没有 HTTP 请求。加了作用域代理后注入的是空壳代理,方法调用时才去 RequestContextHolder 找当前线程的实例
ThreadLocal 不 remove 会怎样?两个后果:① 线程池复用线程,下个请求读到上个请求的数据(数据串了,比泄漏更可怕);② ThreadLocalMap 的 Entry key 是弱引用但 value 是强引用,key 被回收后 value 变成不可达却回收不掉,造成泄漏
Spring 的单例和设计模式的单例一样吗?不一样。设计模式的单例是整个 JVM 一个实例(私有构造器 + 静态持有);Spring 的单例是每个容器内、每个 beanName 一个实例,同一个类注册两个不同 beanName 就有两个实例,父子容器也可能各有一份

✅ 项目关联话术

「我们排查过一个很隐蔽的线上问题:某个导出接口偶发导出别人的数据。最后发现是有人在单例 Service 里加了一个 private Long currentUserId 字段,在方法开头赋值然后在私有方法里读,并发时被别的线程覆盖了。修复方案是把状态改成参数传递;同时我们把用户上下文统一收口到一个 ThreadLocal 工具类,在拦截器的 afterCompletion 里强制 remove,并且在 @Async 的线程池装饰器里做了上下文透传,避免异步任务里拿不到用户信息。」

题 8:@Configuration 的 proxyBeanMethods 是什么?@Bean 方法互相调用会创建两个实例吗? ​

精简口述答案(90 秒版)

不会,默认情况下仍然只有一个实例。因为 @Configuration 默认 proxyBeanMethods = true(Full 模式),Spring 会用 ConfigurationClassEnhancer 给配置类生成一个 CGLIB 子类,覆写每个 @Bean 方法。拦截逻辑是:先查容器里有没有这个 beanName,有就直接返回容器里的,没有才调父类原方法创建并注册。这就保证了单例语义。

如果显式设成 proxyBeanMethods = false(Lite 模式),就不生成 CGLIB 子类,@Bean 方法退化为普通方法。这时手动调 b() 会真的 new 一个新对象,而且不进容器——这就是 Lite 模式下最容易踩的坑。

Lite 模式的好处是启动更快、少一次字节码生成,所以 Spring Boot 内部大量自动配置类都标了 proxyBeanMethods = false。

正确写法是不管哪种模式都不要在 @Bean 方法里直接调用另一个 @Bean 方法,要依赖就声明成方法参数,让容器去解析注入。

连环追问链

追问答题要点
@Configuration 和 @Component 里的 @Bean 有区别吗?有。@Component 里的 @Bean 方法等价于 Lite 模式(无 CGLIB 增强),方法间互调会产生不受容器管理的新对象。所以配置类要用 @Configuration
@Import 有哪几种用法?① 导入普通类注册成 Bean;② 导入 @Configuration 配置类;③ 导入 ImportSelector(返回类名数组,Spring Boot 自动配置的核心);④ 导入 ImportBeanDefinitionRegistrar(编程式注册 BeanDefinition,MyBatis @MapperScan 用的就是它)
Spring Boot 自动配置的原理?@SpringBootApplication 里的 @EnableAutoConfiguration → @Import(AutoConfigurationImportSelector.class) → 读 META-INF/spring/*.AutoConfiguration.imports(2.7 之前是 spring.factories)→ 经过 @Conditional 过滤 → 注册成 BeanDefinition
@ConditionalOnMissingBean 为什么能让用户配置优先?它判断的是"评估那一刻容器里有没有",所以依赖 BeanDefinition 注册顺序。自动配置类通过 @AutoConfigureAfter 保证在用户配置之后才被评估。在普通 @Component 上用它结果不可预测
为什么 AOT / 原生镜像下有些写法会失效?AOT 把 BeanDefinition 固化到编译期,运行期不再扫描、不再动态生成代理。所以运行期动态注册 BeanDefinition、运行期反射造类这类"黑魔法"都会失效,要改用 @Conditional 条件装配

✅ 项目关联话术

「我们写内部 Starter 时踩过 Lite 模式的坑:为了启动性能给配置类加了 proxyBeanMethods = false,但代码里还保留着 @Bean public A a(){ return new A(b()); } 这种互调写法,结果线上出现了两个 DataSource,连接数翻倍。修复后我们把团队规范定成:@Bean 方法之间一律通过方法参数声明依赖,绝不直接互相调用——这样两种模式下行为都正确,依赖关系在方法签名上也一目了然。」

题 9:Spring 的配置是怎么注入进来的?@Value 和 @ConfigurationProperties 怎么选? ​

基础简答

Spring 把所有配置来源抽象成 PropertySource,按顺序放在 Environment 的 MutablePropertySources 里,查找时从前往后第一个命中即返回,这就是「命令行参数 > 环境变量 > application-{profile}.yml > application.yml > 默认值」优先级的由来。

@Value 的注入链路是:PropertySourcesPlaceholderConfigurer(一个 BFPP)向容器注册 embeddedValueResolver,AutowiredAnnotationBeanPostProcessor 在属性填充阶段拿到 @Value 的字符串,先做 ${} 占位符替换,若含 #{} 再交给 SpEL 求值,最后用 ConversionService 转成字段类型。

选型上:零散开关用 @Value,一个模块的配置集合一律用 @ConfigurationProperties。后者支持松散绑定、复杂类型、@Validated 启动期校验和 IDE 元数据提示,@Value 三样都没有。

连环追问链

追问答题要点
${} 和 #{} 有什么区别?${} 是占位符文本替换,由 Environment 完成;#{} 是 SpEL 表达式求值,能调方法、引用 Bean。混用时顺序是先 ${} 后 #{},所以要写成 #{'${a.b}'.split(',')}
@Value 注入 static 字段为什么是 null?依赖注入只作用于实例字段,AutowiredAnnotationBeanPostProcessor 会跳过 static。要么改成实例字段,要么在 @PostConstruct 里手动赋给静态变量
配置项不存在会怎样?@Value("${a.b}") 直接抛 IllegalArgumentException 导致启动失败。应写 ${a.b:默认值},或注入 @Value("${a.b:#{null}}")
@ConfigurationProperties 为什么支持松散绑定?它走 Binder + ConfigurationPropertyName 规范化,会把 db-url、db_url、DB_URL、dbUrl 统一成同一个 name。这正是 K8s 环境变量能映射到 yml 配置的原因
@ConfigurationProperties 的类需要加 @Component 吗?不一定。三种注册方式:① 类上加 @Component;② 配置类上 @EnableConfigurationProperties(X.class);③ 启动类上 @ConfigurationPropertiesScan。只加注解不注册等于没写
它是在 Bean 生命周期哪一步绑定的?ConfigurationPropertiesBindingPostProcessor(一个 BPP)在 postProcessBeforeInitialization 里绑定,所以 @PostConstruct 里能拿到完整配置,但构造器里拿不到(除非用构造器绑定)
怎么让配置写错在启动期就暴露?@ConfigurationProperties + @Validated + JSR-303 注解,绑定失败直接 BindValidationException 启动失败。这比运行时 NPE 便宜得多
配置能热更新吗?原生不能。@Value 注入后就是个普通字段。要热更新得靠 Spring Cloud 的 @RefreshScope(本质是作用域代理 + 销毁重建),或自己监听配置中心事件后手动 set
@PropertySource 能读 yml 吗?默认只支持 properties,读 yml 要自己写 PropertySourceFactory。这也是推荐用 application.yml 而非 @PropertySource 的原因

✅ 项目关联话术

「我们线上出过一次事故:@Value("${pay.timeout}") 注入到了一个工具类的 static 字段上,本地测试因为有默认值没暴露,上线后超时时间一直是 0,导致支付大面积超时。复盘后定了两条规范:一是配置一律收口到 @ConfigurationProperties 配置类,配 @Validated 让非法配置在启动期失败;二是禁止给 static 字段做注入,并加了 ArchUnit 规则在 CI 里卡住。改完之后这类问题再没出现过。」

题 10:@ComponentScan 是怎么扫描到 @Component 的?自定义组合注解为什么能生效? ​

基础简答

@ComponentScan 由 ConfigurationClassPostProcessor 解析,真正干活的是 ClassPathBeanDefinitionScanner:把包名转成 classpath*:com/x/**/*.class 资源,用 ASM 直接读字节码拿注解元数据(只读元数据、不加载类,所以快且无副作用),再经过 excludeFilters / includeFilters 过滤,判断是独立具体类后生成 ScannedGenericBeanDefinition 注册进 registry。

组合注解能生效,是因为 Spring 不用 JDK 原生的 getAnnotation,而是用 MergedAnnotations 递归向上搜索元注解。@Service 本身只是「一个标了 @Component 的注解」,所以我自定义一个标了 @Service + @Transactional 的 @BizService,一样会被识别成组件并带上事务语义,@AliasFor 还能让属性互相映射。

连环追问链

追问答题要点
扫描阶段会加载类吗?不会。走 ASM 读字节码(MetadataReader),避免触发静态初始化和大量类加载。只有真正要实例化时才 Class.forName
父类标了 @Component,子类会被注册吗?不会。@Component 没有 @Inherited,类注解默认不继承;而且抽象类会被 isCandidateComponent 直接排除
两个不同包下的同名类都被扫到会怎样?默认 beanName 都是类名首字母小写,发生冲突直接抛 ConflictingBeanDefinitionException。解法是自定义 BeanNameGenerator 或显式指定 @Component("xxx")
怎么批量把某个接口的所有实现纳入容器?@ComponentScan(includeFilters = @Filter(type = ASSIGNABLE_TYPE, classes = BaseHandler.class)),或用 ImportBeanDefinitionRegistrar 自己注册(MyBatis @MapperScan 就是这么做的)
@SpringBootApplication 默认扫哪些包?@ComponentScan 不写 basePackages 时扫当前配置类所在包及其子包。所以启动类必须放在最外层包,否则扫不到兄弟包
为什么 @Transactional 标在接口上有时失效?方法注解不继承,CGLIB 代理的是类,拿不到接口方法上的注解。规范是一律标在实现类的方法上
扫描慢怎么优化?缩小 basePackages、减少 ** 通配、用 @Import 显式导入代替大面积扫描;终极手段是 Spring 6 AOT——把扫描结果固化到编译期

✅ 项目关联话术

「我们内部 Starter 里做了一个 @BizService 组合注解,把 @Service、@Transactional(rollbackFor = Exception.class)、@Validated 合三为一,靠的就是 MergedAnnotations 递归搜元注解。好处是把团队规范编码进注解,新同学不会再漏写 rollbackFor。另外我们排查过启动慢的问题,发现有人把 @ComponentScan 写到了根包 com,连第三方依赖的包都被扫了一遍,收窄包路径后启动时间从 18 秒降到 9 秒。」

✍️ 手撕代码【核心实现 + 边界 Case 自测清单 + 变种拓展题】 ​

题 1:手写迷你 IOC 容器(注解扫描 + 实例化 + 字段注入 + 三级缓存解环) ​

考察点:能不能把"容器"这个黑盒拆成四个动作——扫描、注册 BeanDefinition、实例化、注入;以及三级缓存到底怎么落地。

java
// ========== 1. 注解定义 ==========
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyComponent {
    String value() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAutowired {
}

// ========== 2. BeanDefinition:Bean 的"身份证" ==========
public class BeanDefinition {
    private final String beanName;
    private final Class<?> beanClass;
    private String scope = "singleton";

    public BeanDefinition(String beanName, Class<?> beanClass) {
        this.beanName = beanName;
        this.beanClass = beanClass;
    }
    public String getBeanName()  { return beanName; }
    public Class<?> getBeanClass() { return beanClass; }
    public String getScope()     { return scope; }
    public void setScope(String scope) { this.scope = scope; }
}

// ========== 3. 容器核心 ==========
public class MiniApplicationContext {

    /** 一级缓存:完整可用的成品单例 */
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(64);
    /** 二级缓存:已曝光但未注入完成的半成品(可能是代理对象) */
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
    /** 三级缓存:对象工厂,延迟决定"暴露原始对象还是代理对象" */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
    /** 正在创建中的 Bean,用于检测构造器循环依赖 */
    private final Set<String> singletonsCurrentlyInCreation =
            Collections.synchronizedSet(new HashSet<>());

    private final Map<String, BeanDefinition> beanDefinitionMap = new LinkedHashMap<>();
    /** 模拟 BeanPostProcessor 链 */
    private final List<MyBeanPostProcessor> processors = new ArrayList<>();

    public MiniApplicationContext(String basePackage) {
        scan(basePackage);                 // ① 扫描并注册 BeanDefinition
        registerBeanPostProcessors();      // ② 先把后置处理器自己创建好
        preInstantiateSingletons();        // ③ 预实例化所有非懒加载单例
    }

    // ---------- ① 扫描 ----------
    private void scan(String basePackage) {
        for (Class<?> clazz : ClassScanner.scan(basePackage)) {
            MyComponent ann = clazz.getAnnotation(MyComponent.class);
            if (ann == null) continue;
            String beanName = ann.value().isEmpty()
                    ? Introspector.decapitalize(clazz.getSimpleName())
                    : ann.value();
            // ⚠️ 同名冲突必须快速失败,否则后注册的会静默覆盖前者
            if (beanDefinitionMap.containsKey(beanName)) {
                throw new IllegalStateException("重复的 beanName: " + beanName);
            }
            beanDefinitionMap.put(beanName, new BeanDefinition(beanName, clazz));
        }
    }

    private void registerBeanPostProcessors() {
        for (BeanDefinition bd : new ArrayList<>(beanDefinitionMap.values())) {
            if (MyBeanPostProcessor.class.isAssignableFrom(bd.getBeanClass())) {
                processors.add((MyBeanPostProcessor) getBean(bd.getBeanName()));
            }
        }
    }

    private void preInstantiateSingletons() {
        for (BeanDefinition bd : new ArrayList<>(beanDefinitionMap.values())) {
            if ("singleton".equals(bd.getScope())) {
                getBean(bd.getBeanName());
            }
        }
    }

    // ---------- ② 获取 Bean:三级缓存查找 ----------
    @SuppressWarnings("unchecked")
    public <T> T getBean(String beanName) {
        Object singleton = getSingleton(beanName, true);
        if (singleton != null) {
            return (T) singleton;
        }
        BeanDefinition bd = beanDefinitionMap.get(beanName);
        if (bd == null) {
            throw new NoSuchElementException("No bean named '" + beanName + "'");
        }
        if ("prototype".equals(bd.getScope())) {
            return (T) createBean(bd);     // prototype 不进缓存,也不解环
        }
        synchronized (singletonObjects) {
            singleton = getSingleton(beanName, true);
            if (singleton != null) return (T) singleton;
            if (!singletonsCurrentlyInCreation.add(beanName)) {
                throw new IllegalStateException("无法解决的循环依赖(构造器注入): " + beanName);
            }
            try {
                Object bean = createBean(bd);
                singletonObjects.put(beanName, bean);
                earlySingletonObjects.remove(beanName);
                singletonFactories.remove(beanName);
                return (T) bean;
            } finally {
                singletonsCurrentlyInCreation.remove(beanName);
            }
        }
    }

    /** 一级 → 二级 → 三级 依次查找,这就是 Spring 的 getSingleton(name, true) */
    private Object getSingleton(String beanName, boolean allowEarlyReference) {
        Object bean = singletonObjects.get(beanName);
        if (bean == null && singletonsCurrentlyInCreation.contains(beanName)) {
            bean = earlySingletonObjects.get(beanName);
            if (bean == null && allowEarlyReference) {
                ObjectFactory<?> factory = singletonFactories.get(beanName);
                if (factory != null) {
                    bean = factory.getObject();          // 这里才决定代理还是原始对象
                    earlySingletonObjects.put(beanName, bean); // 升级到二级,保证唯一
                    singletonFactories.remove(beanName);
                }
            }
        }
        return bean;
    }
}

创建流程:实例化 → 提前曝光 → 注入 → 初始化

java
private Object createBean(BeanDefinition bd) {
    String beanName = bd.getBeanName();
    Object raw;
    try {
        Constructor<?> ctor = bd.getBeanClass().getDeclaredConstructor();
        ctor.setAccessible(true);
        raw = ctor.newInstance();                       // ③ 实例化(空壳)
    } catch (Exception e) {
        throw new IllegalStateException("实例化失败: " + beanName, e);
    }

    // ④ 提前曝光:放入三级缓存。注意放的是 lambda,不是对象本身
    final Object exposed = raw;
    if ("singleton".equals(bd.getScope())) {
        singletonFactories.put(beanName, () -> getEarlyBeanReference(beanName, exposed));
    }

    populateBean(beanName, raw);                        // ⑤ 字段注入
    return initializeBean(beanName, raw);               // ⑥ 初始化 + 后置处理
}

/** 模拟 SmartInstantiationAwareBeanPostProcessor:决定提前曝光的是原始对象还是代理 */
private Object getEarlyBeanReference(String beanName, Object bean) {
    Object result = bean;
    for (MyBeanPostProcessor p : processors) {
        result = p.getEarlyBeanReference(beanName, result);
    }
    return result;
}

private void populateBean(String beanName, Object bean) {
    for (Field field : bean.getClass().getDeclaredFields()) {
        if (!field.isAnnotationPresent(MyAutowired.class)) continue;
        // 简化版:先按名字找,找不到按类型找
        String dependName = field.getName();
        Object dependency = beanDefinitionMap.containsKey(dependName)
                ? getBean(dependName)
                : getBeanByType(field.getType());
        field.setAccessible(true);
        try {
            field.set(bean, dependency);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("注入失败: " + beanName + "." + field.getName(), e);
        }
    }
}

private Object getBeanByType(Class<?> type) {
    List<String> candidates = beanDefinitionMap.values().stream()
            .filter(bd -> type.isAssignableFrom(bd.getBeanClass()))
            .map(BeanDefinition::getBeanName)
            .collect(Collectors.toList());
    if (candidates.isEmpty()) {
        throw new NoSuchElementException("找不到类型为 " + type.getName() + " 的 Bean");
    }
    if (candidates.size() > 1) {   // 对应 Spring 的 NoUniqueBeanDefinitionException
        throw new IllegalStateException("存在多个候选 Bean: " + candidates);
    }
    return getBean(candidates.get(0));
}

private Object initializeBean(String beanName, Object bean) {
    // Aware 回调
    if (bean instanceof MyBeanNameAware) {
        ((MyBeanNameAware) bean).setBeanName(beanName);
    }
    // BPP before
    Object result = bean;
    for (MyBeanPostProcessor p : processors) {
        result = p.postProcessBeforeInitialization(beanName, result);
    }
    // 初始化方法
    if (result instanceof MyInitializingBean) {
        ((MyInitializingBean) result).afterPropertiesSet();
    }
    // BPP after:AOP 代理通常在这里产生
    for (MyBeanPostProcessor p : processors) {
        result = p.postProcessAfterInitialization(beanName, result);
    }
    return result;
}

🔍 边界 Case 自测清单

#Case期望行为
1A 字段注入 B,B 字段注入 A两边都能拿到对方,启动成功
2A 构造器注入 B,B 构造器注入 A抛出"无法解决的循环依赖",而不是 StackOverflowError
3A 自己注入自己同样走三级缓存,能成功
4循环依赖中的 A 被 AOP 代理B 持有的必须是代理对象,不是原始对象(用 b.getA() == context.getBean("a") 断言)
5两个类同名 beanName启动期快速失败,不静默覆盖
6一个接口两个实现,按类型注入抛"存在多个候选 Bean",提示而不是随机挑一个
7注入字段类型找不到任何 Bean抛 NoSuchElementException,信息里带字段名
8prototype Bean 参与循环依赖直接报错(不进缓存无法解环),与 Spring 行为一致
9并发 getBean 同一个单例只实例化一次,双检锁生效
10getBean 一个不存在的名字抛异常而不是返回 null

🚀 变种拓展题

  1. 加上 @MyValue("${xxx}") 属性注入,并支持占位符解析(模拟 PropertySourcesPlaceholderConfigurer)。
  2. 支持 构造器注入:解析参数类型递归 getBean,并保留对循环依赖的快速失败。
  3. 支持 @MyLazy:注入时先放一个 JDK 代理,首次调用方法时才真正 getBean。
  4. 实现 destroy 回调:容器 close() 时逆序调用 DisposableBean,并解释为什么 prototype 不会被调用。
  5. 把二级缓存删掉,跑一遍 Case 4,观察"同一个 Bean 被代理两次"的现象,用它反向证明二级缓存的必要性。

题 2:手写 JDK 动态代理与 CGLIB 代理,实现简易 AOP 链 ​

考察点:两种代理的 API 差异、代理失效的本质、以及"多个通知如何串成责任链"。

java
// ========== 目标对象 ==========
public interface OrderService {
    String createOrder(String sku);
}

public class OrderServiceImpl implements OrderService {
    @Override
    public String createOrder(String sku) {
        System.out.println("  [target] 真正下单: " + sku);
        return "ORDER-" + sku;
    }
}

// ========== 1. JDK 动态代理:基于接口 ==========
public class JdkLogProxy implements InvocationHandler {

    private final Object target;

    private JdkLogProxy(Object target) { this.target = target; }

    @SuppressWarnings("unchecked")
    public static <T> T create(T target) {
        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),   // ⚠️ 只代理接口里的方法
                new JdkLogProxy(target));
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // Object 自带方法不要加增强,否则 equals/hashCode 行为会被污染
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(target, args);
        }
        long start = System.nanoTime();
        try {
            Object result = method.invoke(target, args);   // 环绕通知的"前后夹"
            System.out.println("  [jdk] " + method.getName() + " 返回: " + result);
            return result;
        } catch (InvocationTargetException e) {
            // ⚠️ 必须拆包,否则调用方看到的是 InvocationTargetException 而非业务异常
            System.out.println("  [jdk] 异常通知: " + e.getTargetException());
            throw e.getTargetException();
        } finally {
            System.out.println("  [jdk] 耗时 " + (System.nanoTime() - start) / 1000 + "us");
        }
    }
}

// ========== 2. CGLIB 代理:基于子类 ==========
public class CglibLogProxy implements MethodInterceptor {

    @SuppressWarnings("unchecked")
    public static <T> T create(Class<T> targetClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);              // 生成目标类的子类
        enhancer.setCallback(new CglibLogProxy());
        return (T) enhancer.create();                     // 会调用一次父类构造器
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)
            throws Throwable {
        if (Modifier.isFinal(method.getModifiers())) {     // final 方法根本不会走到这里
            return proxy.invokeSuper(obj, args);
        }
        System.out.println("  [cglib] before " + method.getName());
        try {
            // invokeSuper 走 FastClass,不走反射;invoke 会导致递归 StackOverflow
            return proxy.invokeSuper(obj, args);
        } finally {
            System.out.println("  [cglib] after " + method.getName());
        }
    }
}

把多个通知串成责任链(Spring 的 ReflectiveMethodInvocation 简化版)

java
public interface MyInterceptor {
    Object intercept(MyInvocation invocation) throws Throwable;
}

/** 维护一个游标,每次 proceed() 往后走一个拦截器,走完才调目标方法 */
public class MyInvocation {

    private final Object target;
    private final Method method;
    private final Object[] args;
    private final List<MyInterceptor> chain;
    private int cursor = -1;

    public MyInvocation(Object target, Method method, Object[] args,
                        List<MyInterceptor> chain) {
        this.target = target; this.method = method;
        this.args = args;     this.chain = chain;
    }

    public Object proceed() throws Throwable {
        if (++cursor == chain.size()) {
            return method.invoke(target, args);        // 链条走到底,执行真正业务
        }
        return chain.get(cursor).intercept(this);
    }

    public Method getMethod() { return method; }
    public Object[] getArgs()  { return args; }
}

// 三个拦截器,注册顺序即执行顺序
List<MyInterceptor> chain = Arrays.asList(
    inv -> {                                   // 事务(最外层)
        System.out.println("tx begin");
        try {
            Object r = inv.proceed();
            System.out.println("tx commit");
            return r;
        } catch (Throwable t) {
            System.out.println("tx rollback");
            throw t;
        }
    },
    inv -> {                                   // 日志
        System.out.println("log before " + inv.getMethod().getName());
        try { return inv.proceed(); }
        finally { System.out.println("log after"); }
    },
    inv -> {                                   // 参数校验(最内层)
        if (inv.getArgs()[0] == null) throw new IllegalArgumentException("sku 不能为空");
        return inv.proceed();
    }
);

执行顺序与"洋葱模型":

text
tx begin
  log before createOrder
    validate
      [target] 真正下单
    (返回)
  log after
tx commit

📌 单独澄清:为什么 proceed() 必须是实例方法而不是静态递归

游标 cursor 是 MyInvocation 的实例字段,一次方法调用对应一个 MyInvocation 实例。这也解释了 Spring 里为什么 MethodInvocation 不能复用、不能在多线程间共享——它是一次调用的上下文,不是单例组件。

🔍 边界 Case 自测清单

#Case期望行为
1JDK 代理一个没有接口的类抛异常/无法代理,验证"JDK 代理必须有接口"
2CGLIB 代理 final 类Enhancer.create() 直接报 Cannot subclass final class
3CGLIB 代理里存在 final 方法该方法不被增强,直接走原逻辑
4CGLIB 代理 private 方法不被增强(子类看不见父类私有方法)
5目标方法抛业务异常调用方捕获到的是业务异常本身,不是 InvocationTargetException
6目标类没有无参构造器CGLIB 用 Enhancer 创建会失败,验证"CGLIB 要调父类构造器"
7代理对象调 toString()/equals()不被日志增强污染
8链上某个拦截器不调 proceed()短路返回,目标方法根本不执行(权限拦截就是这么做的)
9同一个 MyInvocation 调两次 proceed()游标继续前进,验证"不可复用"
10CGLIB 中误用 proxy.invoke 代替 invokeSuper出现无限递归 StackOverflowError

🚀 变种拓展题

  1. 加上 切点匹配:只对带 @MyLog 注解的方法生效,注意注解在接口上还是实现类上的差异(AnnotationUtils.findAnnotation)。
  2. 实现 代理选型逻辑:有接口用 JDK、无接口用 CGLIB,再加一个 proxyTargetClass 开关强制 CGLIB。
  3. 把拦截器链接入题 1 的 MiniApplicationContext,在 postProcessAfterInitialization 里返回代理对象。
  4. 实现 exposeProxy:用 ThreadLocal 保存当前代理,提供 AopContext.currentProxy(),解决自调用失效。
  5. 用 ByteBuddy 或 ASM 重写一遍,对比启动耗时与首次调用耗时。

题 3:自定义注解 + 切面实战(耗时监控 / 审计日志 / 分布式幂等) ​

考察点:真实项目里 AOP 的落地写法,尤其是 @Around 的参数获取、SpEL 解析 key、以及切面之间的顺序。

java
// ========== 1. 幂等注解 ==========
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Idempotent {
    /** 支持 SpEL,如 "#order.orderNo" 或 "#userId + ':' + #sku" */
    String key();
    long expire() default 5;
    TimeUnit unit() default TimeUnit.SECONDS;
    String message() default "操作正在处理中,请勿重复提交";
}

// ========== 2. 切面实现 ==========
@Aspect
@Component
@Order(20)   // 数字越小越外层;要在事务(默认 Ordered.LOWEST_PRECEDENCE)外面
public class IdempotentAspect {

    private final StringRedisTemplate redisTemplate;
    private final ExpressionParser parser = new SpelExpressionParser();
    private final ParameterNameDiscoverer discoverer =
            new DefaultParameterNameDiscoverer();

    public IdempotentAspect(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
        String key = "idem:" + resolveKey(pjp, idempotent.key());
        String token = UUID.randomUUID().toString();

        Boolean ok = redisTemplate.opsForValue()
                .setIfAbsent(key, token, idempotent.expire(), idempotent.unit());
        if (!Boolean.TRUE.equals(ok)) {
            throw new BizException(idempotent.message());
        }
        try {
            return pjp.proceed();
        } catch (Throwable t) {
            // 业务失败要释放锁,允许重试;成功则让 key 自然过期防重放
            releaseIfOwner(key, token);
            throw t;
        }
    }

    /** ⚠️ 删锁必须校验持有者,否则会删掉别人的锁 */
    private void releaseIfOwner(String key, String token) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] "
                      + "then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(key), token);
    }

    private String resolveKey(ProceedingJoinPoint pjp, String spel) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        String[] names = discoverer.getParameterNames(method);
        Object[] args = pjp.getArgs();

        EvaluationContext ctx = new StandardEvaluationContext();
        if (names != null) {
            for (int i = 0; i < names.length; i++) {
                ctx.setVariable(names[i], args[i]);
            }
        }
        Object value = parser.parseExpression(spel).getValue(ctx);
        if (value == null) {
            throw new IllegalArgumentException("幂等 key 解析为空: " + spel);
        }
        return String.valueOf(value);
    }
}

耗时监控 + 审计日志切面

java
@Aspect
@Component
@Order(10)   // 最外层:无论幂等拦截还是事务回滚,耗时都要记到
public class MonitorAspect {

    private static final Logger log = LoggerFactory.getLogger(MonitorAspect.class);

    /** 切点复用:定义一次,到处引用 */
    @Pointcut("execution(public * com.example.*.controller..*.*(..))")
    public void controllerLayer() {}

    @Around("controllerLayer()")
    public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature sig = (MethodSignature) pjp.getSignature();
        String name = sig.getDeclaringType().getSimpleName() + "#" + sig.getName();
        long start = System.currentTimeMillis();
        String result = "success";
        try {
            return pjp.proceed();
        } catch (Throwable t) {
            result = "fail:" + t.getClass().getSimpleName();
            throw t;                              // ⚠️ 必须原样抛出,不能吞异常
        } finally {
            long cost = System.currentTimeMillis() - start;
            // 慢接口单独告警,正常接口只打 debug,避免日志爆量
            if (cost > 500) {
                log.warn("[slow] {} cost={}ms result={}", name, cost, result);
            } else if (log.isDebugEnabled()) {
                log.debug("[monitor] {} cost={}ms", name, cost);
            }
            Metrics.timer("api.cost", "api", name, "result", result)
                   .record(cost, TimeUnit.MILLISECONDS);
        }
    }
}

@Aspect
@Component
public class AuditAspect {

    /** 事务提交后再落审计,避免"审计有记录、业务已回滚" */
    @AfterReturning(pointcut = "@annotation(audit)", returning = "ret")
    public void afterReturning(JoinPoint jp, Audit audit, Object ret) {
        AuditEvent event = new AuditEvent(audit.action(), currentUserId(),
                JSON.toJSONString(jp.getArgs()), String.valueOf(ret));
        // 发布事件 + @TransactionalEventListener(AFTER_COMMIT) 消费
        publisher.publishEvent(event);
    }
}

📌 单独澄清:幂等切面必须在事务切面的"外面"

顺序结果
❌ 事务在外、幂等在内事务先开启 → 加锁 → 业务失败回滚 → 但锁的释放在事务提交之前/之后不可控,容易出现"事务已回滚、锁还在",用户重试被拦
✅ 幂等在外、事务在内先抢锁 → 再开事务 → 失败时事务已回滚完毕,切面才释放锁,状态一致

同样的道理适用于分布式锁切面:@Order 必须小于事务切面(事务默认 Ordered.LOWEST_PRECEDENCE,即最内层),否则就是经典的"锁在事务内部,锁释放早于事务提交"导致并发穿透。

🔍 边界 Case 自测清单

#Case期望行为
1同一 key 并发两次请求只有一个进入业务,另一个抛"请勿重复提交"
2业务抛异常锁被释放,允许立即重试
3业务成功锁不删除,靠过期时间防重放
4SpEL 引用了不存在的参数名启动/运行时明确报错,而不是把 key 变成 null
5编译未加 -parametersgetParameterNames 返回 null,需降级为 #p0 索引写法
6切面方法内部调同类另一个带注解的方法不生效(自调用),验证代理边界
7目标方法是 private / final切面不生效
8释放锁时锁已过期并被他人持有Lua 脚本校验失败,不误删
9幂等切面与事务切面顺序颠倒复现"事务回滚但锁未释放"的问题
10监控切面中目标方法抛异常异常原样向上抛,同时耗时指标照常上报

🚀 变种拓展题

  1. 把幂等改成支持返回上次结果:Redis 里存结果快照,重复请求直接返回首次结果(真正的幂等语义)。
  2. 加 @RateLimit 限流切面,用 Redis + Lua 实现滑动窗口,并与幂等切面排好 @Order。
  3. 用 @Around + TransactionSynchronizationManager 实现"事务提交后才释放锁"。
  4. 把审计从 @AfterReturning 改成 @TransactionalEventListener(phase = AFTER_COMMIT),对比两者在回滚场景下的差异。
  5. 用 @DeclareParents(Introduction)给一批 Service 动态"植入"一个新接口,体会 AOP 的引介增强。

题 4:自定义扩展点实战(BFPP 改定义 + BPP 包装对象 + 自定义注解注入) ​

考察点:两类后置处理器的作用阶段差异,以及"改 BeanDefinition"与"换 Bean 实例"分别该用谁。

java
// ========== 1. BeanFactoryPostProcessor:批量改"图纸" ==========
@Component
public class ScopeAndLazyBfpp implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        for (String name : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition bd = beanFactory.getBeanDefinition(name);
            String className = bd.getBeanClassName();
            if (className == null) continue;

            // 把所有 XxxTask 结尾的 Bean 改成懒加载,缩短启动时间
            if (className.endsWith("Task")) {
                bd.setLazyInit(true);
            }
            // 给带 @Prototype 标记的类改作用域(注意此时类还没实例化)
            if (className.endsWith("Context")) {
                bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
            }
        }
    }
}

// ========== 2. BeanDefinitionRegistryPostProcessor:编程式注册新定义 ==========
@Component
public class MapperRegistrar implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
        // 模拟 MyBatis:扫描接口,为每个接口注册一个 FactoryBean
        for (Class<?> mapper : ClassScanner.scanInterfaces("com.example.mapper")) {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder
                    .genericBeanDefinition(MapperFactoryBean.class);
            builder.addConstructorArgValue(mapper);          // 把接口类型传给 FactoryBean
            builder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            registry.registerBeanDefinition(
                    Introspector.decapitalize(mapper.getSimpleName()),
                    builder.getBeanDefinition());
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // 这一步在上面之后执行,此时已能看到新注册的定义
    }
}

/** FactoryBean:getObject() 返回的才是容器里的 Bean */
public class MapperFactoryBean<T> implements FactoryBean<T> {

    private final Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    @SuppressWarnings("unchecked")
    public T getObject() {
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(),
                new Class[]{mapperInterface},
                (proxy, method, args) -> {
                    if (method.isDefault()) {               // 别漏了接口默认方法
                        return InvocationHandler.invokeDefault(proxy, method, args);
                    }
                    return SqlExecutor.execute(mapperInterface, method, args);
                });
    }

    @Override
    public Class<?> getObjectType() { return mapperInterface; }  // ⚠️ 必须返回真实类型

    @Override
    public boolean isSingleton() { return true; }
}

BeanPostProcessor:自定义注解注入 + 条件包装

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyRedisKey {
    String prefix();
}

@Component
public class MyRedisKeyBpp implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        // 属性注入类型的扩展放在 before(与 @Autowired 的处理阶段一致)
        ReflectionUtils.doWithFields(bean.getClass(), field -> {
            MyRedisKey ann = field.getAnnotation(MyRedisKey.class);
            if (ann == null) return;
            if (field.getType() != String.class) {
                throw new BeanCreationException(beanName,
                        "@MyRedisKey 只能标注在 String 字段上: " + field.getName());
            }
            ReflectionUtils.makeAccessible(field);
            ReflectionUtils.setField(field, bean, ann.prefix() + ":" + appEnv());
        });
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // 对象替换/代理放在 after(AOP 也在这一阶段)
        if (!(bean instanceof OrderService)) {
            return bean;
        }
        return Proxy.newProxyInstance(bean.getClass().getClassLoader(),
                bean.getClass().getInterfaces(),
                (proxy, method, args) -> {
                    System.out.println("[bpp-proxy] " + method.getName());
                    try {
                        return method.invoke(bean, args);
                    } catch (InvocationTargetException e) {
                        throw e.getTargetException();
                    }
                });
    }
}

📌 单独澄清:为什么 BPP 里不能随便 @Autowired 业务 Bean

BPP 自身必须早于普通 Bean 被创建。如果在 BPP 里用 @Autowired 注入一个业务 Bean,就会把那个业务 Bean 的创建时机提前到"所有 BPP 都还没注册完"的时刻,导致它错过后续的 BPP 增强(典型症状:事务注解、AOP 切面对它全部失效,日志里出现 is not eligible for getting processed by all BeanPostProcessors)。

✅ 正确做法:BPP 里依赖的东西改成延迟获取。

java
// ❌ 错误:把 DataSource 的创建提前,它可能错过 AOP/事务增强
@Component
public class BadBpp implements BeanPostProcessor {
    @Autowired private DataSource dataSource;
}

// ✅ 正确:持有 BeanFactory,用的时候再取
@Component
public class GoodBpp implements BeanPostProcessor, BeanFactoryAware {
    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (bean instanceof ReportService) {
            DataSource ds = beanFactory.getBean(DataSource.class);  // 用时才取
            // ...
        }
        return bean;
    }
}

🔍 边界 Case 自测清单

#Case期望行为
1BFPP 里调用 beanFactory.getBean()会提前实例化该 Bean,启动日志出现"not eligible"警告
2BFPP 里改作用域为 prototype生效(此时还没实例化)
3BPP 里改 BeanDefinition无效,因为实例已创建,验证"阶段不可逆"
4MapperFactoryBean.getObjectType() 返回 null按类型注入失败 / 提前实例化,验证它必须返回真实类型
5用 getBean("mapperFactoryBean") 与 getBean("&mapperFactoryBean")前者拿产品,后者拿工厂本身
6@MyRedisKey 标在 int 字段上启动期快速失败并提示字段名
7@MyRedisKey 标在 static final 字段上明确拒绝(final 字段反射赋值行为不可靠)
8postProcessAfterInitialization 返回 null容器里拿到的 Bean 变成 null,验证"必须返回 bean"
9BPP 返回 JDK 代理,但目标类无接口注入按具体类型时失败,验证代理类型必须兼容
10两个 BPP 都包装同一个 Bean形成嵌套代理,确认执行顺序由 @Order 决定

🚀 变种拓展题

  1. 实现一个 @MyValue + 配置中心热更新:BPP 记录字段位置,收到配置变更事件后反射刷新。
  2. 实现 接口默认实现兜底:扫描到接口无实现类时,自动注册一个返回默认值的代理 Bean。
  3. 用 InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation 返回对象,验证它会直接跳过容器后续的实例化与注入流程。
  4. 用 MergedBeanDefinitionPostProcessor 预先缓存需要注入的字段元数据,对比每次反射扫描的性能差异。
  5. 写一个 BPP 统计每个 Bean 的初始化耗时,输出启动耗时 Top 10(排查慢启动的常用手段)。

题 5:复现并修复三类经典故障(自调用失效 / 循环依赖 / 单例状态污染) ​

考察点:能不能写出可运行的最小复现,并给出两种以上修复方案,这是面试里最能体现"真踩过坑"的部分。

java
// ========== 故障一:@Transactional 自调用失效 ==========
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired private OrderMapper orderMapper;
    /** ✅ 修复方案 A:注入自己的代理(Spring 会用三级缓存解掉这个自引用) */
    @Autowired @Lazy private OrderService self;

    @Override
    public void createBatchBad(List<Order> orders) {
        for (Order o : orders) {
            saveOne(o);          // ❌ this.saveOne(),绕过代理,@Transactional 完全失效
        }
    }

    @Override
    public void createBatchGoodA(List<Order> orders) {
        for (Order o : orders) {
            self.saveOne(o);     // ✅ 走代理,事务生效
        }
    }

    @Override
    public void createBatchGoodB(List<Order> orders) {
        for (Order o : orders) {
            // ✅ 修复方案 B:AopContext(需 @EnableAspectJAutoProxy(exposeProxy = true))
            ((OrderService) AopContext.currentProxy()).saveOne(o);
        }
    }

    @Transactional(rollbackFor = Exception.class,
                   propagation = Propagation.REQUIRES_NEW)
    @Override
    public void saveOne(Order o) {
        orderMapper.insert(o);
        if (o.getAmount() < 0) {
            throw new IllegalStateException("金额非法");
        }
    }
}

/** ✅ 修复方案 C(最推荐):把事务方法挪到另一个 Bean,彻底消除自调用 */
@Service
public class OrderWriter {
    @Autowired private OrderMapper orderMapper;

    @Transactional(rollbackFor = Exception.class)
    public void saveOne(Order o) { orderMapper.insert(o); }
}

验证事务是否真的生效(不要靠"看起来对")

java
@SpringBootTest
class TxProxyTest {

    @Autowired private OrderService orderService;

    @Test
    void 事务管理器应被真正激活() {
        // 方式一:断言当前是代理对象
        assertTrue(AopUtils.isAopProxy(orderService));

        // 方式二:在方法内部断言事务同步器已激活(最可靠)
        //   TransactionSynchronizationManager.isActualTransactionActive()
        //   自调用场景下这里会返回 false,一眼看出失效
    }

    @Test
    void 自调用场景下第二条应已落库未回滚() {
        List<Order> orders = List.of(new Order(10), new Order(-1));
        assertThrows(IllegalStateException.class,
                () -> orderService.createBatchBad(orders));
        // ❌ 失效的证据:第一条数据留在库里
        assertEquals(1, orderMapper.count());
    }
}

故障二:循环依赖的三种结局

java
// ① 字段注入 + 单例 → Spring 能解(三级缓存)
@Service class A1 { @Autowired B1 b; }
@Service class B1 { @Autowired A1 a; }

// ② 构造器注入 → 无法解,启动直接失败
@Service class A2 { A2(B2 b) {} }
@Service class B2 { B2(A2 a) {} }
// BeanCurrentlyInCreationException: Requested bean is currently in creation

// ③ 构造器注入 + @Lazy → 能解(注入的是延迟代理)
@Service class A3 { A3(@Lazy B3 b) {} }
@Service class B3 { B3(A3 a) {} }

// ④ 字段注入 + 但 A 被 AOP 代理且用了 @Async → 可能失败
//    BeanCurrentlyInCreationException / 或拿到的是未增强的原始对象
@Service class A4 { @Autowired B4 b; @Async public void run() {} }
@Service class B4 { @Autowired A4 a; }

四种破环手段(按推荐度排序):

text
1. 重构:抽出第三个 Bean 承载公共逻辑     ← 根治,面试首选答案
2. 改成 setter/字段注入                  ← 能跑但掩盖了设计问题
3. @Lazy 注入延迟代理                    ← 局部缓解
4. ObjectProvider / ApplicationContextAware 运行期取 ← 彻底打破创建期依赖
   (spring.main.allow-circular-references=true 只是放开校验,不是修复)

故障三:单例里塞请求级状态

java
// ❌ 错误:成员变量在多线程下互相覆盖
@Service
public class BadContextHolder {
    private String currentUserId;                       // 所有请求共享这一个字段
    public void set(String id) { this.currentUserId = id; }
    public String get() { return currentUserId; }
}

// ✅ 方案 A:ThreadLocal,注意 finally 一定要 remove
@Service
public class GoodContextHolder {
    private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
    public void set(String id) { HOLDER.set(id); }
    public String get() { return HOLDER.get(); }
    public void clear() { HOLDER.remove(); }            // 不 remove → 线程池下串号 + 内存泄漏
}

// ✅ 方案 B:request 作用域 + 作用域代理
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
       proxyMode = ScopedProxyMode.TARGET_CLASS)        // ⚠️ 少了它注入到单例里会报错
public class RequestContext {
    private String userId;
    public void setUserId(String userId) { this.userId = userId; }
    public String getUserId() { return userId; }
}

🔍 边界 Case 自测清单

#Case期望行为
1createBatchBad 中途抛异常已插入的数据不会回滚,复现失效
2createBatchGoodA/B/C 中途抛异常该条回滚,前面成功的保留(REQUIRES_NEW 语义)
3在方法里打印 isActualTransactionActive()自调用场景为 false,走代理为 true
4AopContext.currentProxy() 未开 exposeProxy抛 IllegalStateException,提示没有暴露代理
5@Autowired private OrderService self 不加 @LazyBoot 2.6+ 默认报循环依赖错误
6构造器循环依赖启动失败,错误信息里能看到完整依赖环
7构造器 + @Lazy启动成功,首次调用方法时才真正初始化对方
8字段循环依赖 + 其中一个加 @Async复现"拿到未增强对象"或启动报错
9ThreadLocal 不 remove + 线程池复用第二个请求读到上一个请求的 userId
10request 作用域 Bean 去掉 proxyMode启动即报 No thread-bound request found
11在 @Async 线程里读 ThreadLocal读不到(需 TaskDecorator 传递上下文)
12事务方法抛受检异常且未配 rollbackFor不回滚,复现默认只回滚 RuntimeException

🚀 变种拓展题

  1. 写一个启动期自检组件:实现 SmartInitializingSingleton,遍历所有 Bean,检查"@Transactional 方法是否被同类其它方法直接调用",发现即启动失败(可用 ASM 分析字节码)。
  2. 实现 TaskDecorator,把 ThreadLocal/MDC 的 traceId 透传到 @Async 线程池,并处理"任务执行完要清理"。
  3. 用 @TransactionalEventListener 把"事务提交后发 MQ"落地,复现并修复"事务未提交就发消息导致消费者查不到数据"。
  4. 把三个故障各写成一个失败的单元测试,再逐个修复成绿色,形成自己的回归用例集。
  5. 打开 spring.main.allow-circular-references=true 跑用例,解释为什么"能启动"不等于"没问题"(启动顺序敏感、代理对象可能不一致)。

⚠️ 当日踩坑实录 ​

❌ 坑✅ 解决方案
@Transactional 方法被同类其它方法 this.xxx() 调用,事务完全失效,日志里看不到任何提示本质是没走代理。三种修复:① 把事务方法挪到另一个 Bean(推荐);② @Autowired @Lazy 注入自己;③ @EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy()。验证手段用 TransactionSynchronizationManager.isActualTransactionActive(),不要靠肉眼
事务方法抛了受检异常(如 IOException),数据却没回滚默认只对 RuntimeException 和 Error 回滚。统一加 @Transactional(rollbackFor = Exception.class),并写进团队规范/代码检查
事务方法里 try-catch 吞掉异常,以为"catch 了就回滚"异常被吞,代理感知不到失败 → 照常提交。要么原样抛出,要么在 catch 里手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
@Transactional 加在 private / final / static 方法上,没有任何报错也没有效果CGLIB 靠生成子类覆写方法,这三类方法无法被覆写。改成 public 非 final 实例方法
把 @Transactional 加在 @Async 方法或新建线程里的逻辑上,事务莫名失效事务上下文绑定在 ThreadLocal 上,换线程即丢失。异步任务内部要自己开事务,且外层事务不能指望它一起回滚
Spring Boot 2.6 升级后启动直接失败:Circular references are not allowed2.6 起默认禁止循环依赖。正确做法是重构解环(抽第三个 Bean / 改成运行期 ObjectProvider 获取);spring.main.allow-circular-references=true 只是临时放开校验,不是修复
构造器注入出现循环依赖,报 BeanCurrentlyInCreationException,以为是 Spring 的 bug构造器注入发生在实例化阶段,连"半成品"都造不出来,三级缓存无从下手。这其实是优点——把设计问题暴露在启动期。临时可用 @Lazy,根治靠重构
在构造器里使用 @Autowired 字段,必 NPE字段注入发生在实例化之后。依赖在构造期就要用 → 改构造器注入;只是初始化逻辑 → 挪到 @PostConstruct
循环依赖 + AOP 场景下,B 持有的 A 竟然是没有被切面增强的原始对象说明提前曝光时没有走 getEarlyBeanReference。排查是否自定义了 BPP 却没实现 SmartInstantiationAwareBeanPostProcessor;用 b.getA() == context.getBean("a") 断言同一性
以为 @Order 能控制 Bean 的创建顺序,加上之后依然报依赖未就绪@Order 只影响集合注入的排序和切面/处理器的执行顺序,与实例化顺序无关。要控创建顺序用 @DependsOn,或直接声明依赖关系
给配置类加 proxyBeanMethods = false 后,线上出现两个 DataSource,连接池翻倍Lite 模式下 @Bean 方法互调就是普通 Java 调用,每次 new 一个新对象。规范:@Bean 之间一律通过方法参数声明依赖,不直接互相调用
单例 Service 里用成员变量存当前用户/当前请求参数,压测时数据串号单例的实例变量被所有线程共享。改用 ThreadLocal,或 @Scope(value = "request", proxyMode = TARGET_CLASS)
ThreadLocal 用完没 remove(),线程池复用后读到上一个请求的数据,还伴随内存泄漏finally 里必须 remove()。Web 层统一用拦截器的 afterCompletion 兜底清理
@Scope("request") 的 Bean 注入到单例里,启动就报 No thread-bound request found必须加 proxyMode = ScopedProxyMode.TARGET_CLASS,注入的是作用域代理,每次方法调用时才去当前请求找真实对象
prototype Bean 的 @PreDestroy / destroy-method 从来不执行容器不管理 prototype 的销毁,交出去就不再持有引用。需要清理资源的自己 try-finally,或注册 DisposableBeanAdapter
把 prototype Bean 用 @Autowired 注入单例,结果每次拿到的都是同一个实例注入只发生一次。改用 ObjectProvider<T> / @Lookup / proxyMode 三种方式之一,在每次使用时向容器申请新实例
在 BeanPostProcessor 里 @Autowired 了一个业务 Bean,结果那个 Bean 的事务和切面全部失效启动日志里会有 is not eligible for getting processed by all BeanPostProcessors。BPP 里改成实现 BeanFactoryAware,用的时候再 getBean,不要在字段上注入
自定义 BeanPostProcessor 的 postProcessAfterInitialization 忘记 return bean,返回了 null,容器里 Bean 全空两个回调都必须返回对象;不做处理时原样返回入参
自定义 FactoryBean 的 getObjectType() 返回 null,按类型注入失败或 Bean 被提前实例化必须返回真实产品类型,容器靠它做类型匹配而不实例化。想拿工厂本身用 &beanName
在 BeanFactoryPostProcessor 里调 getBean(),启动日志出现一堆 "not eligible" 警告BFPP 阶段只应该改 BeanDefinition,不要触发实例化。确实要拿实例请挪到 ApplicationRunner / SmartInitializingSingleton
@PostConstruct 里调用其它 Bean 的方法报错或拿到半成品@PostConstruct 时自己虽已注入完成,但依赖方可能仍在初始化中。需要"全部 Bean 就绪"才执行的逻辑,用 ApplicationRunner 或 ApplicationReadyEvent
@Async 方法被同类调用不生效与事务自调用完全同源——都依赖代理。修复方式一致:挪到另一个 Bean
@Async 没配线程池,并发一上来就创建海量线程默认是 SimpleAsyncTaskExecutor,每次新建线程不复用。必须自定义 ThreadPoolTaskExecutor 并实现 AsyncConfigurer
@Async 返回 void 时异常被静默吞掉,线上问题完全无感实现 AsyncUncaughtExceptionHandler 统一兜底,或把返回值改成 CompletableFuture 让调用方感知
分布式锁/幂等切面加在事务切面内层,并发依然穿透锁的释放早于事务提交,形成"锁已放、数据未可见"的窗口。给锁切面设更小的 @Order(事务默认是最低优先级即最内层)
ApplicationEvent 发布后以为是异步的,结果接口响应变慢Spring 事件默认同步,在同一线程同一事务内执行。要异步请配 SimpleApplicationEventMulticaster 的 taskExecutor 或给监听器加 @Async
事务里发布事件通知下游,下游查不到数据监听器在事务提交前执行。改用 @TransactionalEventListener(phase = AFTER_COMMIT)
一个接口两个实现,@Autowired 启动报 NoUniqueBeanDefinitionException五种手段任选:@Primary、@Qualifier、字段名与 beanName 一致、注入 Map<String, T> 做策略路由、@Order + List<T>
@ConditionalOnMissingBean 加在普通 @Component 上,行为随机它判断的是"评估那一刻容器里有没有",强依赖注册顺序。只应该用在自动配置类里,并配合 @AutoConfigureAfter
Spring MVC 下 @Service 在 Controller 里注入不到,或事务不生效父子容器问题:子容器(DispatcherServlet)能看父容器,反之不行。确保扫描范围不重叠——父容器扫 Service/Dao,子容器只扫 Controller
升级 Spring Boot 3 后大量编译错误:找不到 javax.servletJakarta EE 命名空间迁移,javax.* 全部变成 jakarta.*。同时注意 JDK 17 起步、三方库需同步升版本
打成 GraalVM 原生镜像后,运行期动态注册 BeanDefinition / 反射造类的代码全部失效AOT 把 BeanDefinition 固化到编译期,运行期不再扫描。把动态逻辑改成 @Conditional 条件装配,或补 reflect-config.json 提示文件
@Value 注入 static 字段,值永远是 null / 0依赖注入只作用于实例字段,static 会被直接跳过且不报错,最隐蔽的一类事故。改成实例字段,或在 @PostConstruct 里手动赋值给静态变量
@Value("${a.b}") 对应的配置项不存在,服务启动直接失败占位符解析不到就抛 IllegalArgumentException。所有 @Value 都应写默认值:${a.b:3000};要注入 null 写 ${a.b:#{null}}
用 @Value 注入 List<String>,拿到的是一个带逗号的整串@Value 不会自动切割。写成 #{'${a.hosts}'.split(',')}(单引号不能省),或直接改用 @ConfigurationProperties
yml 里写 db-url,@Value("${dbUrl}") 取不到@Value 不支持松散绑定,名字必须一字不差。松散绑定是 @ConfigurationProperties 的能力
@ConfigurationProperties 类加了注解但字段全是默认值忘了注册。三种方式之一:类上 @Component、配置类 @EnableConfigurationProperties(X.class)、启动类 @ConfigurationPropertiesScan
在 @ConfigurationProperties 类的构造器里读配置,拿到的是 null绑定由 ConfigurationPropertiesBindingPostProcessor 在初始化前完成,比构造器晚。要在构造器里用就改成构造器绑定(record / @ConstructorBinding)
配置中心改了值,@Value 字段却不变注入后就是普通字段,原生不支持热更新。需要 @RefreshScope(作用域代理 + 销毁重建)或监听配置事件手动 set
@PropertySource("classpath:x.yml") 读不到任何值它默认只支持 properties 格式,yml 需自定义 PropertySourceFactory。常规配置请放 application.yml 走 Boot 的加载链
把 @Component 加在抽象基类上,以为所有子类都会被注册类注解默认不继承(@Component 无 @Inherited),抽象类还会被扫描器排除。要批量纳入请用 ASSIGNABLE_TYPE 过滤器或在每个实现类上标注
@Transactional 标在接口方法上,CGLIB 代理下时而生效时而不生效方法注解不继承,CGLIB 代理的是类。规范:事务注解一律标在实现类的方法上
启动类放在了子包里,Service 一个都扫不到不写 basePackages 时只扫启动类所在包及子包。启动类必须放在最外层包
@ComponentScan 图省事写到根包 com,启动变慢十几秒第三方依赖的包也被逐个 ASM 扫了一遍。收窄 basePackages,或用 @Import 显式导入
两个不同包下的同名类都被扫到,启动报 ConflictingBeanDefinitionException默认 beanName 都是类名首字母小写导致冲突。显式写 @Component("xxx") 或自定义 BeanNameGenerator
缓存了 ReflectiveMethodInvocation 对象想复用,并发下执行顺序错乱它用 currentInterceptorIndex 记录推进位置,有状态、天生不可复用,每次调用必须新建

📌 当日复盘任务 ​

IOC 与容器

  • [ ] 用一句话说清 IOC 反转的是控制权而不是创建动作,并写出 ✅/❌ 对照代码
  • [ ] 画出 BeanFactory → ApplicationContext 的继承关系,列出三条核心差异
  • [ ] 解释 FactoryBean 与 BeanFactory 的区别,并说出 &beanName 的作用
  • [ ] 说明 BeanDefinition 里至少 6 个关键属性,以及它从哪三类配置来源解析而来
  • [ ] 写出 @Configuration Full / Lite 两种模式的差异,解释 proxyBeanMethods = false 的风险
  • [ ] 背出 refresh() 的十二步骨架,并指出其中三个最关键的步骤

依赖注入

  • [ ] 对比构造器 / setter / 字段注入的四个维度,说明为什么官方推荐构造器
  • [ ] 解释"构造器注入解决不了循环依赖"为什么是优点
  • [ ] 列出一接口多实现的五种消歧手段,并各写一段代码
  • [ ] 对比 @Autowired 与 @Resource 的来源、匹配顺序、必填性差异
  • [ ] 说明 @Autowired 注入 List<T> / Map<String, T> 的典型用途(策略模式)

AOP 与代理

  • [ ] 背出七大术语与五种通知,画出单切面与多切面的执行顺序
  • [ ] 写出 JDK 动态代理与 CGLIB 的代码骨架,说出各自的三个限制
  • [ ] 解释 Spring AOP 与 AspectJ 的根本差异(运行期织入 vs 编译期织入)
  • [ ] 列出 AOP 失效的全部场景:自调用、private、final、static、非 Spring 管理对象
  • [ ] 解释 @Order 在切面上的含义,并说明锁/幂等切面为什么要在事务外层
  • [ ] 手写一遍 MethodInvocation 责任链,解释为什么它不能复用

Bean 生命周期

  • [ ] 完整背出 12 步生命周期链路,标出每一步可插入的扩展点
  • [ ] 对比 BFPP 与 BPP 的执行阶段、能改什么、不能改什么
  • [ ] 排出 @PostConstruct、InitializingBean、init-method 三者的执行顺序
  • [ ] 解释为什么构造器里拿不到 @Autowired 字段
  • [ ] 列出 Aware 家族至少 6 个接口及其用途,写一个 SpringUtil 工具类
  • [ ] 说明 SmartInitializingSingleton、ApplicationRunner、ApplicationReadyEvent 的触发时机差异

作用域与线程安全

  • [ ] 列出六种作用域,说明 singleton 与 prototype 的管理边界差异
  • [ ] 解释"单例线程不安全的根源是实例变量",写出 ✅/❌ 对照代码
  • [ ] 说出 prototype 的两个经典陷阱与三种获取新实例的手段
  • [ ] 实现 request 作用域 Bean,解释 proxyMode 缺失会报什么错
  • [ ] 写出 ThreadLocal 的正确使用模板,说明不 remove 的两个后果

循环依赖

  • [ ] 画出 A⇄B 字段注入的三级缓存完整推演流程
  • [ ] 回答"为什么必须三级缓存、两级不够",并说出二级缓存的两个作用
  • [ ] 列出 Spring 能解 / 不能解循环依赖的全部场景
  • [ ] 说明 Spring Boot 2.6+ 默认禁止循环依赖的原因与正确应对姿势
  • [ ] 写出 getEarlyBeanReference 的调用链,解释代理对象如何被提前曝光

事务与事件

  • [ ] 列出 @Transactional 失效的七种场景,每种写出 ❌/✅ 成对代码
  • [ ] 背出七种传播行为,重点说清 REQUIRES_NEW 与 NESTED 的差异
  • [ ] 说明事务回滚的默认规则与 rollbackFor 的必要性
  • [ ] 解释 Spring 事件默认同步的事实,写出异步化与事务后置消费两种改法

配置注入与组件扫描

  • [ ] 画出 Environment 的属性源顺序,说明「命令行 > 环境变量 > profile 配置 > 主配置 > 默认值」的由来
  • [ ] 说清 @Value 的完整注入链路:BFPP 注册解析器 → ${} 替换 → #{} SpEL → ConversionService 转换
  • [ ] 对比 ${} 与 #{},写出 #{'${a.hosts}'.split(',')} 并解释单引号为什么不能省
  • [ ] 列出 @Value 与 @ConfigurationProperties 的六个维度差异,说出各自适用场景
  • [ ] 写一个 @ConfigurationProperties + @Validated + 构造器绑定的配置类,验证非法配置启动期失败
  • [ ] 复现 @Value 注入 static 字段为 null 的事故,解释为什么它不报错
  • [ ] 背出 ClassPathBeanDefinitionScanner#doScan 七步,重点说清「ASM 读元数据不加载类」
  • [ ] 自定义一个 @BizService 组合注解(@Service + @Transactional + @Validated),验证其生效
  • [ ] 解释类注解为什么默认不继承,以及批量纳入某接口全部实现的两种做法

设计模式

  • [ ] 把 11 个设计模式各指到一个 Spring 具体类,并说出一句关键细节
  • [ ] 用「模板方法定骨架 → 工厂产 Bean → 注册表存单例 → 装饰扩展 → 代理增强 → 责任链串联 → 适配器统一」一句话串起整个容器

动手任务

  • [ ] 手写迷你 IOC 容器,跑通字段循环依赖并断言代理对象同一性
  • [ ] 手写 JDK + CGLIB 两种代理,并实现拦截器责任链
  • [ ] 写一个幂等切面(Redis + Lua + SpEL key),排好与事务的 @Order
  • [ ] 写一个 BPP 统计所有 Bean 的初始化耗时,输出启动慢 Top 10
  • [ ] 把"自调用事务失效""构造器循环依赖""单例状态污染"各写成一个失败用例并修复
  • [ ] 跑一遍把二级缓存删掉的实验,用现象反向证明它的必要性

面试表达

  • [ ] 10 道高频题的 90 秒口述答案各复述一遍,录音回听卡顿处
  • [ ] 每道题准备一段真实项目关联话术,确保"原理 + 踩坑 + 修复 + 规范"四段式完整
  • [ ] 准备 Spring 6 / Boot 3(AOT、jakarta 迁移)的加分表述,控制在 30 秒内