My App

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 异常?)

其中? 的表示可以省略的部分:

部分说明
访问修饰符可省略(如 publicprotected)。
包名.类名可省略,常用通配符代替。
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 实现步骤

  1. 编写自定义注解(可带 @Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD) 等,保证运行时、方法上可用)。
  2. 在业务类中,将需要作为连接点的方法上添加该自定义注解
  3. 在切面中使用 @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 快速匹配,例如:findXxxupdateXxxdeleteXxx
  • 描述切入点时优先基于接口(如 execution(* com.example.service.DeptService.*(..))),而不是直接写实现类,便于扩展和替换实现。
  • 在满足业务的前提下尽量缩小匹配范围:例如包名尽量用 * 匹配单层包,少用 .. 匹配多级包,减少误伤、提高可读性。

On this page