当前位置: 首页 > news >正文

【Java Web 快速入门】十、AOP

目录

  • AOP
    • AOP 基础
      • 概述
      • 快速入门
      • 核心概念
    • AOP 进阶
      • 通知类型
      • 通知顺序
      • 切入点表达式
        • execution
        • annotation
      • 连接点

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

http://www.lryc.cn/news/621951.html

相关文章:

  • 「 CentOS7 安装部署k8s」
  • 水环境遥感分析!R语言编程+多源遥感数据预处理;水体指数计算、水深回归分析、水温SVM预测、水质神经网络建模及科研级可视化制图
  • 关于simplifyweibo_4_moods数据集的分类问题
  • 云原生俱乐部-k8s知识点归纳(3)
  • 2025年中国AI算力基础设施发展趋势洞察
  • MySQL 全面指南:从入门到精通——深入解析安装、配置、操作与优化
  • Linux 进程、线程与 exec/系统调用详解
  • 力扣top100(day04-06)--贪心算法
  • 自动处理考勤表——如何使用Power Query,步步为营,一点点探索自定义函数
  • 陪伴,是挫折教育最暖的底色
  • Java 中使用阿里云日志服务(SLS)完整指南
  • Hologres实战:路径分析函数
  • 【开发语言】Groovy语言:Java生态中的动态力量
  • 1.2. qemu命令起虚拟机增加网络配置
  • [git] 当GitHub宕机时,我们如何协作?| github同步gitee的部署方法
  • uniApp App 端日志本地存储方案:实现可靠的日志记录功能
  • Flutter 自定义组件开发指南
  • Wi-Fi 与蜂窝网络(手机网络)的核心区别,以及 Wi-Fi 技术未来的发展方向
  • css变量的妙用(setProperty()的使用)
  • MySQL的学习笔记
  • 前端性能优化工具Performance面板实战指南
  • w484扶贫助农系统设计与实现
  • Android项目中Ktor的引入与使用实践
  • @[TOC](计算机是如何⼯作的) JavaEE==网站开发
  • 从理论到实战:KNN 算法与鸢尾花分类全解析
  • Python基础(Flask①)
  • Sklearn 机器学习 手写数字识别 使用K近邻算法做分类
  • DAY41打卡
  • IO多路复用底层原理
  • TDengine IDMP 高级功能(1. 元素模板)