Spring IoC 与 AOP 核心知识点详解
一、Spring IoC 核心(控制反转 Inversion of Control)
1.1 IoC 核心定义与设计思想
1.1.1 什么是 IoC
IoC(Inversion of Control,控制反转)是 Spring 框架的核心思想,本质是反转了Bean的创建、依赖管理和生命周期的控制权:传统开发中,由开发者手动创建对象(new关键字)、维护对象依赖关系;而 IoC 模式下,这些控制权被转移到 Spring 容器(IoC Container),容器负责创建Bean实例、注入依赖、管理Bean的生命周期,开发者仅需定义Bean的配置,无需关心具体的创建和依赖细节。
核心目的:解耦,降低组件之间的耦合度,提升代码的可维护性、可扩展性和可测试性;同时实现组件的复用,简化开发流程。
1.1.2 IoC 与 DI 的关系
DI(Dependency Injection,依赖注入)是 IoC 的具体实现方式,二者不可分割:
-
IoC 是“思想”:强调“控制权反转”,是一种设计原则。
-
DI 是“手段”:通过容器将依赖的Bean注入到目标Bean中,实现控制权反转的具体操作。
通俗理解:IoC 是“老板”,负责统筹安排(确定需要哪些Bean、如何管理);DI 是“员工”,负责执行具体操作(将依赖的Bean送到目标Bean手中)。没有 DI,IoC 思想无法落地;没有 IoC,DI 也失去了存在的意义。
1.2 IoC 容器核心原理
1.2.1 IoC 容器的本质与核心接口
Spring IoC 容器本质是一个管理Bean的工厂,核心接口是 BeanFactory 和 ApplicationContext,二者是父子关系,后者是前者的增强版,也是实际开发中最常用的容器。
(1)BeanFactory 接口(基础容器)
BeanFactory 是 Spring IoC 容器的顶层接口,定义了容器的核心功能:获取Bean、判断Bean是否存在、获取Bean的类型等,是一个“懒加载”容器(仅在调用 getBean() 方法时,才会创建Bean实例)。
核心方法:
-
getBean(String name):根据Bean名称获取Bean实例。 -
getBean(Class<T> requiredType):根据Bean类型获取Bean实例。 -
containsBean(String name):判断容器中是否存在指定名称的Bean。 -
isSingleton(String name):判断Bean是否为单例。
常见实现类:XmlBeanFactory(已过时,基于XML配置的基础容器)、DefaultListableBeanFactory(Spring 内部核心实现,也是 ApplicationContext 的底层容器)。
(2)ApplicationContext 接口(增强容器)
ApplicationContext 继承自 BeanFactory,在其基础上增加了更多增强功能,是“预加载”容器(容器启动时,会自动创建所有单例Bean实例,除非配置了懒加载),更适合实际开发。
核心增强功能:
-
支持国际化(MessageSource)。
-
支持事件发布与监听(ApplicationEvent、ApplicationListener)。
-
支持资源加载(ResourceLoader,可加载本地文件、网络资源等)。
-
支持AOP集成(与Spring AOP无缝衔接)。
-
支持环境配置(Environment,可获取系统环境变量、配置文件参数)。
常见实现类(开发常用):
-
ClassPathXmlApplicationContext:加载类路径下的XML配置文件,创建容器。 -
FileSystemXmlApplicationContext:加载本地文件系统中的XML配置文件。 -
AnnotationConfigApplicationContext:加载基于注解的配置(如@Configuration、@Component),Spring Boot 底层核心容器。 -
WebApplicationContext:适用于Web应用,整合Spring MVC,如XmlWebApplicationContext、AnnotationConfigWebApplicationContext。
1.2.2 IoC 容器的核心流程(Bean的生命周期)
IoC 容器的核心工作是“创建Bean、注入依赖、管理Bean生命周期”,整个流程分为4个阶段,贯穿Bean从创建到销毁的全过程,也是 IoC 原理的核心体现:
阶段1:Bean的定义(Definition)
容器首先加载Bean的配置信息(XML配置、注解配置),将其解析为 BeanDefinition 对象(Bean的定义信息),存储在 BeanDefinitionRegistry(Bean定义注册表)中。
BeanDefinition 包含的核心信息:Bean的类名、作用域(singleton/prototype)、依赖关系、初始化方法、销毁方法、是否懒加载等。
阶段2:Bean的实例化(Instantiation)
容器根据 BeanDefinition 的信息,创建Bean实例(调用Bean的构造器),此时Bean仅完成实例化(内存分配),尚未注入依赖,也未执行初始化操作。
实例化方式(核心细节):
-
默认方式:调用Bean的无参构造器(若没有无参构造器,且未指定构造器注入,会抛出异常)。
-
构造器注入方式:根据@Autowired标注的构造器,传入依赖的Bean实例,完成实例化。
-
静态工厂方法:通过BeanDefinition指定工厂类和静态方法,调用静态方法创建Bean实例(如 )。
-
实例工厂方法:先创建工厂Bean实例,再调用工厂实例的方法创建目标Bean。
阶段3:Bean的初始化(Initialization)
实例化完成后,容器对Bean进行初始化,核心操作包括:依赖注入、执行初始化方法,此时Bean才真正可用。
初始化流程(按顺序执行):
-
注入依赖:容器将Bean依赖的其他Bean实例,通过构造器、setter方法或字段注入到当前Bean中(DI的核心操作)。
-
调用
BeanNameAware接口的setBeanName()方法:将Bean的名称注入到Bean中。 -
调用
BeanFactoryAware接口的setBeanFactory()方法:将IoC容器实例注入到Bean中。 -
调用
ApplicationContextAware接口的setApplicationContext()方法:将ApplicationContext容器实例注入到Bean中(仅ApplicationContext容器支持)。 -
调用
BeanPostProcessor的postProcessBeforeInitialization()方法:初始化前增强(AOP的前置增强可在此实现)。 -
执行自定义初始化方法:
-
XML配置:通过
init-method属性指定。 -
注解配置:通过
@PostConstruct注解标注(JSR-250规范,优先于init-method)。
-
-
调用
BeanPostProcessor的postProcessAfterInitialization()方法:初始化后增强(AOP的动态代理在此生成,核心!)。
阶段4:Bean的销毁(Destruction)
当IoC容器关闭时,会销毁容器中的Bean(仅单例Bean,原型Bean由开发者手动管理),销毁流程:
-
调用
DisposableBean接口的destroy()方法。 -
执行自定义销毁方法:
-
XML配置:通过
destroy-method属性指定。 -
注解配置:通过
@PreDestroy注解标注(JSR-250规范,优先于destroy-method)。
-
1.2.3 核心扩展接口(深度细节)
Spring IoC 容器支持通过扩展接口自定义Bean的创建和管理流程,核心扩展接口如下(开发中常用,面试高频):
-
BeanPostProcessor:Bean后置处理器,用于在Bean初始化前后进行增强处理,是AOP实现的核心基础(动态代理在此生成)。
注意:该接口的方法会对容器中所有Bean生效,若需针对性增强,可在方法中判断Bean类型。 -
BeanFactoryPostProcessor:Bean工厂后置处理器,用于在BeanDefinition加载完成后、Bean实例化前,修改BeanDefinition的配置信息(如修改Bean的作用域、依赖关系)。 常见实现类:
PropertyPlaceholderConfigurer(读取配置文件参数,替换XML中的${}占位符)。 -
FactoryBean:工厂Bean,用于创建复杂Bean(如数据库连接池、MyBatis的SqlSessionFactory),与普通Bean的区别:普通Bean是直接创建自身实例,FactoryBean是创建指定的目标Bean实例。
核心方法:`getObject()`:返回目标Bean实例;`getObjectType()`:返回目标Bean的类型;`isSingleton()`:判断目标Bean是否为单例。
1.3 IoC 核心配置方式(广度覆盖)
Spring IoC 支持3种核心配置方式,实际开发中可混合使用,核心是“告诉容器如何创建Bean、注入依赖”。
1.3.1 XML配置方式(传统方式)
通过XML文件定义Bean的配置信息,适用于早期Spring项目,优点是配置清晰、易于维护,缺点是配置繁琐。
核心标签与配置示例:
<!-- 1. 基础Bean配置 -->
<bean id="userService" class="com.xxx.service.UserService" scope="singleton" lazy-init="false"
init-method="init" destroy-method="destroy">
<!-- 2. 构造器注入 -->
<constructor-arg name="userDao" ref="userDao"/>
<!-- 3. setter注入 -->
<property name="orderDao" ref="orderDao"/>
<!-- 4. 基本类型注入 -->
<property name="pageSize" value="10"/>
</bean>
<!-- 依赖的Bean -->
<bean id="userDao" class="com.xxx.dao.UserDaoImpl"/>
<bean id="orderDao" class="com.xxx.dao.OrderDaoImpl"/>
<!-- 5. 扫描指定包下的注解Bean(简化XML配置) -->
<context:component-scan base-package="com.xxx"/>
<!-- 6. 引入配置文件 -->
<context:property-placeholder location="classpath:application.properties"/>
1.3.2 注解配置方式(主流方式)
通过注解标注Bean和依赖关系,简化配置,是Spring Boot的核心配置方式,优点是开发效率高、代码简洁。
核心注解(必掌握):
-
Bean注册注解:用于将类交给IoC容器管理,生成Bean实例。
-
@Component:通用注解,适用于所有组件(无明确分层)。 -
@Controller:用于Web层(Spring MVC控制器),继承自@Component。 -
@Service:用于业务层(Service),继承自@Component,便于分层管理。 -
@Repository:用于数据层(Dao),继承自@Component,支持异常转换(将数据库异常转换为Spring的DataAccessException)。 -
@Bean:用于方法上,手动创建Bean实例(适用于第三方组件,如配置数据源、RedisTemplate),优先级高于@Component注解。
-
-
依赖注入注解:用于注入依赖的Bean。
-
@Autowired:Spring自带注解,根据类型(byType)注入,若有多个实现类,需配合@Qualifier指定Bean名称。 -
@Qualifier:配合@Autowired,根据Bean名称(byName)注入,解决多个实现类的注入冲突。 -
@Resource:JSR-250规范注解,默认根据名称(byName)注入,若名称不存在,再根据类型(byType)注入,无需配合其他注解。 -
@Value:注入基本类型、字符串,以及配置文件中的参数(如 @Value("${spring.datasource.url}"))。
-
-
配置类注解:
-
@Configuration:标记类为配置类,替代XML配置文件,类中可通过@Bean注解创建Bean。 -
@ComponentScan:指定扫描的包,让容器自动扫描包下的注解Bean(相当于XML中的context:component-scan)。 -
@PropertySource:引入外部配置文件(如application.properties),配合@Value使用。
-
配置示例:
// 配置类(替代XML)
@Configuration
@ComponentScan(basePackages = "com.xxx")
@PropertySource("classpath:application.properties")
public class SpringConfig {
// 手动创建Bean(第三方组件)
@Bean
public DataSource dataSource(@Value("${spring.datasource.url}") String url,
@Value("${spring.datasource.username}") String username,
@Value("${spring.datasource.password}") String password) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
// 业务层Bean
@Service
public class UserService {
// 构造器注入(推荐)
private final UserDao userDao;
@Autowired
public UserService(UserDao userDao) {
this.userDao = userDao;
}
// setter注入(可选依赖)
private OrderDao orderDao;
@Autowired
public void setOrderDao(OrderDao orderDao) {
this.orderDao = orderDao;
}
}
1.3.3 JavaConfig 配置方式(纯注解进阶)
JavaConfig 是基于Java类的配置方式,本质是@Configuration注解的延伸,完全替代XML配置,是Spring Boot的底层配置方式。核心特点:类型安全、可扩展性强,支持复杂的Bean配置(如条件配置、导入配置)。
核心进阶注解:
-
@Import:导入其他配置类,实现配置类的复用(相当于XML中的)。 -
@Conditional:条件配置,根据指定条件决定是否创建Bean(如@ConditionalOnClass、@ConditionalOnProperty,Spring Boot自动配置的核心)。 -
@Lazy:懒加载,仅对单例Bean生效,延迟Bean的实例化(容器启动时不创建,第一次使用时创建)。 -
@Scope:指定Bean的作用域(singleton、prototype等)。
1.4 IoC 核心问题与解决方案(深度实战)
1.4.1 循环依赖问题(高频面试+实战重点)
定义:两个或多个Bean互相依赖(如A依赖B,B依赖A),导致容器无法完成Bean的初始化,抛出 BeanCurrentlyInCreationException 异常。
Spring 对循环依赖的支持情况:
-
支持:单例Bean + setter注入/字段注入(底层通过“三级缓存”解决)。
-
不支持:单例Bean + 构造器注入、原型Bean(无论哪种注入方式)。
三级缓存核心原理(源码级细节):
-
一级缓存(singletonObjects):存储完全初始化完成的单例Bean(可用状态)。
-
二级缓存(earlySingletonObjects):存储提前暴露的、未完全初始化的单例Bean(仅实例化,未注入依赖)。
-
三级缓存(singletonFactories):存储Bean的工厂对象(ObjectFactory),用于生成提前暴露的Bean实例(解决AOP动态代理的循环依赖)。
循环依赖解决流程(以A依赖B、B依赖A为例):
-
容器启动,创建A的BeanDefinition,开始实例化A,实例化后将A的工厂对象放入三级缓存。
-
A需要注入B,容器查找B,发现B未创建,开始实例化B,实例化后将B的工厂对象放入三级缓存。
-
B需要注入A,容器从三级缓存获取A的工厂对象,生成A的提前暴露实例(未初始化),放入二级缓存,删除三级缓存中的A。
-
B注入A后,完成初始化,放入一级缓存,删除二级缓存中的B。
-
回到A的注入流程,从一级缓存获取B,注入A,完成A的初始化,放入一级缓存,删除二级缓存中的A。
解决方案:
-
构造器注入循环依赖:改用setter注入;或在其中一个Bean的构造器参数上添加@Lazy注解,延迟依赖注入。
-
原型Bean循环依赖:避免原型Bean互相依赖;或手动管理Bean的创建,不交给IoC容器管理。
-
设计层面:拆分Bean的依赖关系,将共同依赖抽取为独立Bean,避免循环。
1.4.2 Bean注入失败问题
常见原因及解决方案:
-
Bean未被容器扫描:确保Bean所在包被@ComponentScan扫描,或通过@Bean注册。
-
注入类型不匹配:注入的接口没有实现类,或存在多个实现类(用@Qualifier指定Bean名称)。
-
单例Bean依赖原型Bean:原型Bean每次获取都是新实例,单例Bean初始化时仅注入一次,需通过ObjectFactory或Provider动态获取。
-
Bean的作用域不匹配:如request域Bean被单例Bean注入(单例Bean生命周期长于request域),需动态获取。
1.5 IoC 实战总结
-
核心思想:控制反转,将Bean的创建、依赖、生命周期控制权交给容器,解耦。
-
核心实现:DI(依赖注入)是IoC的具体手段,容器通过构造器、setter、字段注入依赖。
-
容器核心:ApplicationContext是实际开发的首选容器,功能强大,支持预加载、事件、资源加载等。
-
关键细节:Bean生命周期、三级缓存(解决循环依赖)、扩展接口(BeanPostProcessor等)是深度核心。
二、Spring AOP 核心(面向切面编程 Aspect-Oriented Programming)
2.1 AOP 核心定义与设计思想
2.1.1 什么是 AOP
AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架的另一核心特性,与 OOP(面向对象编程)互补:OOP 以“类”为核心,解决纵向的业务逻辑复用;AOP 以“切面”为核心,解决横向的通用功能复用(如日志、事务、权限校验),将这些通用功能从业务逻辑中剥离出来,实现“关注点分离”。
核心目的:复用通用功能、解耦业务逻辑与通用功能,让开发者专注于核心业务逻辑,同时提升代码的可维护性和可扩展性。
通俗理解:AOP 就像“给业务代码戴帽子、穿鞋子”——帽子(日志)、鞋子(事务)是通用功能,业务代码是核心,无需在每个业务方法中重复编写通用功能,而是通过AOP动态织入。
2.1.2 AOP 与 OOP 的区别与联系
| 维度 | OOP(面向对象) | AOP(面向切面) |
|---|---|---|
| 核心思想 | 以“类”为核心,封装业务逻辑,实现纵向复用 | 以“切面”为核心,封装通用功能,实现横向复用 |
| 解决问题 | 业务逻辑的封装与复用,解决代码重复(纵向) | 通用功能的复用,解决代码重复(横向) |
| 关系 | 互补关系:OOP 负责核心业务逻辑,AOP 负责通用功能,共同构成Spring的核心架构 |
2.2 AOP 核心概念(必掌握,面试高频)
AOP 的核心概念围绕“切面”展开,需准确理解每个概念的含义及相互关系,是掌握AOP原理的基础:
-
切面(Aspect):封装通用功能的类,是AOP的核心,包含切入点(Pointcut)和通知(Advice)。例如:日志切面、事务切面。
-
通知(Advice):切面中具体的通用功能逻辑(即“要做什么”),是切面的执行逻辑。Spring 提供5种通知类型,覆盖不同的执行时机。
-
切入点(Pointcut):定义“在哪些方法上执行通知”,通过表达式指定目标方法(即“在哪里做”)。
-
连接点(JoinPoint):程序执行过程中的所有可插入切面的点(如方法调用、异常抛出、字段赋值),切入点是连接点的子集(仅选中的连接点)。
-
目标对象(Target):被切面增强的对象(即业务逻辑对象),AOP通过动态代理对目标对象进行增强。
-
代理对象(Proxy):Spring AOP 动态生成的对象,包含目标对象的业务逻辑和切面的通知逻辑,开发者实际调用的是代理对象。
-
织入(Weaving):将切面的通知逻辑插入到目标对象的连接点中的过程,是AOP的实现核心。Spring AOP 采用“动态织入”(运行时织入)。
2.2.1 5种通知类型(核心细节)
Spring AOP 提供5种通知类型,按执行时机排序,覆盖方法执行的全流程:
-
前置通知(Before):在目标方法执行之前执行,无法阻止目标方法的执行(即使抛出异常,目标方法仍会执行)。
场景:日志记录(记录方法开始执行)、参数校验。 -
后置通知(After):在目标方法执行之后执行(无论目标方法是否抛出异常,都会执行)。
场景:资源释放(如关闭流)、清理操作。 -
返回通知(AfterReturning):在目标方法正常执行完成后执行(仅当目标方法没有抛出异常时执行)。
场景:日志记录(记录方法返回值)、结果处理。 -
异常通知(AfterThrowing):在目标方法抛出异常后执行(仅当目标方法抛出异常时执行)。
场景:异常处理、日志记录(记录异常信息)。 -
环绕通知(Around):包裹目标方法,在目标方法执行前后都能执行,可控制目标方法的执行(如阻止目标方法执行、修改参数、修改返回值),是功能最强大的通知。
场景:事务管理(开启事务、提交/回滚事务)、性能监控(统计方法执行时间)。 注意:环绕通知需通过 `ProceedingJoinPoint` 的 `proceed()` 方法调用目标方法,否则目标方法不会执行。
2.2.2 切入点表达式(Pointcut Expression)
切入点表达式用于指定“哪些方法需要被增强”,Spring AOP 支持多种表达式类型,最常用的是 execution 表达式,其次是 @annotation 表达式。
(1)execution 表达式(最常用)
语法格式:execution(修饰符 返回值类型 包名.类名.方法名(参数类型) throws 异常类型)
通配符说明:
-
*:匹配任意字符(单个),如返回值类型、包名、类名、方法名、参数类型。 -
..:匹配任意字符(多个),用于包名(匹配多级包)、参数类型(匹配任意个数、任意类型的参数)。 -
+:匹配指定类及其子类。
常用示例:
-
execution(* com.xxx.service.*.*(..)):匹配 com.xxx.service 包下所有类的所有方法(任意返回值、任意参数)。 -
execution(public * com.xxx.service.UserService.*(String, ..)):匹配 UserService 类中所有public方法,第一个参数为String类型,后续参数任意。 -
execution(* com.xxx.service..*.*(..)):匹配 com.xxx.service 包及其子包下所有类的所有方法。 -
execution(* com.xxx.service.UserService+.*(..)):匹配 UserService 类及其子类的所有方法。
(2)@annotation 表达式(常用)
语法格式:@annotation(注解全类名),用于匹配“被指定注解标注的方法”。
示例:
@annotation(com.xxx.annotation.Log):匹配所有被 @Log 注解标注的方法(常用于自定义切面,如日志切面)。
(3)其他表达式(了解)
-
within:匹配指定包或类下的所有方法(如 within(com.xxx.service.*))。 -
this:匹配代理对象为指定类型的所有方法。 -
target:匹配目标对象为指定类型的所有方法。
2.3 AOP 核心原理(动态代理,深度重点)
Spring AOP 的核心实现是 动态代理,织入方式为“运行时织入”(在程序运行时,动态生成代理对象,将切面逻辑织入到目标对象中),不修改目标对象的源码,完全符合“开闭原则”。
Spring AOP 支持两种动态代理方式,自动选择:
2.3.1 JDK 动态代理(默认,优先使用)
基于 Java 自带的java.lang.reflect.Proxy 类和 InvocationHandler 接口实现,仅支持接口代理(目标对象必须实现至少一个接口)。
核心原理:
-
目标对象实现一个或多个接口。
-
Spring 生成一个代理类,该代理类实现目标对象的所有接口。
-
代理类中注入切面的通知逻辑,调用目标方法时,先执行通知逻辑,再通过
InvocationHandler.invoke()方法调用目标对象的方法。
优点:JDK 自带,无需依赖第三方jar包,效率较高。
缺点:仅支持接口代理,无法代理没有实现接口的类。
2.3.2 CGLIB 动态代理(补充)
基于第三方jar包(cglib)实现,通过继承目标类生成代理对象,支持代理没有实现接口的类(也支持接口代理)。
核心原理:
-
Spring 生成一个代理类,该代理类继承自目标类。
-
重写目标类的所有方法,在重写的方法中织入切面的通知逻辑,再调用父类(目标对象)的方法。
-
由于是继承,目标类不能被 final 修饰(final类无法被继承),目标方法也不能被 final 修饰(final方法无法被重写)。
优点:支持代理无接口的类,适用范围更广。
缺点:依赖cglib jar包(Spring 已经集成,无需手动导入),效率略低于JDK动态代理。
2.3.3 Spring AOP 动态代理的选择规则
-
如果目标对象实现了接口:默认使用 JDK 动态代理;也可手动指定使用 CGLIB 动态代理(通过配置 spring.aop.proxy-target-class=true)。
-
如果目标对象没有实现接口:必须使用 CGLIB 动态代理。
-
Spring Boot 2.x 中,默认配置 spring.aop.proxy-target-class=true,即无论目标对象是否实现接口,都优先使用 CGLIB 动态代理(简化配置,提升兼容性)。
2.3.4 AOP 织入流程(源码级简化)
-
容器启动时,扫描所有带有 @Aspect 注解的切面类,解析切面中的切入点和通知。
-
容器创建目标对象时,判断该对象是否被切入点匹配(是否需要增强)。
-
若需要增强,根据目标对象是否实现接口,选择 JDK 或 CGLIB 动态代理,生成代理对象。
-
将切面的通知逻辑织入到代理对象的对应方法中(如前置通知织入到目标方法执行前)。
-
开发者调用代理对象的方法时,代理对象先执行通知逻辑,再调用目标对象的方法,完成增强。
2.4 AOP 核心配置方式(广度覆盖)
Spring AOP 支持3种配置方式,实际开发中以注解配置为主,XML配置为辅。
2.4.1 注解配置方式(主流,Spring Boot 首选)
核心注解:
-
@Aspect:标记类为切面类(必须标注,否则容器不会识别为切面)。 -
@Pointcut:定义切入点,标注在方法上(该方法仅作为切入点的载体,方法体为空)。 -
通知注解:@Before、@After、@AfterReturning、@AfterThrowing、@Around,标注在切面类的方法上,指定对应的通知逻辑和切入点。
-
@EnableAspectJAutoProxy:开启AOP自动代理(Spring Boot 中无需标注,自动开启;Spring 纯注解配置中需在配置类上标注)。
实战示例(日志切面):
// 1. 开启AOP自动代理(Spring 纯注解配置需添加)
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}
// 2. 切面类
@Aspect
@Component // 必须交给IoC容器管理
public class LogAspect {
// 3. 定义切入点(匹配com.xxx.service包下所有方法)
@Pointcut("execution(* com.xxx.service.*.*(..))")
public void logPointcut() {
// 方法体为空,仅作为切入点载体
}
// 4. 前置通知
@Before("logPointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// JoinPoint:获取目标方法信息(方法名、参数等)
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("前置通知:方法" + methodName + "开始执行,参数:" + Arrays.toString(args));
}
// 5. 环绕通知(统计方法执行时间)
@Around("logPointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 调用目标方法
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("环绕通知:方法" + joinPoint.getSignature().getName() + "执行时间:" + (end - start) + "ms");
return result; // 返回目标方法的返回值
}
// 6. 异常通知
@AfterThrowing(pointcut = "logPointcut()", throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
System.out.println("异常通知:方法" + methodName + "抛出异常,异常信息:" + e.getMessage());
}
}
2.4.2 XML 配置方式(传统方式)
通过XML文件配置切面、切入点和通知,适用于早期Spring项目,优点是配置清晰,缺点是繁琐。
配置示例:
<!-- 1. 开启AOP自动代理 -->
<aop:aspectj-autoproxy/>
<!-- 2. 注册目标对象和切面类 -->
<bean id="userService" class="com.xxx.service.UserService"/>
<bean id="logAspect" class="com.xxx.aspect.LogAspect"/>
<!-- 3. 配置切面 -->
<aop:config>
<!-- 定义切入点 -->
<aop:pointcut id="logPointcut" expression="execution(* com.xxx.service.*.*(..))"/>
<!-- 配置切面类和通知 -->
<aop:aspect ref="logAspect">
<aop:before pointcut-ref="logPointcut" method="beforeAdvice"/>
<aop:around pointcut-ref="logPointcut" method="aroundAdvice"/>
<aop:after-throwing pointcut-ref="logPointcut" method="afterThrowingAdvice" throwing="e"/>
</aop:aspect>
</aop:config>
2.4.3 自定义注解+AOP(实战常用)
通过自定义注解,实现“精准增强”(仅增强被自定义注解标注的方法),灵活性更高,适用于个性化需求(如自定义日志、权限校验)。
实战示例(自定义日志注解):
// 1. 自定义注解
@Target(ElementType.METHOD) // 仅用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface Log {
// 注解属性(可选)
String value() default ""; // 日志描述
}
// 2. 切面类(针对@Log注解)
@Aspect
@Component
public class CustomLogAspect {
// 切入点:匹配被@Log注解标注的方法
@Pointcut("@annotation(com.xxx.annotation.Log)")
public void customLogPointcut() {
}
// 环绕通知
@Around("customLogPointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Log logAnnotation = method.getAnnotation(Log.class);
String logDesc = logAnnotation.value();
System.out.println("自定义日志:" + logDesc + ",方法" + method.getName() + "开始执行");
Object result = joinPoint.proceed();
System.out.println("自定义日志:" + logDesc + ",方法" + method.getName() + "执行完成");
return result;
}
}
// 3. 使用自定义注解(业务方法)
@Service
public class UserService {
@Log("查询用户信息") // 标注自定义注解,触发切面
public User getUserById(Long id) {
// 业务逻辑
return new User(id, "张三");
}
}
2.5 AOP 核心问题与解决方案(深度实战)
2.5.1 通知执行顺序问题
当多个切面增强同一个目标方法时,通知的执行顺序由切面的优先级决定,默认优先级不确定(按容器扫描顺序),需手动指定。
解决方案:
-
通过
@Order注解标注切面类,注解值越小,优先级越高(如 @Order(1) 比 @Order(2) 优先级高)。 -
多个切面的通知执行顺序:优先级高的切面的前置通知先执行,后置通知后执行(环绕通知同理)。
2.5.2 动态代理的局限性
-
JDK 动态代理:仅支持接口代理,无法代理无接口的类。
-
CGLIB 动态代理:无法代理 final 类和 final 方法;无法代理 static 方法(static方法属于类,不属于实例,动态代理基于实例)。
解决方案:
-
避免使用 final 修饰需要被增强的类和方法。
-
static 方法的增强:通过其他方式(如手动调用通用方法),无法通过AOP增强。
2.5.3 同一类中方法调用,AOP 增强失效问题(高频实战问题)
问题描述:同一类中,被AOP增强的方法A调用另一个被AOP增强的方法B,方法B的AOP增强不生效(如方法B被@Log注解标注,但调用时未触发日志切面)。
原因:Spring AOP 基于动态代理,同一类中方法调用时,调用的是目标对象自身的方法,而非代理对象的方法,因此无法触发AOP增强。
解决方案:
-
自注入:在类中通过 @Autowired 注入自身的代理对象(需注意避免循环依赖,可配合@Lazy注解)。
-
通过 ApplicationContext 获取代理对象:在类中注入 ApplicationContext,通过 getBean() 方法获取自身的代理对象,再调用方法。
-
拆分类:将方法A和方法B拆分到不同的类中,避免同一类中方法调用。
示例(自注入方式):
@Service
public class UserService {
// 自注入自身的代理对象(配合@Lazy避免循环依赖)
@Autowired
@Lazy
private UserService userService;
@Log("方法A")
public void methodA() {
// 调用代理对象的methodB,触发AOP增强
userService.methodB();
}
@Log("方法B")
public void methodB() {
// 业务逻辑
}
}
2.5.4 切入点表达式精准度问题
问题描述:切入点表达式过于宽泛,导致不需要增强的方法被增强;或表达式过于狭窄,导致需要增强的方法未被增强。
解决方案:
-
精准编写 execution 表达式,明确包名、类名、方法名和参数类型。
-
使用 @annotation 表达式,仅增强被指定注解标注的方法,精准度更高。
-
结合 within、this 等表达式,进一步缩小切入点范围。
2.6 AOP 实战场景(广度延伸)
AOP 在实际开发中应用广泛,核心场景如下:
-
日志记录:记录方法的调用时间、参数、返回值、异常信息,无需在每个方法中重复编写日志代码。
-
事务管理:Spring 声明式事务的底层实现就是 AOP,通过 @Transactional 注解(本质是切面),在方法执行前开启事务,执行后提交/回滚事务。
-
权限校验:在方法执行前,校验用户是否有对应的权限,无权限则抛出异常,阻止方法执行。
-
性能监控:通过环绕通知,统计方法的执行时间,定位性能瓶颈。
-
异常处理:统一捕获方法抛出的异常,进行日志记录和统一返回处理(如返回标准化的错误信息)。
-
缓存增强:在方法执行前,查询缓存;方法执行后,更新缓存,减少数据库查询压力。