详解-Spring-AOP核心概念实现原理与配置实践
目录
详解 Spring AOP:核心概念、实现原理与配置实践
在软件开发中,日志记录、事务管理、权限校验等共性功能常常嵌入业务代码,导致代码冗余、耦合度高,维护难度大增。而 Spring AOP(面向切面编程)通过 “抽离共性、动态织入” 的思想,完美解决了这一问题。
一、Spring AOP 核心概念解析
1. 核心概念定义
- AOP(面向切面编程):将与业务无关但被多个业务模块共同调用的共性逻辑(如日志、事务)封装为 “切面”,通过动态织入的方式融入业务流程,以减少重复代码、降低耦合度,提升可维护性与可扩展性1。
- 切面(Aspect): “切点 + 通知” ,它是 AOP 实现的核心载体,包含了 “要增强的方法(切点)” 和 “增强的逻辑(通知)”,例如 “给用户查询方法加日志” 就是一个切面。
- 目标对象(Target):即被代理的对象,也就是包含核心业务逻辑的对象,比如处理用户数据的
UserService
类。 - 连接点(Join Point):连接点是指程序执行过程中可被增强的 “点”,包括方法调用、字段访问、构造器执行等,但 Spring AOP 仅支持方法级别的连接点,因此日常开发中可聚焦于 “所有方法” 这一范畴。
- 切点(Pointcut):是连接点的子集,指 “实际被增强的方法”。例如,若只给
UserService
的getUser()
和addUser()
方法加日志,这两个方法就是切点,而其他方法仅为连接点。 - 通知(Advice):“抽取的共性功能”,更完整的表述是 “包含增强逻辑与执行时机的组合”。通知决定了 “在什么时候(如方法执行前)做什么(如记录日志)”,具体分为前置、后置、返回、异常、环绕 5 种类型1。
- 织入(Weaving):将通知添加到切点的过程,Spring AOP 采用 “运行期织入”,即在程序运行时通过动态代理生成代理对象,完成通知与业务逻辑的融合。
- 代理对象(Proxy):织入后生成的对象,代理目标对象执行逻辑。调用者实际操作的是代理对象,而非原始目标对象,代理对象会先执行通知逻辑,再调用目标对象的业务方法。
2. 连接点的范围
Spring AOP 仅支持方法级连接点,不支持字段访问、构造器等其他类型的连接点。这是因为 Spring AOP 基于动态代理实现,动态代理技术只能拦截方法调用,无法处理其他程序执行点。
二、Spring AOP 动态代理实现原理
Spring AOP 底层依赖动态代理技术,会根据目标对象是否实现接口,自动选择以下两种代理方式,确保在不同场景下都能实现增强逻辑:
1. JDK 动态代理
当目标对象已实现至少一个接口时,Spring AOP 会使用 JDK 动态代理,核心依赖 java.lang.reflect.InvocationHandler
接口和 Proxy
类:
- 实现逻辑:代理对象会 “实现目标对象的所有接口”,因此调用者需通过接口类型接收代理对象(如
UserService proxy = context.getBean(UserService.class)
)。当调用代理对象的接口方法时,会触发InvocationHandler
的invoke()
方法,在该方法中先执行通知逻辑,再通过反射调用目标对象的原始方法。 - 适用场景:目标对象有明确的接口定义,例如
UserService
实现了IUserService
接口,OrderService
实现了IOrderService
接口等。
2. CGLIB 动态代理
当目标对象未实现任何接口时,无法使用 JDK 动态代理,此时 Spring AOP 会使用 CGLIB 动态代理,核心依赖 net.sf.cglib.proxy.MethodInterceptor
接口和 Enhancer
类:
- 实现逻辑:通过 “继承目标对象” 生成代理类(代理对象是目标对象的子类),因此调用者可通过目标对象类型接收代理对象(如
UserServiceImpl proxy = context.getBean(UserServiceImpl.class)
)。当调用代理对象的方法时,会被MethodInterceptor
的intercept()
方法拦截,先执行通知逻辑,再通过MethodProxy
调用目标对象的原始方法。 - 注意事项:若目标对象是
final
类(无法被继承),或目标方法是final
方法(无法被重写),CGLIB 无法生成代理,会直接抛出异常。
三、AOP 通知类型与切点表达式
1. 5 种通知类型(含执行时机与作用)
通知的核心是 “增强逻辑 + 执行时机”,Spring AOP 定义了 5 种通知类型,覆盖方法执行的全生命周期:
- 前置通知(Before):在目标方法执行前执行,常用于参数校验、日志记录(如 “方法开始执行”),无论目标方法是否抛异常都会执行;
- 后置通知(After):在目标方法执行后执行(无论是否异常),常用于资源清理(如关闭连接),但无法获取目标方法的返回值;
- 返回通知(After-returning):在目标方法正常返回后执行,常用于处理返回结果(如记录返回数据),若目标方法抛异常则不执行;
- 异常通知(After-throwing):在目标方法抛出异常后执行,常用于异常处理(如记录错误日志),可指定捕获特定类型的异常;
- 环绕通知(Around):包裹目标方法,可在方法执行前后自定义逻辑(如性能监控、事务控制),需手动调用
ProceedingJoinPoint.proceed()
触发目标方法,若不调用则目标方法不会执行1。
2. 切点表达式:精准定位增强方法
- 语法格式:
execution(修饰符 返回值 包名.类名.方法名(参数列表))
1; - 通配符说明:
*
:匹配 “任意一个”,可用于修饰符、返回值、包名、类名、方法名、参数类型,例如execution(* com.service.*.*(..))
表示 “com.service 包下所有类的所有方法”;..
:匹配 “任意多个”,包名中使用表示 “任意层级的包”(如com..service
表示 com 下所有子包的 service 包),参数列表中使用表示 “任意个数、任意类型的参数”(如(..)
表示无参数或任意参数);
- 常用示例:
- 匹配指定方法:
execution(public void com.service.UserService.getUser(int))
(匹配UserService
的getUser
方法,修饰符 public,返回值 void,参数为 int); - 匹配包下所有方法:
execution(* com.service.*.*(..))
(com.service 包下所有类的所有方法); - 匹配子包下所有方法:
execution(* com..service.*.*(..))
(com 及其子包下 service 包的所有方法)1。
- 匹配指定方法:
四、Spring AOP 两种配置方式(XML + 注解)
1. 前置准备:引入依赖
无论哪种配置方式,都需引入 Spring AOP 与 AspectJ 依赖(AspectJ 提供切点表达式支持),Maven 依赖如下:
<!-- Spring 核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.20</version>
</dependency>
<!-- Spring AOP 依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.20</version>
</dependency>
<!-- AspectJ 依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
2. XML 配置方式
适合传统项目或需要集中管理切面的场景,步骤如下:
- 定义目标对象:编写业务类(如
UserService
),实现核心业务逻辑; - 定义切面类:编写包含通知逻辑的类(如
LogAspect
),提供前置通知、环绕通知等方法; - XML 配置织入:通过
<aop:config>
标签配置目标对象、切面类、切点与通知的关联,示例如下:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置目标对象 -->
<bean id="userService" class="com.service.UserService"/>
<!-- 配置切面类 -->
<bean id="logAspect" class="com.aspect.LogAspect"/>
<!-- AOP 织入配置 -->
<aop:config>
<aop:aspect ref="logAspect">
<!-- 定义切点 -->
<aop:pointcut id="userPointcut" expression="execution(* com.service.UserService.*(..))"/>
<!-- 配置通知:前置通知关联切点 -->
<aop:before method="beforeAdvice" pointcut-ref="userPointcut"/>
<!-- 配置环绕通知关联切点 -->
<aop:around method="aroundAdvice" pointcut-ref="userPointcut"/>
</aop:aspect>
</aop:config>
</beans>
3. 注解配置方式
适合现代项目,配置更简洁,步骤如下:
- 开启 AOP 注解支持:在 Spring 配置类或 XML 中添加
@EnableAspectJAutoProxy
(注解配置)或<aop:aspectj-autoproxy/>
(XML 配置); - 目标对象加注解:业务类上添加
@Service
等注解,交给 Spring 管理; - 切面类加注解:通过
@Aspect
声明切面类,@Pointcut
定义切点,@Before
/@Around
等注解配置通知,示例如下:
// 切面类
@Aspect
@Service
public class LogAspect {
// 定义切点
@Pointcut("execution(* com.service.UserService.*(..))")
public void userPointcut() {}
// 前置通知
@Before("userPointcut()")
public void beforeAdvice() {
System.out.println("前置通知:方法开始执行");
}
// 环绕通知
@Around("userPointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // 调用目标方法
long end = System.currentTimeMillis();
System.out.println("环绕通知:方法执行耗时 " + (end - start) + "ms");
return result;
}
}