一、bean的生命周期
1.创建
ApplicationContext ctx=new AnnotionConfigApplicationContext(App.class);
2.实例化
Object rawBean=clazz.getConstructor().newInstance();//反射创建
3.依赖注入(属性赋值)
populateBean(beanName,mbd,instanceWrapper);//属性赋值
4.初始化
initializeBean(beanName,exposedObject,mbd);//初始化
5.生成动态代理
return applyBeanPostProcessorAfterInitialization(wrappedBean,beanName);//aop封装
6.放入单例池后续使用
singletonObjects.put(beanName,proxyBean);//放入单例池
7.销毁
destoryBean(beanName,bean);//容器关闭,资源销毁
二、如何解决Spring多线程事务失效的问题?
当使用事务注解,底层就会给你创建一个事务的动态代理开启事务,就会把你开启事务的信息放到Theadlocal 当你调用后面一个方法,后面方法动态台历通过Theadlocal.get()方法获取外层事务信息。你new一个新线程调用那样子就拿不到外层线程的事务信息。通过下面的代码的样例信息就可以把外层事务传递下去(可以学习下Spring源码里面的事务模块)
代码样例:
@Service
public class TranscationalDemoServiceImpl(
@Autowired
private JdbcTemplete jdbcTemplete;
@Autowired
private DataSource datasource;
@Transcational
public void testA() throws Exception{
ConnectionHolder cnt=(ConnectionHolder)
TranscationalSynchronizationManager.getResource(datasource);
jdbcTemplete.execute(" insert .......");
TranscationalDemoService transcationalDemoService=(TranscationalDemoService)
AopContext.currentProxy();
Thread thread=new Thread(()->{
TranscationalSynchronizationManager.bindResource(datasource,cnt);
transcationalDemoService.testA();
});
thread.start;
thread.join();
}
@Transcational
public void testA(){
//执行更新
}
)
三、Spring Bean的循环依赖是怎么解决的?二级缓存不行吗,为什么用三级缓存?
Spring Bean 的循环依赖解决原理
Spring 使用三级缓存 来解决单例 Bean 之间的setter 注入(或字段注入) 造成的循环依赖。对于构造器注入的循环依赖,Spring 无法解决,会直接抛出异常。
三级缓存结构
Spring 在 DefaultSingletonBeanRegistry 中维护了三个 Map:
|------|-------------------------|------------------------------------------------------|
| 缓存名称 | 变量名 | 作用 |
| 一级缓存 | singletonObjects | 存放完全初始化完成的单例 Bean(成品) |
| 二级缓存 | earlySingletonObjects | 存放提前暴露的、尚未填充属性&初始化的 Bean 实例(半成品),用于解决循环依赖 |
| 三级缓存 | singletonFactories | 存放 ObjectFactory (对象工厂),用于生成 Bean 的早期引用(可生成代理对象) |
解决循环依赖的核心流程(以 A→B→A 为例)
假设有两个 Bean:A 依赖 B,B 依赖 A(均通过 setter 注入)。
1. 开始创建 A
-
getBean(A)→ 实例化 A(调用构造器,得到原始对象)- 将 A 的
ObjectFactory放入三级缓存singletonFactories - 开始为 A 填充属性,发现需要 B
2. 尝试获取 B
-
getBean(B)→ 实例化 B- 将 B 的
ObjectFactory放入三级缓存 - 为 B 填充属性,发现需要 A
3. 获取 A 的早期引用
-
getBean(A)此时从缓存中查找:一级没有,二级没有,但三级有- 从三级缓存拿到 A 的 ObjectFactory**,调用** getObject()
- 将得到的对象(可能是原始对象,也可能是 AOP 代理对象)放入二级缓存
earlySingletonObjects - 同时移除三级缓存中的 A 的工厂
- 将这个早期引用注入给 B
4. B 完成创建
-
- B 的属性填充完毕,继续执行初始化方法等
- B 创建完成后,放入一级缓存
singletonObjects
5. 回到 A 的创建
-
- 现在 A 能拿到 B 的完整实例(因为 B 已在一级缓存)
- A 继续完成属性填充、初始化
- A 创建完成后,也放入一级缓存,并移除二级/三级缓存中的 A
为什么二级缓存不行?------ 代理对象的诞生时机问题
如果只有二级缓存 (singletonObjects 和 earlySingletonObjects),对于普通 Java 对象,上述流程完全可行:
- 实例化 A → 放入二级缓存(半成品)→ B 从二级缓存拿到 A 引用 → B 完成 → A 完成。
但是 ,当 Bean 需要被 AOP 代理(例如 @Transactional、@Async 等)时,代理对象不是在实例化阶段生成的,而是在初始化之后 (通过 BeanPostProcessor 的后置处理,如 AbstractAutoProxyCreator)。
如果只有二级缓存,B 拿到的 A 是实例化后的原始对象 ,而 A 最终会变成一个代理对象。这就导致 B 中持有的 A 引用是原始对象,而容器中实际管理的是代理对象,两者不一致,AOP 功能(如事务增强)将失效。
三级缓存如何解决代理问题?
- 三级缓存中存放的不是原始对象,而是ObjectFactory。
- 当调用
getObject()时,可以提前执行BeanPostProcessor的getEarlyBeanReference()方法。 - 该方法的典型实现(如
AbstractAutoProxyCreator)可以在这个早期阶段就生成代理对象。 - 因此 B 拿到的 A 引用,已经是最终的代理对象(如果 A 确实需要被代理的话)。
- 这样既保证了循环依赖得以解决,又保证了 AOP 语义正确。
伪代码演示
// 三级缓存中的 ObjectFactory
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
// 当需要提前暴露时
Object earlySingleton = getSingleton(beanName, true);
// 内部会调用 singletonFactories.get(beanName).getObject()
// getEarlyBeanReference 可能生成代理
如果只有二级缓存,Spring 无法在"提前暴露"这个时间点知道 Bean 是否需要代理,也无法执行 getEarlyBeanReference 逻辑,因此必须借助三级缓存中的工厂来延迟决定最终暴露的对象。
|---------|-------------------|----------------------|
| 缓存级别 | 存储内容 | 作用 |
| 一级(成品) | 完全初始化好的 Bean | 常规获取 |
| 二级(半成品) | 早期暴露的 Bean(可能是代理) | 解决循环依赖的直接引用 |
| 三级(工厂) | ObjectFactory | 在需要时生成早期引用,支持 AOP 代理 |
- 二级缓存能解决普通对象的循环依赖,但无法处理 AOP 等需要动态代理的场景。
- 三级缓存通过工厂模式,将"何时生成代理"的决定权推迟到真正需要早期引用的那一刻,从而保证循环依赖注入的始终是最终正确的对象。
- 这是 Spring 设计中的经典细节:用一层间接层(工厂)换来了更高的灵活性
四、spring 为什么推荐使用构造函数进行依赖注入?
1、保证依赖的不可变性(Immutability)
通过构造函数注入的依赖可以声明为 final 字段,确保对象创建后依赖关系不可变。这带来了线程安全性(对象一旦发布就不会被修改)和清晰的不可变设计。
@Service
public class OrderService {
private final UserRepository userRepository;
private final EmailSender emailSender;
// 构造函数注入
public OrderService(UserRepository userRepository, EmailSender emailSender) {
this.userRepository = userRepository;
this.emailSender = emailSender;
}
}
而字段注入(@Autowired 直接写在字段上)无法使用 final,因为字段需要在对象实例化之后通过反射赋值,依赖的可变性容易引发并发问题。
2、强制依赖不为空,避免 NPE
构造函数注入要求容器在创建 Bean 时提供所有依赖的实例。如果某个依赖在 Spring 容器中不存在,则在启动阶段就会抛出异常(NoSuchBeanDefinitionException),将问题提前暴露。
相反,字段注入或 Setter 注入可能因为某个依赖未注入而导致运行时 NullPointerException,问题更难定位。
// 字段注入可能导致 NPE 延迟到运行时
@Autowired
private UserRepository userRepository;
3、主动发现循环依赖,避免隐藏问题
Spring 的构造函数注入能够提前发现循环依赖 (如 A 和 B 互相通过构造函数注入)。当 Spring 尝试创建 A 时需要 B,创建 B 时需要 A,容器会立即抛出 BeanCurrentlyInCreationException,帮助开发者快速修正设计。
而字段注入或 Setter 注入允许循环依赖存在(Spring 通过三级缓存解决了单例 setter 循环依赖),但这样的设计往往意味着职责耦合过重或架构不合理。使用构造函数注入可以强制开发者优化设计。
4、便于单元测试,无需启动 Spring 容器
构造函数注入使得被测试类成为一个普通的 POJO,不依赖 Spring 的反射机制 。在单元测试中,直接 new 对象并传入 Mock 依赖即可:
// 测试时不需要 Spring 容器
UserRepository mockRepo = mock(UserRepository.class);
EmailSender mockSender = mock(EmailSender.class);
OrderService service = new OrderService(mockRepo, mockSender);
如果是字段注入的类,你需要借助 SpringExtension、MockitoExtension(通过 @InjectMocks)或反射手动设置字段,测试代码更复杂且与框架耦合。
5、明确表示必需依赖,符合"清晰编码"原则
构造函数参数清晰地列出了类正常运行所必需的所有依赖。任何阅读代码的人一眼就能知道这个类需要哪些服务才能工作,而字段注入则将依赖隐藏在类的内部,降低了代码可读性。
同时,如果一个类的构造函数参数过多(例如超过 7 个),这往往是一个代码异味(Code Smell),提示该类可能承担了过多的职责,需要拆分。构造函数注入天然地促进单一职责原则。
6、与 Spring 框架解耦,更通用的代码风格
虽然字段注入的 @Autowired 注解是 Spring 特有的,但构造函数注入可以不使用任何 Spring 特定注解(Spring 4.3+ 自动推断单构造函数的参数进行注入),使得类可以轻松地被其他 IoC 容器(如 Guice)或纯 Java 代码使用。
// 无需 @Autowired,Spring 会自动注入
@Service
public class MyService {
private final MyDao myDao;
public MyService(MyDao myDao) { this.myDao = myDao; }
}
7、性能优势(微小但存在)
构造函数注入在 Bean 实例化时一次性完成所有依赖设置,无需后续的字段访问或反射调用(如字段注入在第一次使用时可能需要调用 postProcessProperties)。在高频创建 Bean 的场景下,构造函数注入略微更高效。
8、最佳实践对比示例
不推荐的字段注入(Field Injection)
@Service
public class LegacyService {
@Autowired
private DependencyA a; // 缺点:final 不可用,测试困难,隐藏依赖
@Autowired
private DependencyB b;
}
推荐的构造函数注入(Constructor Injection)
@Service
public class ModernService {
private final DependencyA a;
private final DependencyB b;
public ModernService(DependencyA a, DependencyB b) {
this.a = a;
this.b = b;
}
}
总结表格
|-------------|------------------|------------------------|
| 维度 | 构造函数注入 | 字段/Setter 注入 |
| 不可变性 | ✅ 支持 final | ❌ 不支持 |
| NPE 预防 | ✅ 启动时检查 | ❌ 运行时发现 |
| 循环依赖检测 | ✅ 立即报错 | ⚠️ 可能被解决,但隐藏设计问题 |
| 单元测试 | ✅ 直接 new 对象 | ❌ 需要反射或 Spring 容器 |
| 依赖显式性 | ✅ 清晰列出所需依赖 | ❌ 依赖隐藏在类内部 |
| 框架耦合度 | ✅ 低(普通 Java 构造器) | ❌ 较高(依赖 @Autowired ) |
| 职责单一性提示 | ✅ 参数过多时可发现设计问题 | ❌ 容易不知不觉添加过多依赖 |
例外情况
虽然构造函数注入是首选,但在以下场景中可以(或需要)使用 Setter 注入:
- 可选依赖:提供合理的默认行为,依赖可为空。
- 循环依赖重构困难:遗留系统中不得不使用 Setter 注入绕过构造器循环依赖问题。
- 需要重新注入:某些动态场景需要在 Bean 创建完成后更换依赖(极少见)。
但在绝大多数新代码中,请坚持使用构造函数注入。
五、spring AOP底层原理
JDK 动态代理 vs CGLIB
|-----------|-------------------------------|----------------------------------------------------------------|
| 对比项 | JDK 动态代理 | CGLIB |
| 实现基础 | Proxy + InvocationHandler | 字节码生成库(ASM),创建子类 |
| 前提条件 | 目标类必须实现接口 | 目标类无需接口 |
| 限制 | 只能代理接口方法 | 无法代理 final 类/方法 |
| Spring 默认 | 有接口时默认使用 | 无接口时使用;Spring Boot 2.x+ 默认 proxy-target-class=true ,优先 CGLIB |
同类方法调用 AOP 失效问题
原因:Spring AOP 基于代理,同类中 this.methodB() 调用的是目标对象自身方法,而非代理对象方法,无法触发增强。
解决方案(按推荐度排序):
自注入:@Autowired @Lazy private UserService self; 然后 self.methodB()
通过 ApplicationContext 获取代理对象:context.getBean(UserService.class).methodB()
拆分到不同类中