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

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

相关文章:

  • Hadoop学习笔记
  • 【算法设计与分析】实验——改写二分搜索算法,众数问题(算法分析:主要算法思路),有重复元素的排列问题,整数因子分解问题(算法实现:过程,分析,小结)
  • Java递归编程中的StackOverflowError问题分析与解决方案
  • 从“敲窗“到“推门“:用Requests库解锁网络数据的“读心术“——Python爬虫入门实战指南
  • SCAU8640--希尔排序
  • 通俗理解“高内聚,低耦合”
  • ipfs下载和安装(windows)
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | Sound Board(音响控制面板)
  • 回文字符串
  • 【烧脑算法】不定长滑动窗口:从动态调整到精准匹配以灵活特性实现高效破题
  • 小目标检测:YOLOV7改进之双坐标注意力(DCA)
  • YOLOv10改进|爆改模型|涨点|C2F引入空间和通道注意力模块暴力涨点(附代码+修改教程)
  • 【C盘瘦身】给DevEco Studio中HarmonyOSEmulator(鸿蒙模拟器)换个地方,一键移动给C盘瘦身
  • FreeRTOS实时操作系统学习笔记
  • 文件索引:数组、二叉树、二叉排序树、平衡树、红黑树、B树、B+树
  • 【LeetCode 热题100】BFS/DFS 实战:岛屿数量 腐烂的橘子(力扣200 / 994 )(Go语言版)
  • vscode中的markdown表格列宽
  • js的时间循环的讲解
  • Y1——链式前向星
  • mybatisplus的总结
  • wordpress 搬家 图片/谷歌seo关键词排名优化
  • 金华做网站的公司/廊坊关键词快速排名
  • wordpress实现浮动联系/太原seo排名
  • 公司网站备案条件/电商网店
  • 京东商城网站设计/营销策划有限公司经营范围
  • 福建省建设系统网站/全面落实疫情防控优化措施