【Java Web 快速入门】十、AOP

目录

AOP

AOP 基础

概述

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

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

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

快速入门

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

导入依赖:

xml 复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

入门程序代码:

java 复制代码
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:异常后通知,此注解标注的通知方法发生异常后执行

以下是测试代码:

java 复制代码
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 注解并指定切入点表达式,其他地方通过类似方法调用的形式引用该表达式:

java 复制代码
@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),且切入点表达式相同:

java 复制代码
// 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 注解,通过指定数字控制顺序。目标方法运行前的通知,数字越小越先执行;目标方法运行后的通知,数字越小越后执行。

java 复制代码
// 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 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

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

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

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

也可以省略为:

java 复制代码
execution(* list())

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

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

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

    java 复制代码
    execution(* com.*.*.service.*.list*())
  • .. :多个连续的任意符号,可以通配任意层级的包或任意类型、任意个数的参数

    java 复制代码
    * com.example..service..*(..))

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

书写建议:

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

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

java 复制代码
@annotation(注解全类名)

首先自定义一个注解:

java 复制代码
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 {
}

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

java 复制代码
@MyLog
@Override
public List<Dept> list() {return deptMapper.list();}

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

java 复制代码
@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 获取信息:

java 复制代码
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 中篡改目标方法执行结果。

相关推荐
侠客行03174 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪4 小时前
深入浅出LangChain4J
java·langchain·llm
子兮曰4 小时前
OpenClaw入门:从零开始搭建你的私有化AI助手
前端·架构·github
吴仰晖4 小时前
使用github copliot chat的源码学习之Chromium Compositor
前端
1024小神4 小时前
github发布pages的几种状态记录
前端
老毛肚6 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎6 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
不像程序员的程序媛6 小时前
Nginx日志切分
服务器·前端·nginx
Yvonne爱编码6 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚6 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言