Spring (基于 Spring 6.2.7 进行源码分析)
字数: 0 字 时长: 0 分钟
IOC 与 DI
Spring IOC (Inversion of Control
控制反转) 是 Spring 框架的核心概念之一。它通过依赖注入(DI Dependency Injection
) 实现的,让对象的创建与管理职责由容器控制,而不是由对象自身控制。
IOC 是一种思想,而不是一个具体的技术。控制反转指的是控制对象的创建这个行为不再由程序员自行完成,而是反转给 IOC 容器完成。
DI 是用来构建 IOC 容器的技术实现,IOC 容器在创建一个对象时,会自动将这个对象的依赖注入进去,这样可以让对象与其依赖的对象解耦。
IOC 有什么好处?
对象的创建由 IOC 控制,对象之间不会有很明确的依赖关系,非常容易设计出松耦合的程序
例如:对象 A 依赖一个实现 B,由 IOC 容器控制对象后,我们不需要明确地在对象 A 的代码里指定写死 B 的实现,只需要写明依赖 B 的接口;然后我们可以在配置文件里定义 A 依赖的 B 的具体实现,如果有新的 B 实现类要更换,只需要在配置文件里将 A 依赖 B 改为 B1 即可,而不需要去改动 A 的任何代码!
Spring 对象创建过程
- 获取 Bean 实例包装器
BeanWrapper
,获取或创建 Bean 的原始实例。
如果是单例,先从 factoryBeanInstanceCache
中尝试获取已存在的包装器;若未命中,则创建实例包装器。
- 处理
MergedBeanDefinition
后处理器
这句话有点难理解,可以分为 MergedBeanDefinition
(合并后的BeanDefinition
) 和 PostProcessor
(后处理器)。
BeanDefinition
:每个 Spring Bean 在容器中都有对应的配置信息(元数据),比如类名、作用域、依赖关系、是否懒加载等。MergedBeanDefinition
:容器创建 Bean 实例时,需要将子定义与其继承的父定义合并,形成完整的配置信息,称为MergedBeanDefinition
(合并后 Bean 定义)。PostProcessor
:这里指的是BeanPostProcessor
,它允许我们在 Bean 的生命周期中插入自定义逻辑,但不修改核心代码
实现
BeanPostProcessor
添加自定义逻辑
// 示例:自定义后处理器监控 Bean 初始化
public class MonitoringPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) {
System.out.println("初始化前:" + name);
return bean;
}
public Object postProcessAfterInitialization(Object bean, String name) {
System.out.println("初始化完成:" + name);
return bean;
}
}
现在,再回到源码,这里调用 applyMergedBeanDefinitionPostProcessors
遍历所有 BeanPostProcessor
后处理器 ,解析注解信息并缓存到 BeanDefinition
中,为后续依赖注入准备元数据
- 依赖注入属性,并初始化 Bean
循环依赖
循环依赖 (Circular Dependency)是指两个或多个模块、类、组件之间相互依赖,形成一个闭环,无法确定加载或初始化的顺序。
比如单例 A 和单例 B 循环依赖,Spring 是如何创建的呢?
- 先创建 A ,此时 A 是不完整的(没有注入 B),用个 map 保存这个不完整的 A
- 再创建 B ,B 需要 A ,所以去找 map 中找那个不完整的 A ,此时 B 就完整了
- 然后 A 就可以注入 B ,A 也就完整了
Spring 中使用了三级缓存来解决循环依赖,每一级缓存使用一个 map 实现,关键是提前暴露未完全创建完毕的 Bean
Spring 如何利用三级缓存解决循环依赖?
- 获取单例 Bean 时,会通过
BeanName
先去singletonObjects
(一级缓存)查找完整的 Bean - 如果一级缓存中没有,去看对象是否在创建中,如果没有直接返回
null
;如果正在创建,则去earlySingletonObjects
(二级缓存)中查找 Bean - 如果二级缓存中也没有,则去
singletonFactories
(三级缓存)中通过BeanName
找到对应的工厂,如果存在工厂则通过工厂创建 Bean,并且放入到二级缓存中 - 如果三级缓存都每找到,则返回
null
为什么 Spring 循环依赖需要三级缓存,二级不够吗?
其实如果单纯解决循环依赖问题,二级缓存已经够了。但是在涉及到 动态代理 AOP 时:
- 直接使用二级缓存不做任何处理,导致我们拿到的 Bean 是未代理的原始对象
- 如果二级缓存内存放的是代理对象,则违反了 Bean 的生命周期
所以再回到 Bean 的创建源码,可以发现在依赖注入和完全创建 Bean 之前,如果检测到需要 AOP ,会注册三级缓存
AOP
AOP(Aspect-Oriented Programming,面向切面编程) 是一种编程范式,用于将通用功能(如日志管理、安全检查、事务管理)与业务逻辑分离开来。
AOP 的核心思想是将与业务逻辑无关的横切关注点抽取出来,通过声明的方式动态地应用到业务方法上,而不是将这些代码直接嵌入业务代码中。
具体实现是通过动态代理或静态代理,在调用想要地对象方法时,进行拦截处理没执行切入逻辑,然后再调用真正的实现。
使用代理执行 计数 的切入逻辑
class A代理 {
A a;// 被代理的 A
void addUser(User user) {
count();// 计数
a.addUser(user); // 调用真正的实现
}
}
AOP 术语
- 切面
Aspect
在 AOP 中,切面(Aspect
) 通常包含多个通知(Advice
)和切入点(Pointcut
)。这些通知和切入点一起定义了横切关注点的功能,例如日志切面可以记录应用中所有服务方法的调用。
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethod(JoinPoint joinPoint) {
System.out.println("Executing method: " + joinPoint.getSignature());
}
}
- 连接点
Join Point
连接点通常是方法的执行点,例如在某个方法执行前或执行后插入一段代码。
- 通知
Advice
通知是在特定的连接点上执行的动作:
- 前置通知:在方法执行之前执行
- 后置通知:在方法执行之后执行
- 环绕通知:在方法执行前后都执行
- 异常通知:在方法抛出异常后执行
- 返回通知:在方法执行完成(无论是否抛出异常)后执行
@Before("execution(* com.example.service.*.*(..))")
public void logBefore() {
System.out.println("Method is about to execute");
}
- 切入点
PointCut
切入点定义了哪些连接点需要应用通知,Spring AOP 使用 AspectJ 的表达式来定义切入点
通过 @Pointcut 注解定义切入点
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
- 目标对象
Target Object
目标对象是被 AOP 代理的对象,它包含原始的业务逻辑,Spring AOP 为目标对象生成代理对象,来增强目标对象的方法,但目标对象本身是不会感知到的。
Spring AOP 支持两种代理模式:
JDK 动态代理:针对实现了接口的类
CGLIB 动态代理:针对没有实现接口的类,通过继承目标类生成代理对象
织入
Weaving
织入是将切面应用到目标对象的过程。Spring AOP 通过运行时动态代理实现织入,而 AspectJ 可以在编译时通过静态代理实现织入。
AOP 拦截器链实战示例
// controller 代码
@RestController
public class UserController {
@Resource
private HelloService helloService;
@RequestMapping("hello")
public String hello() {
return helloService.hello("甜甜");
}
}
// service 层代码
@Service
public class HelloService {
public String hello(String name) {
return "Hello " + name;
}
}
// AOP 切面类
@Aspect
@Component
public class LogAspect {
// 定义切入点,匹配 service 包下所有方法
@Pointcut("execution(* cn.ttdgg.rpc.springboot.consumer.service.*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("前置通知执行:" + joinPoint.getSignature().getName());
}
@After("serviceMethods()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("后置通知执行:" + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logAfterReturn(JoinPoint joinPoint,Object result) {
System.out.println("返回通知执行:" + result);
}
@AfterThrowing(pointcut = "serviceMethods()", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint,Throwable error) {
System.out.println("异常通知执行:" + error.getMessage());
}
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知执行(前置):" + joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
System.out.println("环绕通知执行(后置):" + result);
return result;
}
}
当我们正常访问该接口时,控制台成功在方法执行前后打印出了相关信息,那么 Spring 具体是如何控制调用顺序的呢?实际上这些注解都对应着不同的 interceptor
拦截器实现,他们采用责任链模式实现了拦截器链
注意上面的示例代码,我的 Service
直接使用的注入的是 HelloService
类,而不是接口,因此 AOP 采用的是 CGLIB 动态代理模式为我创建代理对象 (如果是接口,那么 AOP 会采用 JDK 动态代理创建,JDK 动态代理使用反射实现,性能会比 CGLIB 差些)
CGLIB 动态代理入口是 CglibAopProxy
类的内部类 DynamicAdvisedInterceptor
,会尝试获取拦截器链
获取到拦截器链之后,会递归调用连接器链中所有拦截器
IOC 容器相关注解
@Controller
、@Service
、@Repository
、@Component
这四个注解本质上都是 @Component
注解,标记一个类为 Spring 的 Bean,可以被@ComponentScan
检测
@Configuration
标记一个类是配置类,替代传统的 XML 配置文件,内部可以声明 Bean
@Bean
标注在 @Configuration
类或 @Component
类内部的方法上,表名该方法的返回值注册为 Spring 的 Bean
@ComponentScan
配置在 @Configuration
类或 @SpringBootApplication
上,指示 Spring 容器需要扫描哪些包及其子包
依赖注入相关注解
@Autowired
Spring 提供,按类型(byType) 进行依赖注入,可以直接在字段上标注或在 Setter
方法上标注;但官方更推荐在构造器上标注,当类只有一个构造器时,@Autowired
可省略
@Qualifier
和@Primary
因为 @Autowired
按类型进行依赖注入,当有多个匹配的 Bean 时,优先注入被标记了 @Primary
那个
@Qualifier
可以显式指定要注入的 Bean 的名称
@Resource
作用与 @Autowired
一样,不同的是@Resource
是一个标准的 JSR-250 标准,不止能在 Spring 框架中使用,默认 byName 进行依赖注入,不支持构造器注入
@Inject
行为几乎和 @Autowired
相同,是另一个标准化 JSR-330 依赖注入注解,是与 Spring 解耦的 Java 标准
@Value
主要用于注入外配配置文件(如 application.yml
)的值,或注入简单的字面量。
作用域与生命周期注解
@Scope
指定一个 Bean 的作用域,默认是单例(singleton
),也支持 prototype
、request
、session
等
@Component
@Scope("prototype") // 或多用 ScopeConfigurer.PROTOTYPE
public class PrototypeBean { ... }
@PostConstruct
(JSR-250)
标注在方法上,该方法会在Bean 依赖注入完成之后和初始化回调方法之前被执行一次
@PreDestroy
(JSR-250)
标注在方法上,该方法会在容器关闭、Bean被销毁之前被执行一次
@Lazy
标注在 @Component
、@Bean
等地方:该 Bean 只有在第一次被需要时才会被初始化(懒加载),可以加快应用的启动速度
@Async
异步任务
@Async
标注在方法上,可以异步执行任务(需要使用 @EnableAsync
注解来开启异步任务功能,注解可以加在启动类或配置类上)。Spring 通过动态代理为 @Async
方法生成代理对象,拦截方法调用,通过切面将方法调用转交给线程池进行异步执行。
@Async
默认使用 SimpleAsyncTaskExecutor
(每次执行任务都会新开线程池,不推荐),可通过自定义TaskExecutor
优化线程池管理。
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.initialize();
return executor;
}
}
自调用失效问题
如果类自己调用内部的 Async
方法(this.method
)会绕过代理,不会触发 AOP 代理增强,因此该注解会失效。可通过注入代理对象或重构拆分服务的方式解决:
@Service
public class SyncService {
@Autowired
private AsyncService asyncService; // 独立异步服务
public void syncMethod() {
asyncService.asyncMethod(); // 必定生效
}
}
@Service
public class AsyncService {
@Async
public void asyncMethod() {}
}
@Service
public class MyService {
// 关键:注入自身代理
@Autowired
private MyService selfProxy;
public void syncMethod() {
// 正确:通过代理对象调用
selfProxy.asyncMethod();
}
@Async
public void asyncMethod() {}
}
@Transactional
事务
Spring 通过 AOP 动态代理创建目标对象的代理,拦截 @Transactional
方法,交给 PlatformTransactionManager
事务管理器接管。要使用 @Transactional
注解必须先引入依赖,位于 spring-tx
模块中, SpringBoot 直接引入 spring-boot-starter-jdbc
即可
@EventListener
事件监听
标注在方法上,使方法自动监听特定类型的事件,并在事件发布时触发执行。
1. 发布事件
在 Spring 中,可以使用 ApplicationEventPublisher
发布事件。可以发布自定义事件或 Spring 内置事件。
@Component
public class EventPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void publish(String message) {
MyCustomEvent event = new MyCustomEvent(this, message);
eventPublisher.publishEvent(event); // 发布事件
}
}
2. 监听事件
使用 @EventListener
监听事件,事件参数可以是自定义事件或内置事件。
@Component
public class MyEventListener {
@EventListener
public void handleEvent(MyCustomEvent event) {
System.out.println("Event received: " + event.getMessage());
}
}
3. 异步事件监听
在处理某些耗时任务时,我们希望事件监听是异步的,以避免阻塞主线程。通过 @Async
注解,将事件监听方法变为异步执行
@Configuration
@EnableAsync // 启用异步功能
public class AsyncConfig {
}
@Component
public class MyEventListener {
@Async
@EventListener
public void handleEvent(MyCustomEvent event) {
System.out.println("Handling event asynchronously: " + event.getMessage());
}
}
4. 条件事件监听
通过 @EventLiistener(condition = "...")
,可以私用 Spring 的 SpEL 表达式定义条件,只有满足条件时,才会处理该事件。
@Component
public class MyEventListener {
@EventListener(condition = "#event.message.startsWith('Spring')")
public void handleEvent(MyCustomEvent event) {
System.out.println("Event received: " + event.getMessage());
}
}
5. 内置事件
Spring 提供了很多内置事件,例如:
ContextRefreshedEvent
:Spring 容器初始化或刷新时触发ContextClosedEvent
:Spring 容器关闭时触发ApplicationReadyEvent
:应用启动完成时触发
@Component
public class ContextEventListener {
@EventListener
public void onContextRefreshed(ContextRefreshedEvent event) {
System.out.println("Context refreshed");
}
}
6. 返回值事件
@EventListener
标注的方法可以有返回值,如果返回值是事件类型,那么 Spring 会自动将返回值作为新的事件发布。
@Component
public class MyEventListener {
@EventListener
public AnotherEvent handleEvent(MyCustomEvent event) {
System.out.println("Handling event: " + event.getMessage());
return new AnotherEvent(this, "Follow-up event");
}
}