【Java Web 快速入门】十、AOP

发布于:2025-08-16 ⋅ 阅读:(13) ⋅ 点赞:(0)

AOP

AOP 基础

概述

AOP 英文全称为 Aspect Oriented Programming,中文译为面向切面编程或面向方面编程,本质是面向特定方法编程,可在不改动原始方法的基础上对其进行功能增强或改变。

场景:项目中部分功能运行较慢,定位执行耗时较长的业务方法,需要统计每一个业务方法的执行耗时,若逐个修改业务方法添加计时逻辑过于繁琐,而 AOP 可解决此问题。

AOP 实现逻辑:通过定义模板方法,在其中编写公共逻辑(如记录开始和结束时间),中间运行原始业务方法,项目运行时会自动执行模板方法而非直接执行原始方法,类似动态代理技术。动态代理是面向切面编程最主流的实现,而 SpringAOP 是 Spring 框架的高级技术,旨在管理 Bean 对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。

快速入门

统计各个业务层方法执行耗时

导入依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

入门程序代码:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class TimeAspect {

    @Around("execution(* com.example.demo.service.*.*(..))") // 切入点表达式
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {

        // 记录开始时间
        long start = System.currentTimeMillis();

        // 调用原始方法
        Object result = joinPoint.proceed();

        // 记录结束时间
        long end = System.currentTimeMillis();

        // 计算耗时
        long time = end - start;
        log.info("方法耗时:{}毫秒", time);

        return result;
    }
}

@Aspect:Spring AOP 的注解,标识该类是一个切面类(Aspect),用于定义横切逻辑(如日志、性能统计等)

@Around:AOP 中的环绕通知注解,表示该方法会包裹目标方法的执行 —— 既可以在目标方法执行前做操作,也可以在执行后做操作

注解内的表达式execution(* com.example.demo.service.*.*(..))切入点表达式,用于指定哪些方法会被该切面拦截:

  • *:第一个*表示匹配任意返回值类型的方法
  • com.example.demo.service.*:表示匹配com.example.demo.service包下的所有类
  • *:表示匹配类中的所有方法
  • (..):表示匹配任意参数(任意数量、任意类型)的方法
    综上,该表达式会拦截com.example.demo.service包下所有类的所有方法

ProceedingJoinPoint joinPoint:环绕通知特有的参数,用于访问目标方法的信息(如方法名、参数等),并通过joinPoint.proceed()手动调用目标方法

逻辑流程:

  1. 执行目标方法前:通过System.currentTimeMillis()记录当前时间(开始时间)。
  2. 调用joinPoint.proceed():执行被拦截的目标方法,并获取其返回值(result)。
  3. 执行目标方法后:再次记录时间(结束时间),计算两者差值(即方法执行耗时),并通过log.info打印耗时日志。
  4. 返回目标方法的结果:保证业务逻辑不受切面影响(调用方仍能拿到原方法的返回值)。

执行查询所有部门操作的结果如下:

在这里插入图片描述

AOP 的应用场景:包括记录操作日志(记录操作者、时间、参数、返回值等)、完成项目权限控制、实现事务管理(Spring 事务管理底层基于 AOP)等。

AOP 的优势:具有代码无侵入(不修改原始业务方法)、减少重复代码、提高开发效率、维护方便(只需修改 AOP 中的方法)等优势。

AOP 可以理解为 “用代理技术实现的、带有精准目标匹配能力的公共逻辑提取与自动增强机制”。提取公共类是其对逻辑的组织方式,代理技术是其实现无侵入增强的手段,而 “面向特定方法编程”(通过切入点精准匹配)和 “自动织入” 才是其核心价值。

核心概念

连接点:JoinPoint,可以被 AOP 控制的方法(暗含方法执行时的相关信息)

通知:advice,指重复的逻辑,也就是共性功能(最终体现为 AOP 中的一个方法)

切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用(用切入点表达式来表达)

切面:Aspect,描述通知与切入点的对应关系(通知+切入类)

目标对象:Target,通知所应用的对象

AOP 执行流程:

在这里插入图片描述

  1. 目标对象(DeptServiceImpl):标记@Service的业务实现类,含实际要执行的业务方法(如list() ),是代理增强的 “原始对象”。
  2. 切面(TimeAspect):标记@Aspect,通过@Around定义切点(匹配service层方法),实现 “方法耗时统计” 的横切逻辑,会包裹目标方法执行。
  3. 代理对象(DeptServiceProxy):由 Spring 动态生成(或手动模拟),实现与目标对象相同接口(DeptService ),内部会先执行切面逻辑,再调用目标对象方法,起到 “增强 + 转发” 作用。
  4. 流程逻辑:
    • 启动时,Spring 识别切面与目标对象,为目标对象创建代理
    • Controller 注入的是代理对象,调用deptService.list()时,先进入代理逻辑(执行切面的耗时统计),再转发调用DeptServiceImpl的真实方法;
    • 最终实现 “不修改业务类,却能附加通用逻辑(如监控、日志)” 的 AOP 思想。

AOP 进阶

通知类型

@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行

@Before:前置通知,此注解标注的通知方法在目标方法前被执行

@After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行

@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

以下是测试代码:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class TestAspect {

    @Before("execution(* com.example.demo.service.*.*(..))")
    public void before() {
        log.info("before");
    }

    @Around("execution(* com.example.demo.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("around before");
        Object ret = joinPoint.proceed();
        log.info("around after");
        return ret;
    }

    @After("execution(* com.example.demo.service.*.*(..))")
    public void after() {
        log.info("after");
    }

    @AfterReturning("execution(* com.example.demo.service.*.*(..))")
    public void afterReturning() {
        log.info("afterReturning");
    }

    @AfterThrowing("execution(* com.example.demo.service.*.*(..))")
    public void afterThrowing() {
        log.info("afterThrowing");
    }
}

运行结果如下:

在这里插入图片描述

高亮部分为成功执行的通知,会发现除了 @AfterThrowing 通知以外的其他通知都成功执行了,但如果在原始方法中添加一个 int i = 1/0; 的异常,结果将变为以下

在这里插入图片描述

结果显示 @AfterThrowing@Before@After@Around 的前置部分成功执行,但是由于原始方法中存在异常,@AfterReturning@Around 的后置部分并未执行

由于多个通知的切入点表达式可能重复,可将其抽取。声明一个返回值为 void 的无参空方法,在方法上添加 @Pointcut 注解并指定切入点表达式,其他地方通过类似方法调用的形式引用该表达式:

@Slf4j
@Component
@Aspect
public class TestAspect {

    @Pointcut("execution(* com.example.demo.service.*.*(..))")
    public void pt() {}

    @Before("pt()")
    public void before() {
        log.info("before");
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("around before");
        Object ret = joinPoint.proceed();
        log.info("around after");
        return ret;
    }

    @After("pt()")
    public void after() {
        log.info("after");
    }

    @AfterReturning("pt()")
    public void afterReturning() {
        log.info("afterReturning");
    }

    @AfterThrowing("pt()")
    public void afterThrowing() {
        log.info("afterThrowing");
    }
}

若方法为 private,仅能在当前切面类中引用;若要在其他切面类中引用,需将方法设为 public。

注意事项:

  • @Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around 环绕通知方法的返回值,必须指定为 Object 类型,来接收原始方法的返回值

通知顺序

通过前面的程序运行结果可以得知,在同一切面类中:

  • 若原始方法中没有出现异常,通知执行的顺序为:@Around 的前置部分 → \to @Before → \to @AfterReturning → \to @After → \to @Around 的后置部分
  • 若原始方法中出现异常,通知执行的顺序为:@Around 的前置部分 → \to @Before → \to @AfterThrowing → \to @After

接下来研究的是多个切面类中通知的执行顺序。

准备三个切面类 TestAspect1、TestAspect2、TestAspect3,每个类都有前置通知(@Before)和后置通知(@After),且切入点表达式相同:

// TestAspect1
public class TestAspect1 {
    @Before("execution(* com.example.demo.service.*.*(..))")
    public void before() {
        log.info("before");
    }

    @After("execution(* com.example.demo.service.*.*(..))")
    public void after() {
        log.info("after");
    }
}

// TestAspect2
public class TestAspect2 {
    @Before("execution(* com.example.demo.service.*.*(..))")
    public void before() {
        log.info("before");
    }

    @After("execution(* com.example.demo.service.*.*(..))")
    public void after() {
        log.info("after");
    }
}

// TestAspect3
public class TestAspect3 {
    @Before("execution(* com.example.demo.service.*.*(..))")
    public void before() {
        log.info("before");
    }

    @After("execution(* com.example.demo.service.*.*(..))")
    public void after() {
        log.info("after");
    }
}

程序运行结果如下:

在这里插入图片描述

前置通知执行顺序为 1、2、3,后置通知执行顺序为 3、2、1,这与切面类的类名字母排序有关,目标方法运行前的通知,类名排名越靠前越先执行;目标方法运行后的通知,类名排名越靠前越后执行。

可以在切面类上添加 @Order 注解,通过指定数字控制顺序。目标方法运行前的通知,数字越小越先执行;目标方法运行后的通知,数字越小越后执行。

// TestAspect1
@Order(2)
public class TestAspect1 {
    @Before("execution(* com.example.demo.service.*.*(..))")
    public void before() {
        log.info("before");
    }

    @After("execution(* com.example.demo.service.*.*(..))")
    public void after() {
        log.info("after");
    }
}

// TestAspect2
@Order(3)
public class TestAspect2 {
    @Before("execution(* com.example.demo.service.*.*(..))")
    public void before() {
        log.info("before");
    }

    @After("execution(* com.example.demo.service.*.*(..))")
    public void after() {
        log.info("after");
    }
}

// TestAspect3
@Order(1)
public class TestAspect3 {
    @Before("execution(* com.example.demo.service.*.*(..))")
    public void before() {
        log.info("before");
    }

    @After("execution(* com.example.demo.service.*.*(..))")
    public void after() {
        log.info("after");
    }
}

程序运行结果如下:

在这里插入图片描述

切入点表达式

切入点表达式用于决定项目中哪些目标方法应用定义的通知。

常见形式:

  • execution:根据方法签名匹配
  • annotation:根据注解匹配
execution

execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution (访问修饰符? 返回值 包名.类名.? 方法名(方法参数) throws 异常?)

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

  • 访问修饰符:可省略(public、protected、private)
  • 包名.类名:可省略
  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
execution(public * com.example.demo.service.impl.DeptServiceImpl.list())

也可以省略为:

execution(* list())

一般包名和类名不建议省略

可以使用通配符描述切入点:

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
    execution(* com.*.*.service.*.list*())
    
  • .. :多个连续的任意符号,可以通配任意层级的包或任意类型、任意个数的参数
    * com.example..service..*(..))
    

根据业务需要,可以使用 与(&&)或(||)非(!) 来组合比较复杂的切入点表达式

书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update 开头。
  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包。
annotation

@annotation 切入点表达式,用于匹配标识有特定注解的方法

@annotation(注解全类名)

首先自定义一个注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}

在方法上添加这个自定义注解:

@MyLog
@Override
public List<Dept> list() {return deptMapper.list();}

在切面类中使用这个注解:

@Before("@annotation(com.example.demo.aop.MyLog)")
public void before() {
  log.info("before");
}

程序运行结果如下:

在这里插入图片描述

两种切入点表达式的总结区别:execution 根据方法描述信息匹配,是常用方式;annotation 基于注解匹配,在方法名不规则或特殊需求时更灵活,虽需自定义注解但操作灵活。

连接点

连接点可简单理解为可以被 AOP 控制的方法,在 Spring AOP 中特指方法的执行,Spring 通过 JoinPoint 对其抽象,可通过该对象获取目标方法执行时的相关信息,如目标对象的类名、目标方法的方法名、参数信息等,并可在通知中通过 JoinPoint 获取这些信息。

@Around 通知需使用 ProceedJoinPoint 获取连接点信息,其他四种通知类型需使用 JoinPoint 获取,且 JoinPoint 是 ProceedJoinPoint 的父类型。

@Around 通知通过 ProceedJoinPoint 获取信息:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class TestAspect4 {
    @Pointcut("execution(* com.example.demo.service.*.*(..))")
    public void pt() {}

    @Before("pt()")
    public void before() {
        log.info("before");
    }
    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("around before");

        // 1.获取目标对象的类名
        String className = joinPoint.getTarget().getClass().getName();
        log.info("目标对象的类名:{}",className);

        // 2.获取目标方法的方法名
        String methodName = joinPoint.getSignature().getName();
        log.info("目标方法的方法名:{}",methodName);

        // 3.获取目标方法运行时传入的参数
        Object[] args = joinPoint.getArgs();
        log.info("目标方法运行时传入的参数:{}",args);

        // 4.放行目标方法执行
        Object ret = joinPoint.proceed();

        // 5.获取目标方法运行的返回值
        log.info("目标方法运行的返回值:{}",ret);

        log.info("around after");
        return ret;
    }
}

程序运行结果如下:

在这里插入图片描述

运行测试方法后,控制台输出了获取到的相关信息;前置通知无法获取返回值,因为其在原始方法运行前执行;环绕通知中若未将 result 返回,会导致原始方法执行结果丢失,且可在 AOP 中篡改目标方法执行结果。


网站公告

今日签到

点亮在社区的每一天
去签到