Spring AOP
学习目标
- 理解 AOP(面向切面编程) 要解决的问题:将横切关注点(如日志、事务、权限)从业务逻辑中剥离。
- 掌握 AOP 的 核心概念:连接点、切入点、通知、切面、目标对象。
- 能在 Spring 中通过
@Aspect与 通知注解 编写简单切面。
1. AOP 核心概念
AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 中除 IoC、DI 外的重要能力,用于把“分散在多个类里的同一类逻辑”(如日志、权限、事务)集中到一个切面中,减少重复、提高可维护性。下面五个概念是理解 AOP 的基础:
| 概念 | 英文 | 含义 |
|---|---|---|
| 连接点 | JoinPoint | 程序执行过程中的一个“点”,例如方法的执行、异常的处理等。在 Spring AOP 中,连接点通常指方法的执行。 |
| 切入点 | PointCut | 用来限定在哪些连接点上应用通知,是一组连接点的集合。通过表达式(如 execution(...))描述要拦截的方法。 |
| 通知 | Advice | 切面在某个连接点上要执行的动作。常见类型:前置(before)、后置(after)、环绕(around)、返回后(after returning)、异常后(after throwing)。 |
| 切面 | Aspect | 对“横切关注点”的模块化,把通知和切入点组合在一起,描述“在哪些地方、做什么事”。 |
| 目标对象 | Target | 被一个或多个切面增强(advise) 的对象,即我们写的普通业务 Bean。 |
关系简要概括:
- 连接点是“可被拦截的位置”;切入点是“我们选中的那些连接点”。
- 通知是“在切入点上执行的代码”;切面 = 切入点 + 通知。
- 目标对象是“被切面作用到的 Bean”。
2. 在 Spring 中的对应关系
- 连接点:通常对应 Bean 方法的执行。
- 切入点:用 切点表达式(如
@Pointcut("execution(* com.example.service.*.*(..))"))定义。 - 通知:在 Spring AOP 中通过
@Before、@After、@Around、@AfterReturning、@AfterThrowing等注解标注方法,该方法即为通知。 - 切面:一个用
@Aspect标注的类,内部包含切入点与若干通知方法。 - 目标对象:被代理的 Service、Controller 等 Bean。
2.1 连接点对象:JoinPoint / ProceedingJoinPoint
在 AOP 中,连接点可以简单理解为“可以被 AOP 控制的方法”,目标对象中的每个方法理论上都可以作为连接点。
在 Spring AOP 中,连接点又特指“方法的执行”。
在 Spring 中,JoinPoint 抽象了“连接点”对象,可以在通知方法中获取方法执行时的相关信息,例如目标类名、方法名、方法参数等:
- 对于
@Before、@After、@AfterReturning、@AfterThrowing等通知,可以在方法参数中声明JoinPoint joinPoint来获取连接点信息。 - 对于
@Around环绕通知,需要使用其子类型ProceedingJoinPoint,既能获取连接点信息,又能通过proceed()控制目标方法的执行。
简单记忆:
- 获取连接点信息:普通通知用
JoinPoint,环绕通知用ProceedingJoinPoint。 - 控制方法是否、何时执行:只有
ProceedingJoinPoint.proceed()能做到。
3. Spring AOP 通知类型
在切面类中,用以下注解标注的方法即为不同类型的通知,它们决定了“在目标方法的什么时机”执行:
| 注解 | 通知类型 | 执行时机 |
|---|---|---|
@Around | 环绕通知 | 此注解标注的通知方法在目标方法前、后都会执行(可控制是否调用目标方法、修改参数或返回值)。 |
@Before | 前置通知 | 此注解标注的通知方法在目标方法之前执行。 |
@After | 后置通知 | 此注解标注的通知方法在目标方法之后执行,无论目标方法是否抛异常都会执行。 |
@AfterReturning | 返回后通知 | 此注解标注的通知方法在目标方法正常返回之后执行,若目标方法抛异常则不会执行。 |
@AfterThrowing | 异常后通知 | 此注解标注的通知方法在目标方法抛出异常之后执行。 |
简要记忆:
- Before:进方法前。
- After:出方法后(有无异常都执行)。
- AfterReturning:正常返回后才执行。
- AfterThrowing:抛异常后才执行。
- Around:前后都包住,可自定义调用时机与返回值。
3.1 @Around 使用注意
- 注意 1:
@Around(环绕通知) 需要显式调用ProceedingJoinPoint.proceed()才会执行目标方法;其他通知类型(如@Before、@After等)不需要显式考虑目标方法的执行,框架会自动处理。 - 注意 2:
@Around通知方法的返回值必须声明为Object,以便接收原始(目标)方法的返回值,再根据需要返回或包装。
4. 示例:完整切面类
下面是一个切面类示例,对 com.itheima.service 包下所有方法应用五种通知,便于理解各通知的执行时机与写法:
@Slf4j
@Component
@Aspect
public class MyAspect1 {
// 前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(JoinPoint joinPoint) {
log.info("before ...");
}
// 环绕通知:需显式调用 proceed(),返回值类型为 Object
@Around("execution(* com.itheima.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
// 调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
// 原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}
// 后置通知:无论是否异常都会执行
@After("execution(* com.itheima.service.*.*(..))")
public void after(JoinPoint joinPoint) {
log.info("after ...");
}
// 返回后通知:仅当程序正常执行完毕时执行
@AfterReturning("execution(* com.itheima.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint) {
log.info("afterReturning ...");
}
// 异常后通知:仅当程序抛出异常时执行
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
public void afterThrowing(JoinPoint joinPoint) {
log.info("afterThrowing ...");
}
}- 切点表达式
execution(* com.itheima.service.*.*(..))表示:拦截com.itheima.service包下任意类的任意方法(任意参数、任意返回值)。 - 若同一切点多处使用,可用
@Pointcut抽取后复用(见下节)。
5. @Pointcut 抽取切点
作用:将公共的切点表达式抽取出来,需要用到时引用该切点名称即可,避免在多个通知里重复写同一段 execution(...),便于维护和修改。
定义方式:在切面类中定义一个 空方法,方法上标注 @Pointcut("切点表达式"),方法名即该切点的标识,供后续通知引用。
// 定义切点:DeptServiceImpl 类中所有方法
@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
public void pt() {}引用方式:在 @Before、@Around 等通知注解中,不再写完整表达式,而是写 "方法名()",例如:
@Around("pt()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 环绕逻辑:如统计耗时等
long begin = System.currentTimeMillis();
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.info("耗时: {} ms", end - begin);
return result;
}这样,所有使用 pt() 的通知都会应用同一套切点规则;若要修改拦截范围,只需改一处 @Pointcut 定义即可。
6. 切入点表达式 execution
execution 是 Spring AOP 中最常用的切入点表达式,根据方法的返回值、包名、类名、方法名、方法参数等信息进行匹配。
6.1 语法格式
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)其中带 ? 的表示可以省略的部分:
| 部分 | 说明 |
|---|---|
| 访问修饰符 | 可省略(如 public、protected)。 |
| 包名.类名 | 可省略,常用通配符代替。 |
| throws 异常 | 可省略(指方法声明上抛出的异常,不是运行时实际抛出的异常)。 |
示例(精确匹配一个方法):
@Before("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
public void before(JoinPoint joinPoint) { ... }6.2 通配符
可以使用通配符描述切入点,使表达式更简洁、可复用:
| 通配符 | 含义 | 示例 |
|---|---|---|
* | 单个独立的任意符号。可通配:任意返回值、包名、类名、方法名、任意类型的一个参数,或包/类/方法名的一部分。 | execution(* com.*.service.*.update*(*)):com 下任意一层子包中的 service 包、任意类、以 update 开头的方法、且只有一个参数。 |
.. | 多个连续的任意符号。可通配:任意层级的包,或任意类型、任意个数的参数。 | execution(* com.itheima..DeptService.*(..)):com.itheima 及其任意子包下的 DeptService 类、任意方法、任意参数。 |
- 方法参数中:
*表示一个参数,..表示零个或多个参数。 - 包路径中:
..表示当前包及子包(如com.itheima..表示com.itheima及其下所有层级)。
7. @annotation 切入点表达式
当需要匹配多个无规则的方法(例如同时匹配 list() 和 delete())时,若用 execution 描述,就要把多个表达式用 || 等组合起来,书写繁琐。可以改用 @annotation 切入点表达式:通过自定义注解标记“要拦截的方法”,切点只写“带有该注解的方法”,从而简化书写。
7.1 实现步骤
- 编写自定义注解(可带
@Retention(RetentionPolicy.RUNTIME)、@Target(ElementType.METHOD)等,保证运行时、方法上可用)。 - 在业务类中,将需要作为连接点的方法上添加该自定义注解。
- 在切面中使用
@annotation(自定义注解全类名)作为切入点表达式,例如@Before("@annotation(com.example.anno.MyLog)")。
示例:
// 1. 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {}
// 2. 在 Service 方法上添加注解
@Service
public class DeptServiceImpl implements DeptService {
@MyLog
public List<Dept> list() { ... }
@MyLog
public void delete(Integer id) { ... }
}
// 3. 切面中使用 @annotation
@Around("@annotation(com.example.anno.MyLog)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 统一逻辑:如日志、耗时等
return pjp.proceed();
}这样,凡是标注了 @MyLog 的方法都会被拦截,无需为 list()、delete() 各写一条 execution。
8. 切入点表达式书写建议
- 业务方法命名尽量规范,便于用 execution 快速匹配,例如:
findXxx、updateXxx、deleteXxx。 - 描述切入点时优先基于接口(如
execution(* com.example.service.DeptService.*(..))),而不是直接写实现类,便于扩展和替换实现。 - 在满足业务的前提下尽量缩小匹配范围:例如包名尽量用
*匹配单层包,少用..匹配多级包,减少误伤、提高可读性。