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

JavaWeb(后端进阶)

AOP 概述:

AOP:Aspect Oriented Programming (面向切面编程、面向方法编程),是一种思想

优点:减少重复代码,代码无侵入,提高开发效率,维护方便

AOP 入门:

需求:统计部门管理各个业务层方法执行耗时

在 pom.xml 文件中导入 AOP 的依赖:

<!-- AOP依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

编写 AOP 程序:针对于特定方法根据业务需要进行编程

@Component
@Aspect//当前类为切面类
@Slf4j
public class RecordTimeAspect{@Around("execution(* org.example.service.impl.DeptServiceImpl.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable{//记录方法执行开始时间long begin = System.currentTimeMillis();//执行原始方法Object result = pjp.proceed();//记录方法执行结束时间long end = System.currentTimeMillis();//计算方法执行耗时log.info("方法执行耗时: {}毫秒",end-begin);return result;}
}

AOP 详解:

连接点:JoinPoint

指的是可以被 AOP 控制的方法

在 SpringAOP 提供的 JoinPoint 当中,封装了连接点方法在执行时的相关信息

通知:Advice

指的是重复的逻辑,也就是共性的功能

切入点:PointCut

匹配连接点的条件,通知仅会在切入点方法被执行时被应用

切面:Aspect

描述通知与切入点的对应关系

切面所在的类,为切面类(被 @Aspect 注解标识的类)

目标对象:Target

指的是通知所应用的对象

通知类型:

                                                                Spring AOP 通知类型

@Around

环绕通知,此注解标注的通知方法在目标方法前、后都被执行

@Before

前置通知,此注解标注的通知方法在目标方法前被执行

@After

后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

@AfterReturning

返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行

@AfterThrowing

异常后通知,此注解标注的通知方法发生异常后执行

代码测试:

@Slf4j
@Component
@Aspect
public class MyAspect1{//前置通知@Before("execution(* org.example.service.*.*(..))")public void before(JoinPoint joinPoint){log.info("before ...");}//环绕通知@Around("execution(* org.example.service.*.*(..))")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{log.info("around before ...");//调用目标对象的原始方法执行Object result = proceedingJoinPoint.proceed();//原始方法如果执行时有异常,环绕通知中的后置代码将不会执行log.info("around after ...");return result;}//后置通知@After("execution(* org.example.service.*.*(..))")public void after(JoinPoint joinPoint){log.info("after ...");}//返回后通知(程序在正常执行的情况下,会执行的后置通知)@AfterReturning("execution(* org.example.service.*.*(..))")public void afterReturning(JoinPoint joinPoint){log.info("afterReturning ...");}//异常通知(程序在出现异常的情况下,执行的后置通知)@AfterThrowing("execution(* org.example.service.*.*(..))")public void afterThrowing(JoinPoint joinPoint){log.info("afterThrowing ...");}
}

程序发生异常的情况下:

@AfterReturning 标识的通知方法不会执行,@AfterThrowing 标识的通知方法执行

@Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑将不会执行(原始方法调用已经出现异常)

在使用通知时的注意事项:

@Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行

@Around 环绕通知方法的返回值,必须指定为 Object,来接收原始方法的返回值,否则原始方法执行完毕,将获取不到返回值

抽取:

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

@Slf4j
@Component
@Aspect
public class MyAspect1{//切入点方法(公共的切入点表达式)@Pointcut("execution(* org.example.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 修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把 private 改为 public,而在引用的时候,具体的语法为:

@Slf4j
@Component
@Aspect
public class MyAspect2{//引用 MyAspect1 切面类中的切入点表达式@Before("org.example.aop.MyAspect1.pt()")public void before(){log.info("MyAspect2 -> before ...");}
}

通知顺序:

当在项目开发当中定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法,此时当目标方法在运行的时候,这多个切面类当中的这些通知方法都会运行

在不同切面类中,默认按照切面类的类名字母排序:

  • 目标方法前的通知方法:字母排名靠前的先执行

  • 目标方法后的通知方法:字母排名靠前的后执行

控制通知的执行顺序:修改切面类的类名 或 使用 Spring  提供的 @Order 注解(推荐)

切入点表达式:

切入点表达式:描述切入点方法的一种表达式

作用:主要用来决定项目中哪些方法需要加入通知

常见形式:

execution(...):根据方法的签名来匹配

@annotation(...):根据注解匹配

execution:

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

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

使用通配符描述切入点:

* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

切入点表达式的语法规则:

方法的访问修饰符可以省略

返回值可以使用 * 号代替(任意返回值类型)

包名可以使用 * 号代替,代表任意包(一层包使用一个 * )

使用 ..配置包名,标识此包以及此包下的所有子包

类名可以使用 * 号代替,标识任意类

方法名可以使用 * 号代替,表示任意方法

可以使用 * 配置参数,一个任意类型的参数

可以使用 .. 配置参数,任意个任意类型的参数

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

execution(* org.example.service.DeptService.findAll(..)) || execution(* org.example.service.DeptService.deleteById(..))

切入点表达式书写建议:

所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配(如:findXxx,updateXxx)

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

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

@annotation:

基于注解的方式来匹配切入点方法

自定义注解:LogOperation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}

DetpServiceImpl:

@Service
public class DeptServiceImpl implements DeptService{@Autowiredprivate DeptMapper deptMapper;@Override@LogOperation//自定义注解:表示当前方法属于目标方法public List<Dept> findAll(){return deptMapper.findAll();}@Override@LogOperationpublic void deleteById(Integer id){deptMapper.deleteById(id);}/*...*/
}

切面类:

@Slf4j
@Component
@Aspect
public class MyAspect{//前置通知@Before("@annotation(org.example.anno.LogOperation)")public void before(){log.info("MyAspect -> before ...");}//后置通知@After("@annotation(org.example.anno.LogOperation)")public void after(){log.info("MyAspect -> after ...");}
}

AOP 案例:

需求:

将案例中增、删、改相关接口的操作日志记录到数据库表中

操作日志信息包含:

操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

代码实现:

准备工作:

在 pom.xml 中引入 AOP 的依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建数据库表结构:

-- 操作日志表
create table operate_log(id int unsigned primary key auto_increment comment 'ID',operate_emp_id int unsigned comment '操作人ID',operate_time datetime comment '操作时间',class_name varchar(100) comment '操作的类名',method_name varchar(100) comment '操作的方法名',method_params varchar(1000) comment '方法参数',return_value varchar(2000) comment '返回值, 存储json格式',cost_time int comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

创建实体类 OperateLog:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog{private Integer id; //IDprivate Integer operateEmpId; //操作人IDprivate LocalDateTime operateTime; //操作时间private String className; //操作类名private String methodName; //操作方法名private String methodParams; //操作方法参数private String returnValue; //操作方法返回值private Long costTime; //操作耗时
}

创建日志操作 Mapper 接口:

@Mapper
public interface OperateLogMapper{//插入日志数据@Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " +"values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")void insert(OperateLog log); 
}

自定义注解 @LogOperation:

//自定义注解,用于标识哪些方法需要记录日志
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}

定义 AOP 记录日志的切面类:

@Aspect
@Component
public class OperationLogAspect{@Autowiredprivate OperateLogMapper operateLogMapper;//环绕通知@Around("@annotation(log)")public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable{//记录开始时间long startTime = System.currentTimeMillis();//执行方法Object result = joinPoint.proceed();//当前时间long endTime = System.currentTimeMillis();//耗时long costTime = endTime - startTime;//构建日志对象OperateLog operateLog = new OperateLog();operateLog.setOperateEmpId(getCurrentUserId());//需要实现 getCurrentUserId 方法operateLog.setOperateTime(LocalDateTime.now());operateLog.setClassName(joinPoint.getTarget().getClass().getName());operateLog.setMethodName(joinPoint.getSignature().getName());operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));operateLog.setReturnValue(result.toString());operateLog.setCostTime(costTime);//插入日志operateLogMapper.insert(operateLog);return result;}//获取当前用户IDprivate int getCurrentUserId(){return 1;}
}

在 Controller 层需要记录日志的方法上加上注解 @LogOperation:

@Slf4j
@RequestMapping("/depts")
@RestController
public class DeptController{@Autowiredprivate DeptService deptService;//查询部门列表@GetMappingpublic Result list(){log.info("查询部门列表");List<Dept> deptList = deptService.findAll();return Result.success(deptList);}//根据ID删除部门@LogOperation@DeleteMappingpublic Result delete(Integer id){log.info("根据Id删除部门, id: {}" , id);deptService.deleteById(id);return Result.success();}//新增部门@LogOperation@PostMappingpublic Result add(@RequestBody Dept dept){log.info("新增部门, dept: {}" , dept);deptService.add(dept);return Result.success();}//根据ID查询部门@GetMapping("/{id}")public Result getById(@PathVariable Integer id){log.info("根据ID查询部门, id: {}" , id);Dept dept = deptService.getById(id);return Result.success(dept);}//修改部门@LogOperation@PutMappingpublic Result update(@RequestBody Dept dept){log.info("修改部门, dept: {}" , dept);deptService.update(dept);return Result.success();}
}
@Slf4j//在类中自动生成日志对象
@RequestMapping("/clazzs")
@RestController//标识 RESTful 风格的控制器类
public class ClazzController{@Autowiredprivate ClazzService clazzService;//条件分页查询班级@GetMapping//绑定 HTTP GET 请求public Result page(String name,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,//请求参数绑定@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end,@RequestParam(defaultValue = "1") Integer page,//接收前端传递的 URL 查询参数@RequestParam(defaultValue = "10") Integer pageSize){PageResult pageResult = clazzService.page(name,begin,end,page,pageSize);return Result.success(pageResult);}//查询全部班级@GetMapping("/list")public Result findAll(){List<Clazz> clazzList = clazzService.findAll();return Result.success(clazzList);}//新增班级@LogOperation@PostMappingpublic Result add(@RequestBody Clazz clazz){//将请求体中的json数据自动转换为指定的对象clazzService.add(clazz);return Result.success();}//根据ID查询班级@GetMapping("/{id}")public Result getInfo(@PathVariable Integer id){Clazz clazz = clazzService.getInfo(id);return Result.success(clazz);}//修改班级@LogOperation@PutMappingpublic Result update(@RequestBody Clazz clazz){clazzService.update(clazz);return Result.success();}//删除班级@LogOperation@DeleteMapping("/{id}")public Result delete(@PathVariable Integer id){clazzService.deleteById(id);return Result.success();}
}

测试操作日志记录功能:

连接点:

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

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

@Around("execution(* org.example.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{String className = joinPoint.getTarget().getClass().getName();//获取目标类名Signature signature = joinPoint.getSignature();//获取目标方法签名String methodName = joinPoint.getSignature().getName();//获取目标方法名Object[] args = joinPoint.getArgs();//获取目标方法运行参数Object res = joinPoint.proceed();//执行原始方法,获取返回值(环绕通知)return res;
}

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

@Before("execution(* org.example.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();//获取目标方法运行参数
}

获取当前登录员工:

ThreadLocal:

ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量

ThreadLocal 为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰

常见方法:

public void set(T value) 设置当前线程的线程局部变量的值

public T get() 返回当前线程所对应的线程局部变量的值

public void remove() 移除当前线程的线程局部变量

记录当前登录员工:

定义 ThreadLocal 操作的工具类,用于操作当前登录员工 ID:

package org.example.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();}
}

在 TokenFilter 中,解析完当前登录员工 ID,将其存入 ThreadLocal (用完后需将其删除)

//令牌校验过滤器
@Slf4j
//@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter{@Overridepublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;//转换为HTTP请求对象HttpServletResponse response = (HttpServletResponse) resp;//转换为HTTP响应对象//获取请求urlString url = request.getRequestURL().toString();//判断是否是登录请求if(url.contains("login")){//登录请求log.info("登录请求 , 放行");chain.doFilter(request, response);return;}//获取请求头中的令牌(token)String jwt = request.getHeader("token");//判断令牌是否存在if(!StringUtils.hasLength(jwt)){//jwt为空log.info("获取到jwt令牌为空, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);//设置响应状态码为401return;}//解析tokentry{Claims claims = JwtUtils.parseJWT(jwt);Integer empId = Integer.valueOf(claims.get("id").toString());CurrentHolder.setCurrentId(empId);}catch(Exception e){//解析失败e.printStackTrace();log.info("解析令牌失败, 返回错误结果");response.setStatus(HttpStatus.SC_UNAUTHORIZED);//设置响应状态码为401return;}//放行log.info("令牌合法, 放行");chain.doFilter(request , response);//允许请求继续访问目标接口//清空当前线程绑定的IDCurrentHolder.remove();}
}

在 AOP 程序中,从 ThreadLocal 中获取当前登录员工的 ID:

@Aspect
@Component
public class OperationLogAspect{@Autowiredprivate OperateLogMapper operateLogMapper;//环绕通知@Around("@annotation(log)")public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable{//记录开始时间long startTime = System.currentTimeMillis();//执行方法Object result = joinPoint.proceed();//当前时间long endTime = System.currentTimeMillis();//耗时long costTime = endTime - startTime;//构建日志对象OperateLog operateLog = new OperateLog();operateLog.setOperateEmpId(getCurrentUserId());//需要实现 getCurrentUserId 方法operateLog.setOperateTime(LocalDateTime.now());operateLog.setClassName(joinPoint.getTarget().getClass().getName());operateLog.setMethodName(joinPoint.getSignature().getName());operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));operateLog.setReturnValue(result.toString());operateLog.setCostTime(costTime);//插入日志operateLogMapper.insert(operateLog);return result;}//获取当前用户IDprivate int getCurrentUserId(){return CurrentHolder.getCurrentId();}
}

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

http://www.dtcms.com/a/577045.html

相关文章:

  • VOC浓度快速测定仪在厂界预警中的实战应用:PID传感器技术与数据分析
  • 【SRE】安装Grafana实践
  • 在 PHP 中打印数据(调试、输出内容)
  • 网站运营有什么用做公司网站需要了解哪些东西
  • 段描述符属性测试
  • Ubuntu安装mysql5.7及常见错误问题
  • 第四届图像处理、计算机视觉与机器学习国际学术会议(ICICML 2025)
  • 网站后台编辑网站开发科普书
  • 单位加强网站建设专门做素菜的网站
  • Rust 在内存安全方面的设计方案的核心思想是“共享不可变,可变不共享”
  • NXP的GUI Guider开发LVGL
  • 《金仓KingbaseES vs 达梦DM:从迁移到运维的全维度TCO实测对比》
  • 【开题答辩全过程】以 基于Java的相机专卖网的设计与实现为例,包含答辩的问题和答案
  • 增量爬取策略:如何持续监控贝壳网最新成交数据
  • 400Hz 橡胶弹性体动刚度扫频试验系统指标
  • Weavefox 携手 GLM-4.6/4.5V 打造新一代智能厨房小助手
  • 如何建立网站后台wordpress 主题 翻译
  • 深入理解 Java 双亲委派机制:JVM 类加载体系全解析
  • Linux 进程间关系与守护进程
  • 基于 Cursor 的智能测试用例生成系统 - 项目介绍与实施指南
  • 时序数据库选型指南:从大数据视角切入,聚焦 Apache IoTDB
  • Node.js 环境变量配置实战:从入门到精通
  • 嵌入式系统入门指南
  • 一次丝滑的内网渗透拿下域控
  • 福建亨利建设集团有限公司网站互展科技网站建设
  • 网页变成PDF下载到本地
  • Spring Boot 3 + Flyway 全流程教程
  • 【洛谷】枚举专题-二进制枚举 从子集到矩阵问题,经典应用与实现
  • 网站信息可以wordpress可视化编辑器插件
  • 机器学习训练过程中回调函数常用的一些属性