Java Web实战(五)Web后端之AOP-面向切面编程原理用法详解

文章目录

    • 1. 场景示例
      • Spring AOP快速入门:统计各个业务层方法执行耗时
    • 2. AOP核心概念
      • 2.1 通知类型
      • 2.2 PointCut
      • 2.3 通知顺序
      • 2.4 使用通知函数的参数
      • 2.5 示例
    • 3. 切入点表达式
      • 3.1 切入点表达式-execution
      • 3.2 切入点表达式-`@annotation`
    • 4. 案例

AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程。

1. 场景示例

案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时

如何实现? 一种策略是为为每个执行方法都添加获取执行耗时的逻辑. 缺点在于: 它需要更改原先的所有的业务代码, 同时获取执行耗时的代码逻辑相同, 造成了大量重复.

一种解决思想是: 使用动态代理, 也即在执行指定的部分代码时会在合适的节点转到代理逻辑中执行. 动态代理是面向切面编程最主流的实现。而 SpringAOP 是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。

Spring AOP快速入门:统计各个业务层方法执行耗时

  1. 先引入 aop 依赖

    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  2. 连包带类创建并编写AOP程序: 针对于特定方法根据业务需要进行编程. 通过使用 @Aspect 注解来表明这是一个切面类, 该类同时还是一个 java-bean.

@Component
@Aspect
public class TimeAspect {@Around("execution(* com.itheima.service.*.*(..))")public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long begin = System.currentTimeMillis();Object object = proceedingJoinPoint.proceed(); //调用原始方法运long end = System.currentTimeMillis();log.info(proceedingJoinPoint.getSignature()+"执行耗时: {}ms", end - begin);return object;}
}

2. AOP核心概念

连接点: JoinPoint, 可以被AOP控制的方法(暗含方法执行时的相关信息)
通知: Advice, 指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点: PointCut, 匹配连接点的条件,通知仅会在切入点方法执行时被应用
切面: Aspect, 描述通知与切入点的对应关系(通知+切入点)
目标对象: Target,通知所应用的对象

参考1中的程序, 其中 @Around("execution(* com.itheima.service.*.*(..))") 就是切入点, 引号内的表达式即为切入点表达式, 它规定了哪些类中的哪些方法需要被代理执行. @Around 注解是通知类型, 它指明了连接点在额外逻辑的执行位置, 这里是被环绕, 也即原方法执行前后均存在代理逻辑.

2.1 通知类型

通知类型
@Before(前置通知)
@After(后置通知)
@Around(环绕通知,重点)
@AfterReturning(返回后通知,了解)
@AfterThrowing(异常后通知,了解)

注解通知类型执行顺序
@Around环绕通知此注解标注的通知方法在目标方法前、后都被执行
@Before前置通知此注解标注的通知方法在目标方法前被执行
@After后置通知此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning返回后通知此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing异常后通知此注解标注的通知方法发生异常后执行
  • @Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。

2.2 PointCut

@PointCut: 该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。

@Pointcut("execution(* com.example.mytliasaop.controller.DeptController.getDeptById())")
public void allDeptCut(){}@Around("allDeptCut()")
public Object aroundNoticeFunA(ProceedingJoinPoint point) throws Throwable {// some code
}

该表达式也可以使用访问权限修饰符修饰:
private:仅能在当前切面类中引用该表达式
public:在其他外部的切面类中也可以引用该表达式

2.3 通知顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。

不同切面类中,默认按照切面类的类名字母排序:
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行

@Order(数字) 加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行

@Aspect
@Component
@Slf4j
@Order(3)
public class NoticeTypeAsp {
}

2.4 使用通知函数的参数

若有参数, 则只有一个, 也即连接点, 在Spring中用JoinPoint抽象表示连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  • 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint

  • 对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型

@Before("execution(* com.itheima.service.DeptService.*(..))")
public void before(JoinPoint joinPoint) {String className = joinPoint.getTarget().getClass().getName(); //获取目标类名Signature signature = joinPoint.getSignature(); //获取目标方法签名String methodName = joinPoint.getSignature().getName(); //获取目标方法名Object[] args = joinPoint.getArgs(); //获取目标方法运行参数 
}

2.5 示例

编写如下示例程序:

@Aspect
@Component
@Slf4j
public class NoticeTypeAsp {@Pointcut("execution(* com.example.mytliasaop.controller.DeptController.getDeptById(..))")public void allDeptCut(){}@Around("allDeptCut()")public Object aroundNoticeFunA(ProceedingJoinPoint point) throws Throwable {log.info("Around Notice: enter ... ...");Object res = point.proceed();log.info("Around Notice: after point.proceed(), before return ... ...");return res;}@Before("allDeptCut()")public void beforeNotice() {log.info("Before Notice: execute ... ...");}@After("allDeptCut()")public void afterNotice() {log.info("After Notice: execute ... ...");}@AfterReturning("allDeptCut()")public void afterReturnNotice(){log.info("After returning Notice: execute ... ...");}@AfterThrowing("allDeptCut()")public void afterThrowNotice() {log.info("After Throw Notice: enter ... ...");}@Before("allDeptCut()")public void beforeWithArgs(JoinPoint point){log.info("Before Notice with join-point args: enter ... ...");log.info("Before Notice with join-point args: 方法名 {}", point.getSignature());log.info("Before Notice with join-point args: 方法参数 {}}", point.getArgs());log.info("Before Notice with join-point args: 类名 {}", point.getTarget().getClass().getName());}
}

getDeptById() 方法正常执行完毕哪些通知会被执行? 出现异常时哪些通知会被执行?

example.mytliasaop.aop.NoticeTypeAsp   : Around Notice: enter ... ...
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice: execute ... ...
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice with join-point args: enter ... ...
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice with join-point args: 方法名 Result com.example.mytliasaop.controller.DeptController.getDeptById(Integer)
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice with join-point args: 方法参数 2}
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice with join-point args: 类名 com.example.mytliasaop.controller.DeptController
e.m.controller.DeptController          : 依据id获取部门信息: 2
... ... ...
example.mytliasaop.aop.NoticeTypeAsp   : After returning Notice: execute ... ...
example.mytliasaop.aop.NoticeTypeAsp   : After Notice: execute ... ...
example.mytliasaop.aop.NoticeTypeAsp   : Around Notice: after point.proceed(), before return ... ...

异常时:

example.mytliasaop.aop.NoticeTypeAsp   : Around Notice: enter ... ...
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice: execute ... ...
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice with join-point args: enter ... ...
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice with join-point args: 方法名 Result com.example.mytliasaop.controller.DeptController.getDeptById(Integer)
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice with join-point args: 方法参数 4}
example.mytliasaop.aop.NoticeTypeAsp   : Before Notice with join-point args: 类名 com.example.mytliasaop.controller.DeptController
e.m.controller.DeptController          : 依据id获取部门信息: 4
example.mytliasaop.aop.NoticeTypeAsp   : After Throw Notice: enter ... ...
example.mytliasaop.aop.NoticeTypeAsp   : After Notice: execute ... ...

3. 切入点表达式

切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
execution(...):根据方法的签名来匹配
@annotation(...) :根据注解匹配

3.1 切入点表达式-execution

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

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

其中带 ? 的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

可以使用通配符描述切入点
*: 单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分.
execution(* com.*.service.*.update*(*))

..: 多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
execution(* com.itheima..DeptService.*(..))

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

切入点表达式-execution书写建议:

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头。

  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。

  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用.., 使用 * 匹配单个包。

3.2 切入点表达式-@annotation

@annotation(com.itheima.anno.Log)
通过注解匹配
先编写一个注解: 只起到标记作用

package com.example.mytliasaop.ana;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 MyAnalog {
}

这表示这是一个用于方法上的在运行时起作用的注解.
然后使用它

@Around("@annotation(com.example.mytliasaop.ana.MyAnalog)")
public Object around(ProceedingJoinPoint pt) throws Throwable {// some code
}

4. 案例

将案例中 增、删、改 相关接口的操作日志记录到数据库表中。
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

准备:

  • 在案例工程中引入AOP的起步依赖
  • 导入资料中准备好的数据库表结构,并引入对应的实体类
  • 编码:
    • 自定义注解 @Log
    • 定义切面类,完成记录操作日志的逻辑
    • 获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id。
@Aspect
@Component
@Slf4j
public class LoggerAspect {@AutowiredHttpServletRequest request;@Autowiredprivate OperateLogMapper mp;@Around("@annotation(com.example.mytliasaop.ana.MyAnalog)")public Object logRecord(ProceedingJoinPoint pt) throws Throwable{log.info("进入logRecord代理");// 1. 获取登录者的信息String jwt = request.getHeader("token");Claims claims = JWTUtils.parse(jwt);Integer operateUser = Integer.parseInt((String) claims.get("id"));// 2. 操作时间LocalDateTime operateTime = LocalDateTime.now();// 3. 操作类名String className = pt.getTarget().getClass().getName();// 4. 方法名String methodName = pt.getSignature().getName();// 5. 方法参数String methodParams = Arrays.toString(pt.getArgs());// 6. 返回值long st = System.currentTimeMillis();Object res = pt.proceed();String returnValue = JSONObject.toJSONString(res);// 7. 操作耗时Long costTime = System.currentTimeMillis() - st;OperateLog operateLog = new OperateLog(null, operateUser, operateTime, className,methodName, methodParams, returnValue, costTime);log.info("{}", operateLog);// 插入 log 记录mp.insertLog(operateLog);return res;}
}

注意, 其中的 request 对象是在 spring web 项目启动时已经由 java-bean 进行管理
此外, 关于 MyAnalog 的位置, 可以放到 controller 层的各个已实现的方法(新增修改删除)上.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/743342.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数学实验_Matlab使用2_简单绘图

简单使用 x -pi * 2 : .1 : pi*2;y sin(x);plot(x, y); % 绘制普通图像plot(x, y, k-.*); % 绘制2维图像&#xff0c;线为实线&#xff0c;*为每个点&#xff08;Matlab的画图比较原始&#xff0c;就是简单的秒点画图&#xff09;grid on; % 打开网状格式% grid off; % 关闭…

【自动驾驶中的BEV算法】

自动驾驶中的BEV算法 在自动驾驶领域中&#xff0c;Bird’s Eye View (BEV) 算法是一种将来自不同传感器&#xff08;如摄像头、激光雷达、毫米波雷达等&#xff09;的数据转换为车辆正上方俯瞰视角下的统一表示的方法。这种转换使得车辆能够获得一个直观且具有空间一致性的环…

6. git 指定某一个提交

在 Git 中&#xff0c;直接“更新”一个特定的提交记录是不常见的操作&#xff0c;因为提交记录是 Git 仓库历史的一部分&#xff0c;通常不应该被修改。 但是&#xff0c;你可以通过一些策略来达到类似的效果&#xff0c;比如创建一个新的提交来撤销或者修改之前的提交。 以下…

SORA和大语言模型的区别

OpenAI的文生视频模型SORA与大语言模型&#xff08;LLM&#xff09;的主要区别在于它们的应用领域和处理的数据类型&#xff0c;数据处理能力、技术架构、多模态能力和创新点。SORA作为一款专注于视频生成的模型&#xff0c;展现了在处理视觉数据方面的独特优势和创新能力。 1…

R语言读取大型NetCDF文件

失踪人口回归&#xff0c;本篇来介绍下R语言读取大型NetCDF文件的一些实践。 1 NetCDF数据简介 先给一段Wiki上关于NetCDF的定义。 NetCDF (Network Common Data Form) is a set of software libraries and self-describing, machine-independent data formats that support…

GlobalExceptionHandler全局异常处理器的设计

在Java Web开发中&#xff0c;全局异常处理器&#xff08;GlobalExceptionHandler&#xff09;是一个非常重要的概念。它允许我们集中处理应用程序中可能发生的各种异常&#xff0c;从而提供统一的错误响应&#xff0c;增强用户体验&#xff0c;并简化异常处理逻辑。下面将详细…

栈队列数组试题(二)——队列

一、单项选择题 01.栈和队列的主要区别在于&#xff08;). A.它们的逻辑结构不一样 B.它们的存储结构不一样 C.所包含的元素不一样 D.插入、删除操作的限定不一样 02&#xff0e;队列的“先进先出…

STM32串口通信—串口的接收和发送详解

目录 前言&#xff1a; STM32串口通信基础知识&#xff1a; 1&#xff0c;STM32里的串口通信 2&#xff0c;串口的发送和接收 串口发送&#xff1a; 串口接收&#xff1a; 串口在STM32中的配置&#xff1a; 1. RCC开启USART、串口TX/RX所对应的GPIO口 2. 初始化GPIO口 …

Linux mktemp命令教程:创建临时文件和目录(附实例详解和注意事项)

Linux mktemp命令介绍 mktemp命令在Linux中用于创建临时文件或目录。这个命令可以帮助我们在脚本或应用程序中创建一个有效且唯一的临时文件或目录。 Linux mktemp命令适用的Linux版本 mktemp命令在所有主要的Linux发行版中都可以使用&#xff0c;包括Debian、Ubuntu、Alpin…

鸿蒙跨包跳转页面-HSP页面路由

页面路由跳转 若开发者想在entry模块中&#xff0c;添加一个按钮跳转至library模块中的menu页面&#xff08;路径为&#xff1a;library/src/main/ets/pages/menu.ets&#xff09;&#xff0c;那么可以在使用方的代码&#xff08;entry模块下的Index.ets&#xff0c;路径为&am…

YOLOv8改进 | 图像去雾 | 特征融合注意网络FFA-Net增强YOLOv8对于模糊图片检测能力(北大和北航联合提出)

一、本文介绍 本文给大家带来的改进机制是由北大和北航联合提出的FFA-net: Feature Fusion Attention Network for Single Image Dehazing图像增强去雾网络,该网络的主要思想是利用特征融合注意力网络(Feature Fusion Attention Network)直接恢复无雾图像,FFA-Net通过特征…

MyBatis-Plus学习记录

目录 MyBatis-Plus快速入门 简介 快速入门 MyBatis-Plus核心功能 基于Mapper接口 CRUD 对比mybatis和mybatis-plus&#xff1a; CRUD方法介绍&#xff1a; 基于Service接口 CRUD 对比Mapper接口CRUD区别&#xff1a; 为什么要加强service层&#xff1a; 使用方式 CR…

开发指南009-从list导出excel文件

从数据库返回一般是对象的列表&#xff0c;平台底层提供了从list转为excel文件的方法。平台的设计思想就是为一些典型的场景设计对应的解决方法&#xff0c;通过模式化的方法来简化编程和提高维护性&#xff08;通过标准化来减少学习成本和维护成本&#xff0c;张三做的东西和李…

ARC 的 retainCount 是怎么存储的

ARC的retainCount是存吃在64张哈希表中的&#xff0c;根据哈希算法去查找所在的位置&#xff0c;无需便利 散列表&#xff08;引用计数表、weak表&#xff09; SideTables 表在 非嵌入式的64位系统中&#xff0c;有 64张 SideTable 表每一张 SideTable 主要是由三部分组成。自…

【MASM汇编语言快速入门】MASM常用伪指令速查表——变量

MASM伪指令速查表–变量 初学MASM时&#xff0c; 常常看不懂db, dup(?)等汇编指令的含义&#xff0c; 教材中也缺乏系统的解释。与机器指令不同&#xff0c;这些指令叫伪指令&#xff0c; 在编译&#xff08;汇编&#xff09;的时候被MASM编译器处理&#xff0c; 而在运行时计…

Boot——组件(导航和选项卡、分页、卡片、轮播图)

Boot——组件&#xff08;下&#xff09; 导航和选项卡 https://v5.bootcss.com/docs/components/navs-tabs/ &#xff08;1&#xff09;导航 <ul class"nav"><li class"nav-item"><a href"#" class"nav-link">…

游戏数据处理

游戏行业关键数据指标 ~ 总激活码发放量、总激活量、总登录账号数 激活率、激活登录率 激活率 激活量 / 安装量 激活率 激活量 / 激活码发放量 激活且登录率 激活且登录量 / 激活码激活量 激活且登录率应用场景 激活且登录率是非常常用的转化率指标之一&#xff0c;广泛…

Ypay源支付6.9无授权聚合免签系统可运营源码

YPay是一款专为个人站长设计的聚合免签系统&#xff0c;YPay基于高性能的ThinkPHP 6.1.2 Layui PearAdmin架构&#xff0c;提供了实时监控和管理的功能&#xff0c;让您随时随地掌握系统运营情况。 说明 Ypay源支付6.9无授权聚合免签系统可运营源码 已搭建测试无加密版本…

HTML5:七天学会基础动画网页13

看完前面很多人可能还不是很明白0%-100%那到底是怎么回事&#xff0c;到底该怎么用&#xff0c;这里我们做一个普遍的练习——心跳动画 想让心❤跳起来&#xff0c;我们先分析一波&#xff0c;这个心怎么写&#xff0c;我们先写一个正方形&#xff0c;再令一个圆形前移: 再来一…

蓝桥杯历年真题 省赛 Java b组 2016年第七届

一、题目 分小组 9名运动员参加比赛&#xff0c;需要分3组进行预赛。 有哪些分组的方案呢&#xff1f; 我们标记运动员为 A,B,C,... I 下面的程序列出了所有的分组方法。 该程序的正常输出为&#xff1a; ABC DEF GHI ABC DEG FHI ABC DEH FGI ABC DEI FGH ABC DFG EHI ABC…