SpringBoot(六)--- AOP、ThreadLocal

article/2025/7/13 16:52:57

目录

前言

一、AOP基础

1.入门程序

2. AOP核心概念

3. 底层原理

二、AOP进阶

1.通知类型

抽取切入点

2. 切入点表达式

2.1 execution

2.2 @annoation

2.3 连接点详解

三、ThreadLocal


前言

AOP(面向切面编程),面向切面编程实际就是面向特定方法编程

假如有一个项目,现在想知道这个项目中每个业务功能执行的市场,以便对执行时长比较长的功能进行优化。

首先想到的应该就是在每个方法执行前加一个System.currentTimeMillis()方法来记录时间,执行完后,再次记录时间。

但是如果业务功能很多,这样操作未免太过于冗余。利用AOP,只需要单独定义一段代码,就可以计算出所有业务功能所执行的时间。

所以,AOP的优势主要体现在以下四个方面:

  • 减少重复代码:不需要在业务方法中定义大量的重复性的代码,只需要将重复性的代码抽取到AOP程序中即可。

  • 代码无侵入:在基于AOP实现这些业务功能时,对原有的业务代码是没有任何侵入的,不需要修改任何的业务代码。

  • 提高开发效率

  • 维护方便

一、AOP基础

1.入门程序

@Component
@Aspect //当前类为切面类
@Slf4j
public class RecordTimeAspect {// Around注解中的属性表示会拦截com.itheima.service.impl这个包下DeptServiceImpl类中的所有方法@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {// 记录方法执行开始时间long begin = System.currentTimeMillis();// 执行原始方法// 由于Around表示方法执行前后都会执行,所以被拦截的方法执行结束后,就会执行下面的代码。Object result = pjp.proceed();//记录方法执行结束时间long end = System.currentTimeMillis();//计算方法执行耗时log .info("方法执行耗时: {}毫秒",end-begin);return result;}
}

通过AOP入门程序完成了业务方法执行耗时的统计,那其实AOP的功能远不止于此,常见的应用场景如下:

  • 记录系统的操作日志

  • 权限控制

  • 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务。

2. AOP核心概念

连接点:JoinPoint,可以被AOP控制的方法(不仅仅指当前被AOP控制的方法,也包含当前没有被AOP控制,但是可以被控制的方法)。

通知:Advice,指那些重复逻辑,也就是共性功能(最终体现为一个方法),例如计算每个业务执行的时间。

切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用,可以简单理解为被AOP控制的方法。

切面:Aspect,描述通知与切入点的对应关系(通知+切入点)。被@Aspect直接所修饰的类被称为切面类。

目标对象:Target,通知所应用的对象,目标对象指的就是通知所应用的对象,称之为目标对象。

3. 底层原理

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

如下图所示,切面类在执行原方法的时候,并不会直接去调用目标对象中的方法。而是创建一个代理对象DeptServiceProxy继承DeptService,在代理对象中执行目标对象的方法。在前端发起请求的时候,Spring通过IOC容器注入的也是这个代理对象。

就像把目标方法看作“核心业务”(制作手机),动态代理就是自动套在业务外的“流水线外壳”(代理商),在制作手机前自动贴标签(前置日志),制作后自动打包(后置日志),而手机工厂无需修改任何代码。如下图,在执行方法之前,计算时间;执行结束之后,计算时间,而Controller层执行代理商的代码即可。

二、AOP进阶

1.通知类型

AOP的通知类型有以下几种:

现有如下代码,通过代码来直观地感受不同通知类型的执行顺序:

@Slf4j
@Component
@Aspect
public class MyAspect1 {//前置通知@Before("execution(* com.itheima.service.*.*(..))")public void before(JoinPoint joinPoint){log.info("before ...");}//环绕通知@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 ...");}
}

随便执行一个业务程序,在程序没有发生异常的情况下,@AfterThrowing标识的通知方法不会执行。

可以发现Around先执行,然后Before执行,因为这两个都是在原始方法执行之前执行的。原始方法执行完毕之后,AfterReturning执行,接着是After,最后是Around。因为@Around是环绕通知,在原始方法执行前后都会执行。

如果程序出现异常,结果如下,@Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了):

抽取切入点

在上面的代码中,可以发现每一个通知都有相同的切入点表达式。假如此时切入点表达式需要变动,就需要将所有的切入点表达式一个一个的来改动,就变得非常繁琐了。因此我们需要将相同的切入点表达式抽取出来。

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

将切入点表达式抽取出来,在通知中就直接引入切入点函数名即可。

@Slf4j
@Component
@Aspect
public class MyAspect1 {//切入点方法(公共的切入点表达式)@Pointcut("execution(* com.itheima.service.*.*(..))")private void pt(){}//前置通知(引用切入点)@Before("pt()")public void before(JoinPoint joinPoint){log.info("before ...");}//环绕通知@Around("pt()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {log.info("around before ...");//调用目标对象的原始方法执行Object result = proceedingJoinPoint.proceed();//原始方法在执行时:发生异常//后续代码不在执行log.info("around after ...");return result;}//后置通知@After("pt()")public void after(JoinPoint joinPoint){log.info("after ...");}//返回后通知(程序在正常执行的情况下,会执行的后置通知)@AfterReturning("pt()")public void afterReturning(JoinPoint joinPoint){log.info("afterReturning ...");}//异常通知(程序在出现异常的情况下,执行的后置通知)@AfterThrowing("pt()")public void afterThrowing(JoinPoint joinPoint){log.info("afterThrowing ...");}
}

如果外部其他切面类也想使用这个切入点,就需要把private改为public,而在引用的时候,具体的语法为:

@Slf4j
@Component
@Aspect
public class MyAspect2 {//引用MyAspect1切面类中的切入点表达式//必须是切入点的全类名@Before("com.itheima.aspect.MyAspect1.pt()")public void before(){log.info("MyAspect2 -> before ...");}
}

2. 切入点表达式

切入点表达式分为两种:execution(……):根据方法的签名来匹配;@annotation(……) :根据注解匹配。

2.1 execution

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

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

其中带?号的部分是可以省略的。

主要理解下面这个表达式即可:

第一个*号指的是任意的访问修饰符,com.itheima.service.impl指的是包名,DeptServiceImpl指的是这个包中的一个类,类名是DeptServiceImpl,delete是这个类中的一个方法,后面的*号指的是delete方法中的所有参数。

execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))

如果想要com.itheima.service.impl包下的所有类,可以改为如下代码:

execution(* com.itheima.service.impl.DeptServiceImpl.*(..))

注意事项:

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

execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))

2.2 @annoation

使用execution,当方法过多的时候,代码就会很繁杂。此时就可以借助另一种切入点表达式 @annotation 来描述这一类的切入点,从而来简化切入点表达式的书写。

实现步骤:

  1. 编写自定义注解

  2. 在业务类要做为连接点的方法上添加自定义注解

自定义注解LogOperation

// Target注解指定注解的作用目标 
// Retention指定注解的生命周期(在运行时有效)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}

业务类DeptServiceImpl

@Slf4j
@Service
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Override@LogOperation //自定义注解(表示:当前方法属于目标方法)public List<Dept> list() {List<Dept> deptList = deptMapper.list();//模拟异常//int num = 10/0;return deptList;}@Override@LogOperation //自定义注解(表示:当前方法属于目标方法)public void delete(Integer id) {//1. 删除部门deptMapper.delete(id);}@Override   // 没有自定义注解,表示当前方法不属于目标方法public void save(Dept dept) {dept.setCreateTime(LocalDateTime.now());dept.setUpdateTime(LocalDateTime.now());deptMapper.save(dept);}}

切面类

@Slf4j
@Component
@Aspect
public class MyAspect6 {//针对list方法、delete方法进行前置通知和后置通知//前置通知//括号里是自定义注解的全类名@Before("@annotation(com.itheima.anno.LogOperation)")public void before(){log.info("MyAspect6 -> before ...");}//后置通知@After("@annotation(com.itheima.anno.LogOperation)")public void after(){log.info("MyAspect6 -> after ...");}
}

上述就是@annoation的基本用法。

总结:

  • execution切入点表达式

    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式

    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐

  • annotation 切入点表达式

    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

2.3 连接点详解

在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

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

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

三、ThreadLocal

当在做业务代码的时候,存在以下问题:

  • 员工登录成功后,哪里存储的有当前登录员工的信息? 给客户端浏览器下发的jwt令牌中

  • 如何从JWT令牌中获取当前登录用户的信息呢? 获取请求头中传递的jwt令牌,并解析

  • TokenFilter 中已经解析了令牌的信息,如何传递给AOP程序、Controller、Service呢?ThreadLocal

ThreadLocal 是 Java 中用于实现线程局部变量的核心类,它通过为每个线程创建独立的变量副本,解决多线程环境下共享变量的并发问题。ThreadLocal 并不是一个Thread,而是Thread的局部变量,为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰。

那么如何操作ThreadLocal呢?

首先定义一个ThreadLocal操作的工具类,用于操作当前登录员工ID。

package com.itheima.utils;public class CurrentHolder {private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();public static void setCurrentId(Integer employeeId) {CURRENT_LOCAL.set(employeeId);}public static Integer getCurrentId() {return CURRENT_LOCAL.get();}public static void remove() {CURRENT_LOCAL.remove();}
}

然后我们在解析JWT令牌的时候,将登入员工的id存入ThreadLocal中,

        //5. 如果token不为空, 调用JWtUtils工具类的方法解析token, 如果解析失败, 响应401状态码try {Claims claims = JwtUtils.parseJWT(token);Integer empId = Integer.valueOf(claims.get("id").toString());// 在这里,将用户ID存入线程空间中CurrentHolder.setCurrentId(empId);log.info("token解析成功, 放行");} catch (Exception e) {log.info("token解析失败, 响应401状态码");response.setStatus(401);return;}

然后我们就可以在任何地方取出这次线程执行的员工ID

    // 示例方法,获取当前用户IDprivate int getCurrentUserId() {return CurrentHolder.getCurrentId();}

在同一个线程/同一个请求中,进行数据共享就可以使用 ThreadLocal


http://www.hkcw.cn/article/sqklyDMCsS.shtml

相关文章

贪心算法应用:在线租赁问题详解

贪心算法应用&#xff1a;在线租赁问题详解 贪心算法是一种在每一步选择中都采取当前状态下最优的选择&#xff0c;从而希望导致结果是全局最优的算法策略。在线租赁问题(Greedy Algorithm for Online Rentals)是一个经典的贪心算法应用场景&#xff0c;下面我将从多个维度全面…

BA-SAM: 用于 Segment Anything 模型的可扩展偏置模式注意力掩码

概要 在本文中&#xff0c;我们解决了 Segment Anything Model &#xff08;SAM&#xff09; 的图像分辨率变化挑战。SAM 以其零样本泛化性而闻名&#xff0c;当面对具有不同图像大小的数据集时&#xff0c;性能会下降。以前的方法倾向于将图像大小调整为固定大小或采用结构修改…

centos8修改IP地址和Hostname

修改ip地址 vim /etc/sysconfig/network-scripts/ifcfg-ens33 BOOTPROTO&#xff1a;设置为 static 表示使用静态 IP 地址。 IPADDR&#xff1a;设置新的 IP 地址。 NETMASK&#xff1a;设置子网掩码。 GATEWAY&#xff1a;设置默认网关&#xff08;可选&#xff0c;但通常需要…

Python Day40 学习(复习学习日志Day5-7)

重新对信贷数据集进行了填补空缺值的操作 自己写的时候&#xff0c;还是出现了问题&#xff1a; 首先是忘记了要定义一下data, 通过data pd.read_csv(data.csv)可以将读取到的数据保存到变量data中&#xff0c;方便后续进行数据分析。 其次&#xff0c;是漏掉了 c data.col…

QML 粒子系统之Affector

目录 基本示例AffectorAge - 改变特定年龄粒子的属性Attractor - 吸引粒子到指定点Friction - 施加摩擦力Gravity - 模拟重力Wander - 随机游走效果Turbulence - 添加湍流效果 下载链接 接上篇QML 粒子系统 (雪花飘落、爆炸粒子效果)&#xff0c;本文继续研究粒子系统中的附属效…

Mac 同时录制系统声音和麦克风声音(OBS + BlackHole 免费版)

&#x1f3ac; 一、你将实现的目标 ✅ 用 OBS 免费录制屏幕时&#xff0c;能同时录到&#xff1a; &#x1f5a5; 系统播放的声音&#xff08;比如视频、PPT音效、背景音乐&#xff09; &#x1f399; 你的麦克风说话声音&#xff08;讲解或旁白&#xff09; &#x1f9f0;…

Pytorch知识点2

Pytorch知识点 1、官方教程2、张量&#x1f9f1; 0、数组概念&#x1f9f1; 1. 创建张量&#x1f4d0; 2. 张量形状与维度&#x1f522; 3. 张量数据类型➗ 4. 张量的数学与逻辑操作&#x1f504; 5. 张量的就地操作&#x1f4e6; 6. 复制张量&#x1f680; 7. 将张量移动到加速…

使用pandas实现合并具有共同列的两个EXCEL表

表1&#xff1a; 表2&#xff1a; 表1和表2&#xff0c;有共同的列“名称”&#xff0c;而且&#xff0c;表1的内容&#xff08;行数&#xff09;<表2的行数。 目的&#xff0c;根据“名称”列的对应内容&#xff0c;将表2列中的“所处行业”填写到表1相应的位置。 实现代…

【农资进销存专用软件】佳易王农资进出库管理系统:赋能农业企业高效数字化管理

一、软件概述与核心优势 &#xff08;一&#xff09;试用版获取方式 资源下载路径&#xff1a;进入博主头像主页第一篇文章末尾&#xff0c;点击卡片按钮&#xff1b;或访问左上角博客主页&#xff0c;通过右侧按钮获取详细资料。 说明&#xff1a;下载文件为压缩包&#xff…

深入理解AMBA总线(七)AHB设计要点和AHB2APB同步桥设计前言

** 深入理解AMBA总线&#xff08;七&#xff09;AHB设计要点和AHB2APB同步桥设计前言 ** 前面的几篇文章介绍了AHB-lite协议。主要内容其实就是文档的介绍加上我自己的一些理解&#xff0c;希望对大家有帮助。今天这篇文章将带来AHB设计需要注意的一些事项&#xff0c;然后带…

消除F/1噪声

目录 简介 如何测量及规定1/f噪声&#xff1f; 1/f噪声对电路有何影响&#xff1f; 如何消除或降低1/f噪声&#xff1f; 简介 本文阐释1/f噪声是什么&#xff0c;以及在精密测量应用中如何降低或消除该噪声。1/f噪声无法被滤除&#xff0c;在精密测量应用中它可能是妨碍实…

洛谷-P3912素数个数题解

P3912 素数个数 题目描述 求 1 , 2 , ⋯ , N 1,2,\cdots,N 1,2,⋯,N 中素数的个数。 输入格式 一行一个整数 N N N。 输出格式 一行一个整数&#xff0c;表示素数的个数。 输入输出样例 #1 输入 #1 10输出 #1 4说明/提示 对于 40 % 40\% 40% 的数据&#xff0c; …

【环境搭建】Java、Python、Nodejs等开发环境搭建

1. 前言 趁着 618 活动&#xff0c;我新换了一台电脑。开发的同学都知道&#xff0c;重新在新电脑搭建开发环境是一件相对繁琐的事&#xff0c;这篇文章我将介绍如何搭建Java&#xff08;jdk、maven等&#xff09;、Python&#xff08;uv、conda等&#xff09;、Nodejs、Docke…

【机器学习基础】机器学习入门核心算法:层次聚类算法(AGNES算法和 DIANA算法)

机器学习入门核心算法&#xff1a;层次聚类算法&#xff08;AGNES算法和 DIANA算法&#xff09; 一、算法逻辑二、算法原理与数学推导1. 距离度量2. 簇间距离计算&#xff08;连接标准&#xff09;3. 算法伪代码&#xff08;凝聚式&#xff09; 三、模型评估1. 内部评估指标2. …

设计模式——迭代器设计模式(行为型)

摘要 本文详细介绍了迭代器设计模式&#xff0c;这是一种行为型设计模式&#xff0c;用于顺序访问集合对象中的元素&#xff0c;同时隐藏集合的内部结构。文章首先定义了迭代器设计模式并阐述了其核心角色&#xff0c;包括迭代器接口、具体迭代器、容器接口和具体容器。接着&a…

【文献阅读】Learning Transferable Visual Models From Natural Language Supervision

摘要 最先进的计算机视觉系统经过训练&#xff0c;可预测一组固定的预先确定的对象类别。这种受限的监督形式限制了它们的通用性和可用性&#xff0c;因为指定任何其他视觉概念都需要额外的标记数据。 直接从关于图像的原始文本中学习是一种很有前途的替代方法&#xff0c;它…

字符函数和字符串函数

目录 1.字符分类函数 2.字符转换函数 3.strlen函数的使用和模拟实现 4.strcpy函数的使用和模拟实现 5.strcat函数的使用和模拟实现 6.strcmp函数的使用和模拟实现 7.strcnpy函数的使用和模拟实现 8.strcnat函数的使用和模拟实现 9.strncmp函数的使用 10.strstr的函数使…

苹果电脑深度清理,让老旧Mac重焕新生

在日常使用苹果电脑的过程中&#xff0c;随着时间推移&#xff0c;系统内会积累大量冗余数据&#xff0c;导致电脑运行速度变慢、磁盘空间紧张。想要让设备恢复流畅&#xff0c;苹果电脑深度清理必不可少。那么&#xff0c;如何进行苹果电脑深度清理呢&#xff1f;接下来为你详…

android binder(1)基本原理

一、IPC 进程间通信&#xff08;IPC&#xff0c;Inter-Process Communication&#xff09;机制&#xff0c;用于解决不同进程间的数据交互问题。 不同进程之间用户地址空间的变量和函数是不能相互访问的&#xff0c;但是不同进程的内核地址空间是相同和共享的&#xff0c;我们可…

2025年ESWA SCI1区TOP,改进成吉思汗鲨鱼算法MGKSO+肝癌疾病预测,深度解析+性能实测

1.摘要 本文针对肝癌&#xff08;HCC&#xff09;早期诊断难题&#xff0c;提出了一种基于改进成吉思汗鲨鱼优化算法&#xff08;MGKSO&#xff09;的计算机辅助诊断系统。由于HCC在早期症状不明显且涉及高维复杂数据&#xff0c;传统机器学习方法易受噪声和冗余特征干扰。为提…