SpringBoot(六)--- AOP、ThreadLocal

目录

前言

一、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.pswp.cn/bicheng/83407.shtml

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

相关文章

【深度学习】 19. 生成模型:Diffusion Models

Diffusion Models Diffusion Models 简介 Diffusion 模型是一类通过逐步添加噪声并再逆向还原的方式进行图像生成的深度生成模型。其基本流程包括&#xff1a; 前向过程&#xff08;Forward Process&#xff09;&#xff1a;将真实图像逐步加噪&#xff0c;最终变为高斯噪声…

Y1——链式前向星

知识点 模版——链表的前插法 head表示头结点的下标 ver[i]表示结点i 的值 tot存储当前已经用到了哪个 add用于将x插到头结点 int head1; intt ver[N],Next[N]; int ttot-1; void add(int x){ver[tot]x;Next[tot]head;headtot; } 常见的链式前向星三种实现形式&#xff…

如何排查Redis单个Key命中率骤降?

问题现象 Redis整体命中率98%&#xff0c;但监控发现特定Key&#xff08;如user:1000:profile&#xff09;的命中率从99%骤降至40%&#xff0c;引发服务延迟上升。 排查步骤 1. 确认现象与定位Key // 通过Redis监控工具获取Key指标 public void monitorKey(String key) {Je…

自定义Shell命令行解释器

目录 1、目标 2、显示命令提示符 2.1 getenv 2.2 getcwd 2.3 putenv 3、获取用户输入的命令 4、解析命令 5、处理内建命令 6、处理外部命令 7、完整代码 7.1 myshell.cpp 7.2 Makefile 1、目标 实现一个Linux的myshell&#xff0c;有以下基本的功能。 显示命令提示…

Laplace 噪声

Laplace 噪声是一种特定概率分布&#xff08;拉普拉斯分布&#xff09;产生的随机扰动。它是差分隐私&#xff08;Differential Privacy, DP&#xff09;中最核心、最常用的噪声机制之一。它的核心作用是在不泄露个体信息的前提下&#xff0c;允许从包含敏感数据的数据库中提取…

基于空天地一体化网络的通信系统matlab性能分析

目录 1.引言 2.算法仿真效果演示 3.数据集格式或算法参数简介 4.MATLAB核心程序 5.算法涉及理论知识概要 5.1 QPSK调制原理 5.2 空天地一体化网络信道模型 5.3 空天地一体化网络信道特性 6.参考文献 7.完整算法代码文件获得 1.引言 空天地一体化网络是一种将卫星通信…

【Delphi】接收windows文件夹中文件拖拽

本文根据EmailX45的视频文件&#xff0c;进行了优化改进&#xff0c;原文参见&#xff1a;Delphi: Drag and Drop Files from Explorer into TPanel / TMemo - YouTube 在Windows中&#xff0c;如果将选择的文件拖动到Delphi程序的控件上&#xff0c;有很多实现方法&#xff0c…

基于热力学熵增原理的EM-GAN

简介 简介:提出基于热力学熵增原理的EM-GAN,通过生成器熵最大化约束增强输出多样性。引入熵敏感激活函数与特征空间熵计算模块,在MNIST/CelebA等数据集上实现FID分数提升23.6%,有效缓解模式崩溃问题。 论文题目:Entropy-Maximized Generative Adversarial Network (EM-G…

HashMap与ConcurrentHashMap详解:实现原理、源码分析与最佳实践

引言 在Java编程中&#xff0c;集合框架是最常用的工具之一&#xff0c;而HashMap和ConcurrentHashMap则是其中使用频率最高的两个Map实现。它们都用于存储键值对数据&#xff0c;但在实现机制、性能特点和适用场景上有着显著差异。 HashMap作为单线程环境下的首选Map实现&am…

CSS之动画(奔跑的熊、两面反转盒子、3D导航栏、旋转木马)

一、 2D转换 1.1 transform: translate( ) 转换&#xff08;transform&#xff09; 是CSS3中具有颠覆性的特征之一&#xff0c;可以实现元素的位移、旋转、缩放等效果 移动&#xff1a;translate 旋转&#xff1a;rotate 缩放&#xff1a;scale 下图为2D转换的坐标系 回忆…

【笔记】在 MSYS2(MINGW64)中安装 python-maturin 的记录

#工作记录 &#x1f4cc; 安装背景 操作系统&#xff1a;MSYS2 MINGW64当前时间&#xff1a;2025年6月1日Python 版本&#xff1a;3.12&#xff08;通过 pacman 安装&#xff09;目标工具&#xff1a;maturin —— 用于构建和发布 Rust 编写的 Python 包 &#x1f6e0;️ 安装…

基于微信小程序的垃圾分类系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

工作日记之权限校验-token的实战案例

背景说明 我们组负责维护的一个系统&#xff0c;前端界面挂载在其他两个系统上&#xff0c;因为历史遗留原因&#xff0c;同时也挂在公网上&#xff0c;没有登陆功能和用户体系&#xff0c;只要输入网址就能访问&#xff0c;虽然这个系统是给公司内部人员使用&#xff0c;但是…

mysql双主模式下基于keepalived的虚拟ip实现高可用模式搭建

数据库安装和升级和双主配置的操作可以参考我的另一篇文章&#xff1a; 数据库安装和升级和双主配置 1、在两台服务器都下载和安装keepalived 下载&#xff1a; yumdownloader --resolve keepalived 下载后得到&#xff1a; [rootlocalhost keepalivedRpm]# ll 总用量 1896 …

展会聚焦丨漫途科技亮相2025西北水务博览会!

2025第三届西北水务数字化发展论坛暨供排水节水灌溉新技术设备博览会在兰州甘肃国际会展中心圆满落幕。本届展会以“科技赋能水资源&#xff0c;数智引领新动能”为主题&#xff0c;活动汇集水务集团、科研院所、技术供应商等全产业链参与者&#xff0c;旨在通过前沿技术展示与…

单调栈(打卡)

本篇基于b站灵茶山艾府。 下面是灵神上课讲解的题目与课后作业&#xff0c;课后作业还有三道实在写不下去了&#xff0c;下次再写。 739. 每日温度 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是…

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

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

已有的前端项目打包到tauri运行(windows)

1.打包前端项目产生静态html、css、js 我们接下来用vue3 vite编写一个番茄钟案例来演示。 我们执行npm run build 命令产生的dist目录下的静态文件。 2.创建tarui项目 npm create tauri-applatest一路回车&#xff0c;直到出现。 3.启动运行 我们将打包产生的dist目录下的…

Unity3D仿星露谷物语开发55之保存地面属性到文件

1、目标 将游戏保存到文件&#xff0c;并从文件中加载游戏。 Player在游戏中种植的Crop&#xff0c;我们希望保存到文件中&#xff0c;当游戏重新加载时Crop的GridProperty数据仍然存在。这次主要实现保存地面属性&#xff08;GridProperties&#xff09;信息。 我们要做的是…

Java面试:企业协同SaaS中的技术挑战与解决方案

Java面试&#xff1a;企业协同SaaS中的技术挑战与解决方案 面试场景 在一家知名互联网大厂&#xff0c;面试官老王正在对一位应聘企业协同SaaS开发职位的程序员谢飞机进行技术面试。 第一轮提问&#xff1a;基础技术 老王&#xff1a;谢飞机&#xff0c;你好。首先&#xf…