一、AOP与代理模式的核心关联
AOP(面向切面编程)的核心作用是对业务逻辑进行横向扩展,其底层实现依赖于代理模式。简单来说,AOP可以看作是代理模式的简化与升级,通过自动为目标接口或类生成代理对象,无需开发者手动编写大量代理代码,即可实现对目标方法的增强(如日志记录、权限校验等)。
二、代理设计模式详解
代理模式的核心是为目标对象提供一个代理对象,由代理对象控制对目标对象的访问,并在访问前后执行额外的增强逻辑。根据代理类创建时机的不同,分为静态代理和动态代理两类。
2.1 静态代理
静态代理由开发者手动创建代理类,代理类与目标类通常实现同一个接口。在程序编译阶段,代理类就已经存在,其缺点是代码冗余------每增加一个目标类,就需要对应编写一个代理类,维护成本高。
核心特点:编译期确定代理类,手动编码代理逻辑,一对一对应目标类。
2.2 动态代理
动态代理无需开发者手动编写代理类,而是在程序运行时,由框架(如JDK、CGLIB)动态生成代理类字节码并加载到内存中。动态代理可以批量为多个目标类生成代理,极大减少了代码冗余,是AOP的核心实现方式。
2.2.1 动态代理的两种实现方案
-
JDK动态代理(JDK自带,无需额外导包)
-
核心依赖:
java.lang.reflect.InvocationHandler接口和java.lang.reflect.Proxy类。 -
限制:只能代理实现了接口的类,无法直接代理无接口的普通类。
-
核心代码示例:
`// 实现InvocationHandler接口,定义增强逻辑
public class MyInvocationHandler implements InvocationHandler {
// 目标对象(被代理的对象)
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
// 代理对象调用方法时,会触发该方法执行
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 方法执行前增强(如权限校验)
System.out.println("JDK代理:目标方法执行前增强");
// 调用目标对象的原始方法
Object result = method.invoke(target, args);
// 方法执行后增强(如日志记录)
System.out.println("JDK代理:目标方法执行后增强");
return result;
}
// 获取代理对象
public Object getProxy() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
}`
-
-
CGLIB动态代理(第三方技术,需导入依赖)
-
核心原理:基于ASM字节码框架,动态生成目标类的子类作为代理类,通过重写目标方法实现增强。
-
优势:无需目标类实现接口,既可以代理接口,也可以代理普通类。
-
依赖说明:Spring已集成CGLIB,导入Spring相关依赖后即可使用(如下文AOP依赖)。
-
三、AOP核心知识(重点)
3.1 AOP的定义与核心思想
AOP(Aspect-Oriented Programming,面向切面编程)是一种"横向"编程思想,与传统"纵向"的OOP(面向对象编程)互补。OOP通过类的继承和多态封装业务逻辑,而AOP将分散在各个业务逻辑中的公共功能(如事务、日志)抽取为"切面",在不修改业务代码的前提下,通过"织入"机制将切面与业务逻辑结合,实现公共功能的统一管理。
3.2 AOP核心原理
AOP底层通过动态代理实现:开发者指定需要增强的目标类和增强逻辑(切面)后,Spring容器会在运行时为目标类生成代理对象。当程序调用目标方法时,实际执行的是代理对象的方法,代理对象会先执行切面中的增强逻辑,再调用目标对象的原始方法,从而完成"增强"。
3.3 AOP的核心优势
-
减少重复代码:将公共功能(如日志、权限)集中在切面中,避免在多个业务类中重复编写。
-
降低耦合度:业务逻辑与公共功能分离,业务类只关注核心业务,切面只关注增强逻辑。
-
便于维护:公共功能的修改只需修改切面代码,无需改动所有业务类。
3.4 AOP的典型应用场景
-
事务管理:目标方法执行前开启事务,执行后提交/回滚事务。
-
权限校验:目标方法执行前校验用户权限,无权限则拦截。
-
日志记录:记录目标方法的调用参数、返回值、执行耗时等。
-
性能检测:统计目标方法的执行时间,分析系统性能瓶颈。
-
异常处理:捕获目标方法抛出的异常,执行统一的异常处理逻辑。
3.5 AOP开发必备依赖
使用Spring AOP需导入以下Maven依赖(以Spring 5.1.6.RELEASE为例):
xml
<!-- Spring核心容器依赖,提供IOC功能 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<!-- Spring AOP依赖,提供AOP相关注解和工具类 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
四、AOP的核心组件与通知类型
4.1 AOP核心组件
-
切面(Aspect):封装公共增强逻辑的类(如包含日志、事务逻辑的类)。
-
通知(Advice):切面中具体的增强逻辑方法(如前置通知、环绕通知)。
-
连接点(JoinPoint):目标类中可以被增强的方法(如业务类的查询、修改方法)。
-
切入点(Pointcut):通过表达式筛选出的、需要被增强的连接点(如"所有ServiceImpl类的方法")。
-
织入(Weaving):将切面的通知与目标类的连接点结合的过程(Spring在运行时自动完成)。
4.2 五种通知类型及应用场景
| 通知类型 | 执行时机 | 核心作用 | 关键API |
|---|---|---|---|
| 环绕通知(Around) | 目标方法执行前+执行后 | 事务管理(开启/提交)、性能检测(计时) | ProceedingJoinPoint(可控制目标方法是否执行) |
| 前置通知(Before) | 目标方法执行前 | 权限校验、参数校验 | JoinPoint(获取目标对象、方法名等信息) |
| 后置通知(After) | 目标方法执行后(无论是否异常) | 释放资源、基础日志记录 | 无特殊依赖,执行时机稳定 |
| 后置返回通知(AfterReturning) | 目标方法正常执行完毕后 | 获取目标方法返回值、结果校验 | 通过参数绑定返回值 |
| 异常通知(AfterThrowing) | 目标方法抛出异常时 | 异常日志记录、异常恢复 | 通过参数绑定异常对象 |
五、AOP使用实现(重点)
Spring AOP的使用分为两种方式:XML配置开发和注解开发。注解开发更简洁高效,是实际开发中的主流方式;XML配置则更适合需要统一管理切面的场景。
5.1 XML配置方式开发AOP
XML配置通过<aop:config>等标签定义切面、通知和切入点,核心步骤为:配置目标类、配置切面类、织入通知(将通知与目标方法绑定)。
5.1.1 环绕通知(最灵活的通知类型)
环绕通知可以完全控制目标方法的执行过程(包括是否执行、执行前后增强),是事务管理的首选。
- 步骤1:创建切面类(含环绕通知方法)`import org.aspectj.lang.ProceedingJoinPoint;
// 切面类:封装增强逻辑
public class MyAspect {
/*
* 环绕通知方法
* ProceedingJoinPoint:连接点对象,可获取目标方法信息并控制其执行
*/
public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 目标方法执行前增强(如开启事务)
System.out.println("环绕通知:开启事务");
// 2. 执行目标方法(必须调用,否则目标方法不会执行)
Object result = joinPoint.proceed(); // 接收目标方法的返回值
// 3. 目标方法执行后增强(如提交事务)
System.out.println("环绕通知:提交事务");
return result; // 将目标方法的返回值返回给调用者
}
}`
-
步骤2:编写Spring XML配置文件 `<?xml version="1.0" encoding="UTF-8"?>
<aop:config proxy-target-class="true">
<aop:aspect ref="myAspect">
<aop:around
method="myAround"
pointcut="execution(* com.luo.service.impl.UserServiceImpl.findUserInfoById(...))" />
</aop:aspect>
</aop:config>
`
5.1.2 前置通知
前置通知在目标方法执行前执行,常用于权限校验、参数校验等场景。通过JoinPoint对象可获取目标对象、方法名等信息。
- 切面类添加前置通知方法`import org.aspectj.lang.JoinPoint;
public class MyAspect {
/* 前置通知方法:JoinPoint用于获取目标方法相关信息 */
public void myBefore(JoinPoint joinPoint) {
// 获取目标对象(被代理的业务对象)
Object target = joinPoint.getTarget();
// 获取目标方法签名(包含方法名、参数等信息)
Signature signature = joinPoint.getSignature();
System.out.println("前置通知:目标对象=" + target);
System.out.println("前置通知:目标方法=" + signature.getName());
System.out.println("前置通知:执行权限校验逻辑");
}
}`
- XML中配置前置通知
<aop:config proxy-target-class="true"> <aop:aspect ref="myAspect"><!-- 前置通知配置 --> <aop:before method="myBefore" pointcut="execution(* com.luo.service.impl.HouseServiceImpl.*(..))" /> <!-- 切入点表达式含义:匹配HouseServiceImpl类的所有方法(任意返回值、任意参数) --> </aop:aspect> </aop:config>
5.1.3 后置通知
后置通知在目标方法执行后执行(无论目标方法是否抛出异常),常用于释放资源、基础日志记录。
-
切面类添加后置通知方法
public class MyAspect { /* 后置通知方法:无特殊参数,执行时机稳定 */ public void myAfter() { System.out.println("后置通知:执行日志记录逻辑"); System.out.println("后置通知:释放数据库连接等资源"); } } -
XML中配置后置通知
<aop:after method="myAfter" pointcut="execution(* com.luo.service.impl.HouseServiceImpl.*(..))" />
5.1.4 后置返回通知
后置返回通知在目标方法正常执行完毕后执行,可通过参数绑定获取目标方法的返回值,常用于结果校验、返回值加工。
-
切面类添加后置返回通知方法 `public class MyAspect {
/*
- 后置返回通知方法
- ret:绑定目标方法的返回值,变量名需与XML中returning属性一致
*/
public void myAfterRet(Object ret) {
System.out.println("后置返回通知:目标方法返回值=" + ret);
// 可对返回值进行加工(如脱敏、格式转换)
}
}`
-
XML中配置后置返回通知
<aop:after-returning method="myAfterRet" pointcut="execution(* com.luo.service.impl.HouseServiceImpl.houseAllInfo(..))" returning="ret" /> <!-- returning:指定绑定返回值的参数名 -->
5.1.5 异常通知
异常通知在目标方法抛出异常时执行,可通过参数绑定获取异常对象,常用于异常日志记录、异常恢复。
-
切面类添加异常通知方法 `public class MyAspect {
/*
- 异常通知方法
- e:绑定目标方法抛出的异常对象,变量名需与XML中throwing属性一致
*/
public void myAfterThrow(Exception e) {
System.out.println("异常通知:目标方法抛出异常=" + e.getMessage());
System.out.println("异常通知:执行异常日志记录");
}
}`
-
XML中配置异常通知
<aop:after-throwing method="myAfterThrow" pointcut="execution(* com.luo.service.impl.HouseServiceImpl.*(..))" throwing="e" /> <!-- throwing:指定绑定异常对象的参数名 -->
5.1.6 抽取公共切入点表达式
当多个通知需要使用相同的切入点表达式时,可通过<aop:pointcut>标签抽取公共表达式,减少重复配置。
xml
<aop:config proxy-target-class="true">
<!-- 抽取公共切入点:id为表达式唯一标识,expression为切入点表达式 -->
<aop:pointcut
id="houseServicePointcut"
expression="execution(* com.luo.service.impl.HouseServiceImpl.*(..))" />
<aop:aspect ref="myAspect">
<!-- 引用公共切入点:通过pointcut-ref指向切入点ID -->
<aop:before method="myBefore" pointcut-ref="houseServicePointcut" />
<aop:after method="myAfter" pointcut-ref="houseServicePointcut" />
</aop:aspect>
</aop:config>
5.2 注解方式开发AOP(主流)
注解开发通过在类和方法上添加AOP相关注解,替代XML配置,更加简洁高效。核心步骤为:开启组件扫描和AOP注解支持、定义切面类、配置通知和切入点。
5.2.1 核心注解说明
| 注解 | 作用 |
|---|---|
| @Component | 将切面类交给Spring IOC容器管理,生成Bean实例 |
| @Aspect | 标识当前类为切面类,Spring会自动识别并处理 |
| @Around | 标识方法为环绕通知,参数为切入点表达式 |
| @Before | 标识方法为前置通知,参数为切入点表达式 |
| @After | 标识方法为后置通知,参数为切入点表达式 |
| @AfterReturning | 标识方法为后置返回通知,可通过returning绑定返回值 |
| @AfterThrowing | 标识方法为异常通知,可通过throwing绑定异常对象 |
| @Pointcut | 抽取公共切入点表达式,供其他通知引用 |
5.2.2 完整实现示例
-
步骤1:编写Spring XML配置文件(开启注解支持) `<?xml version="1.0" encoding="UTF-8"?>
<context:component-scan base-package="com.luo.service,com.luo.aspect" />
` -
步骤2:编写切面类(使用注解配置通知) `import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// 1. @Component:将切面类交给IOC容器管理
// 2. @Aspect:标识当前类为切面类
@Component
@Aspect
public class AspectTest {
// 抽取公共切入点表达式:@Pointcut注解定义,方法体为空(仅作标识)
@Pointcut("execution(* com.luo.service.impl.UserServiceImpl.*(...))")
public void userServicePointcut() {} // 方法名作为切入点标识
/* 1. 环绕通知:引用公共切入点(通过方法名引用) */
@Around("userServicePointcut()")
public Object myAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知:目标方法执行前增强");
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("环绕通知:目标方法执行后增强");
return result; // 注意:返回目标方法的返回值,而非joinPoint
}
/* 2. 前置通知:直接编写切入点表达式 */
@Before("execution(* com.luo.service.impl.UserServiceImpl.findUserInfoById(..))")
public void myBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("前置通知:目标方法=" + methodName + ",执行权限校验");
}
/* 3. 后置返回通知:通过returning绑定返回值 */
@AfterReturning(
value = "userServicePointcut()", // 引用公共切入点
returning = "obj" // 绑定返回值的参数名,需与方法参数一致
)
public void myAfterRet(Object obj) {
System.out.println("后置返回通知:目标方法返回值=" + obj);
}
/* 4. 异常通知:通过throwing绑定异常对象 */
@AfterThrowing(
value = "userServicePointcut()",
throwing = "e" // 绑定异常的参数名,需与方法参数一致
)
public void myException(Exception e) {
System.out.println("异常通知:捕获异常=" + e.getMessage());
}
/* 5. 后置通知:无论是否异常都会执行 */
@After("userServicePointcut()")
public void myAfter() {
System.out.println("后置通知:执行资源释放逻辑");
}
}`
- 步骤3:目标业务类(需添加@Service注解,被组件扫描识别) `import org.springframework.stereotype.Service;
import com.luo.service.UserService;
// @Service:标识为业务层组件,被Spring扫描并创建Bean
@Service
public class UserServiceImpl implements UserService {
@Override
public String findUserInfoById(Integer id) {
if (id == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
return "用户ID:" + id + ",用户名:张三";
}
}`
六、AOP关键注意事项
-
代理模式选择:
proxy-target-class="true"时,Spring强制使用CGLIB代理;为false时,优先使用JDK代理(目标类实现接口),无接口则自动使用CGLIB。 -
环绕通知必须调用
ProceedingJoinPoint.proceed()方法,否则目标方法不会执行;且需返回目标方法的返回值,否则调用者无法获取结果。 -
切入点表达式语法:
execution(* com.luo.service.impl.*ServiceImpl.*(..))表示"匹配com.luo.service.impl包下所有以ServiceImpl结尾的类的所有方法",其中*表示任意返回值,(..)表示任意参数。 -
注解开发时,切面类和目标类必须都交给Spring IOC管理(添加
@Component、@Service等注解),否则AOP无法生效。 -
异常通知仅在目标方法抛出的异常与方法参数声明的异常类型匹配时才会执行(如声明
Exception则匹配所有异常)。