JAVA WEB 进阶

📅 2026/8/25 2:01:51
JAVA WEB 进阶
AOPAOP: Aspect Oriented Programming (面向切面编程、面向方面编程)可简单理解为就是面向特定方法编程。场景:案例中部分业务方法运行较慢定位执行耗时较长的方法此时需要统计每一个业务方法的执行耗时。优势:1.减少重复代码2.代码无侵入3.提高开发效率4.维护方便提示:AOP是一种思想而在Spring框架中对这种思想进行的实现那我们要学习的就是Spring AOP。AOP基础AOP快速入门需求:统计所有业务层方法的执行耗时。获取方法运行的开始时间运行原始方法获取方法运行结束时间计算执行耗时SpringAOP快速入门:统计所有业务层方法的执行耗时1.导入依赖:在pom.xml中引入AOP的依赖2.编写AOP程序:针对于特定的方法根据业务需要进行编程1.引入依赖2.编写AOP程序:针对于特定的方法根据业务需要进行编程package com.itheima.app; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; Aspect //表示当前是一个AOP类 Component public class RecordTimeAspect { Around(execution(* com.itheima.service.impl.*.*(..))) public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { //1.记录方法运行的开始时间 long startTime System.currentTimeMillis(); //2.执行原始方法 Object result pjp.proceed(); //3·记录方法运行的结束时间 记录耗时 long endTime System.currentTimeMillis(); System.out.println(方法运行耗时 (endTime - startTime) ms); return result; } }小结1.SpringAOP程序的开发步骤?引入AOP的依赖编写AOP的程序(公共的逻辑代。2.SpringAOP的应用场景?记录系统的操作日志事务管理权限控制AOP核心概念连接点:JoinPoint可以被AOP控制的方法(暗含方法执行时的相关信息)通知:Advice指那些重复的逻辑也就是共性功能(最终体现为一个方法)切入点:PointCut匹配连接点的条件通知仅会在切入点方法执行时被应用实际被AOP控制的方法切面:Aspect描述通知与切入点的对应关系(通知切入点)目标对象:Target通知所应用的对象切入点一定是连接点但连接点不一定是切入点AOP执行流程通过动态代理技术给目标对象生成一个代理对象小结1.AOP核心概念连点(JoinPoint)切入点(PointCut)通知(Advice)切面(Aspect)目标对象(Target)AOP进阶通知类型根据通知方法执行时机的不同将通知类型分为以下常见的五类:1.Around:环绕通知此注解标注的通知方法在目标方法前、后都被执行2.Before:前置通知此注解标注的通知方法在目标方法前被执行3.After:后置通知此注解标注的通知方法在目标方法后被执行无论是否有异常都会执行4.AfterReturning:返回后通知此注解标注的通知方法在目标方法后被执行有异常不会执行5.AfterThrowing: 异常后通知此注解标注的通知方法发生异常后执行注意1:Around环绕通知需要自己调用 ProceedingJoinPoint.proceed()来让原始方法执行其他通知不需要考虑目标方法执行注意2:Around环绕通知方法的返回值必须指定为object来接收原始方法的返回值。package com.itheima.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; Slf4j Aspect Component public class MyAspect1 { //前置通知--目标方法运行之前运行 Before(execution(* com.itheima.service.impl.*.*(..))) public void before() { log.info(MyAspect1 before...); } //环绕通知--目标方法运行之前和运行之后都运行 Around(execution(* com.itheima.service.impl.*.*(..))) public Object around(ProceedingJoinPoint joinPoint) throws Throwable { log.info(around... before...); Object result joinPoint.proceed(); log.info(around... after...); return result; } //后置通知--目标方法运行之后运行无论目标方法是否发生异常 After(execution(* com.itheima.service.impl.*.*(..))) public void after() { log.info(MyAspect1 after...); } //返回后通知--目标方法运行之后运行出现异常就不运行 AfterReturning(execution(* com.itheima.service.impl.*.*(..))) public void afterReturning() { log.info(MyAspect1 afterReturning...); } //异常后通知--目标方法运行之后运行出现异常就才运行 AfterThrowing(execution(* com.itheima.service.impl.*.*(..))) public void afterThrowing() { log.info(MyAspect1 afterThrowing...); } }PointCut该注解的作用是将公共的切点表达式抽取出来需要用到时引用该切点表达式即可。private:仅能在当前切面类中引用该表达式public:在其他外部的切面类中也可以引用该表达式package com.itheima.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; Slf4j Aspect Component public class MyAspect1 { //切入点表达式 Pointcut(execution(* com.itheima.service.impl.*.*(..))) private void pt(){} //前置通知--目标方法运行之前运行 Before(pt()) public void before() { log.info(MyAspect1 before...); } //环绕通知--目标方法运行之前和运行之后都运行 Around(pt()) public Object around(ProceedingJoinPoint joinPoint) throws Throwable { log.info(around... before...); Object result joinPoint.proceed(); log.info(around... after...); return result; } //后置通知--目标方法运行之后运行无论目标方法是否发生异常 After(pt()) public void after() { log.info(MyAspect1 after...); } //返回后通知--目标方法运行之后运行出现异常就不运行 AfterReturning(pt()) public void afterReturning() { log.info(MyAspect1 afterReturning...); } //异常后通知--目标方法运行之后运行出现异常就才运行 AfterThrowing(pt()) public void afterThrowing() { log.info(MyAspect1 afterThrowing...); } }小结1.常见的通知类型有哪些?分别在什么时候执行a.Before(前置通知)b.After(后置通知)c.Around(环绕通知重点)d. AfterReturning (返回后通知)e.AfterThrowing(异常后通知)2.PointCut注解的作用是什么?抽取公共的切点表达式提高复用性通知顺序当有多个切面的切入点都匹配到了目标方法目标方法运行时多个通知方法都会被执行。执行顺序:不同切面类中默认按照切面类的类名字母排序:目标方法前的通知方法:字母排名靠前的先执行目标方法后的通知方法:字母排名靠前的后执行用0rder(数字)加在切面类上来控制顺序目标方法前的通知方法:数字小的先执行目标方法后的通知方法:数字小的后执行切入点表达式介绍:描述切入点方法的一种表达式。作用:用来决定项目中的哪些方法需要加入通知常见形式:1.execution(....):根据方法的签名来匹配2.annotation(.....):根据注解匹配切入点表达式-executionexecution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配语法为:其中带?的表示可以省略的部分1.访问修饰符:可省略(比如:public、protected)2.包名.类名:可省略3.throws异常:可省略(注意是方法上声明抛出的异常不是实际抛出的异常)可以使用通配符描述切入点1.* :单个独立的任意符号可以通配任意返回值、包名、类名、方法名、任意类型的一个参数也可以通配包、类、方法名的一部分2. .. :多个连续的任意符号可以通配任意层级的包或任意类型、任意个数的参数!注意1:根据业务需要可以使用且()、或()、非(!)组合比较复杂的切入点表达式。1.*号package com.itheima.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; Slf4j Component Aspect public class MyAspect5 { //前置通知 // Before(execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))) // Before(execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))) // Before(execution(void delete(java.lang.Integer)))//包名类名可以省略但是不建议省略 // Before(execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))) // Before(execution(* com.*.service.impl.DeptServiceImpl.delete(java.lang.Integer))) // Before(execution(* com.itheima.service.impl.*.delete(java.lang.Integer))) // Before(execution(* com.itheima.service.impl.*.*(java.lang.Integer))) // Before(execution(* com.itheima.service.impl.*.*(*))) // Before(execution(* com.itheima.service.impl.*.del*(*))) Before(execution(* com.itheima.service.impl.*.*e(*))) public void before(){ log.info(MyAspect4 - before ...); } }2. .. 号package com.itheima.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; Slf4j Component Aspect public class MyAspect5 { //前置通知 //实现类去匹配 // Before(execution(* com..service.impl.DeptServiceImpl.*(..))) //接口去匹配 Before(execution(* com..service.*.*(..))) public void before(){ log.info(MyAspect4 - before ...); } }例子匹配list与deletepackage com.itheima.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; Slf4j Component Aspect public class MyAspect5 { //前置通知 //匹配list与delete Before(execution(* com.itheima.service.impl.DeptServiceImpl.list(..)) || execution(* com.itheima.service.impl.DeptServiceImpl.delete(..))) public void before(){ log.info(MyAspect4 - before ...); } }小结1.execution切入点表达式的完整语法?execution(访问修饰符?返回值 包名.类名.?方法名(方法参数)throws 异常?)2.通配符有哪些?a.*:单个独立的任意符号b...:多个连续的任意符号3.书写建议a.所有业务方法名在命名时尽量规范方便切入点表达式快速匹配。如:findXxxupdateXxx。b.描述切入点方法通常基于接口描述而不是直接描述实现类增强拓展性。c.在满足业务需要的前提下尽量缩小切入点的匹配范围。如:包名尽量不使用..使用*匹配单个包。切入点表达式-annotationannotation 切入点表达式用于匹配标识有特定注解的方法。1.创建一个注解文件package com.itheima.anno; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.METHOD)//修饰注解的注解 表示这个注解在哪生效ElementType.METHOD表示方法上 Retention(RetentionPolicy.RUNTIME)//表示这个注解什么时候生效 public interface LogOperation { }2.注册注解package com.itheima.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; Slf4j Component Aspect public class MyAspect5 { //前置通知 Before(annotation(com.itheima.anno.LogOperation)) public void before(){ log.info(MyAspect4 - before ...); } }3.给方法添加注解连接点在Spring中用JoinPoint抽象了连接点用它可以获得方法执行时的相关信息如目标类名、方法名、方法参数等。对于 Around 通知获取连接点信息只能使用 ProceedingJoinPoint对于其它四种通知获取连接点信息只能使用 JoinPoint它是 ProceedingJoinPoint 的父类型package com.itheima.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.util.Arrays; Slf4j Component Aspect public class MyAspect6 { Before(execution(* com.itheima.service.*.*(..))) public void before(JoinPoint joinPoint){ //1.获取目标对象 Object target joinPoint.getTarget() ; log.info(获取目标对象 : {}, target); //2.获取目标类 String className joinPoint.getTarget().getClass().getName(); log.info(获取目标类 : {}, className); //3.获取目标方法 String methodName joinPoint.getSignature().getName() ; log.info(获取目标方法 : {}, methodName); //4.获取目标方法参数 Object[] args joinPoint.getArgs() ; log.info(获取目标方法参数 : {}, Arrays.toString(args)); } Around(execution(* com.itheima.service.*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { log.info(arount... before ...); Object result pjp.proceed() ; log.info(arount... after ...); return result ; } }AOP案例将案例中增、删、改相关接口的操作日志记录到数据库表中日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长采用哪种通知类型?Around环绕通知切入点表达式该怎么写?由于增、删、改 方法名我们定义的比较规范分别为save、delete、update代码步骤1.创建日志表-- 操作日志表 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(2000) comment 方法参数, return_value varchar(2000) comment 返回值, cost_time bigint unsigned comment 方法执行耗时, 单位:ms ) comment 操作日志表;2.创建日志实体类package org.itqull.pojo; import lombok.Data; import java.time.LocalDateTime; Data public class OperateLog { private Integer id; //ID private Integer operateEmpId; //操作人ID private LocalDateTime operateTime; //操作时间 private String className; //操作类名 private String methodName; //操作方法名 private String methodParams; //操作方法参数 private String returnValue; //操作方法返回值 private Long costTime; //操作耗时 }3.操作数据库表创建mapper类package com.itheima.mapper; import com.itheima.pojo.OperateLog; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.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});) public void insert(OperateLog log); }4.引入依赖!-- AOP起步依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency5.定义注解类package com.itheima.anno; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; Target(ElementType.METHOD)//修饰注解的注解 表示这个注解在哪生效ElementType.METHOD表示方法上 Retention(RetentionPolicy.RUNTIME)//表示这个注解什么时候生效 public interface LogOperation { }6.编写aop类package com.itheima.aop; // 声明当前类所在的包 // 导入 Jackson 的 JSON 序列化异常类用于捕获对象转 JSON 时的异常 import com.fasterxml.jackson.core.JsonProcessingException; // 导入 Jackson 的 JSON 工具类用于把对象转成 JSON 字符串 import com.fasterxml.jackson.databind.ObjectMapper; // 导入操作日志 Mapper用于把日志保存到 operate_log 表 import com.itheima.mapper.OperateLogMapper; // 导入操作日志实体类 OperateLog import com.itheima.pojo.OperateLog; // 导入 Lombok 的 Slf4j 注解让 Spring 自动生成 log 日志对象 import lombok.extern.slf4j.Slf4j; // 导入 AOP 环绕通知的连接点对象通过它获取方法信息并放行原方法 import org.aspectj.lang.ProceedingJoinPoint; // 导入 AOP 环绕通知注解 import org.aspectj.lang.annotation.Around; // 导入 AOP 切面注解标记当前类是一个切面 import org.aspectj.lang.annotation.Aspect; // 导入 Spring 依赖注入注解让 Spring 自动注入对象 import org.springframework.beans.factory.annotation.Autowired; // 导入 Spring 组件注解把当前类交给 Spring 容器管理 import org.springframework.stereotype.Component; // 导入 Java 本地时间类用于记录操作时间 import java.time.LocalDateTime; import java.util.Arrays; Slf4j // Lombok 自动生成 log 字段方便打印日志 Aspect // 声明当前类是一个 AOP 切面 Component // 将当前切面对象交给 Spring 容器管理 public class OperateLogAspect { // 操作日志切面类 // 创建一个 Jackson 工具对象用来把方法参数和返回值转成 JSON 字符串 private static final ObjectMapper OBJECT_MAPPER new ObjectMapper(); Autowired // 让 Spring 自动注入下面的 Mapper 对象 private OperateLogMapper operateLogMapper; // 操作日志 Mapper用于调用 insert 保存日志 // 环绕通知只拦截带有 com.itheima.annp.LogOperation 注解的方法 Around(annotation(com.itheima.annp.LogOperation)) public Object recordOperateLog(ProceedingJoinPoint joinPoint) throws Throwable { // 环绕通知方法返回原方法执行结果 long startTime System.currentTimeMillis(); // 记录方法开始执行的时间用于后面计算耗时 // 创建日志对象并逐项设置日志信息 OperateLog olog new OperateLog(); // 创建操作日志实体对象 olog.setOperateEmpId(getCurrentEmpId()); // 设置操作人 ID从当前登录用户信息中获取 olog.setOperateTime(LocalDateTime.now()); // 设置操作时间即当前系统时间 olog.setClassName(joinPoint.getTarget().getClass().getName()); // 设置目标类的全类名 olog.setMethodName(joinPoint.getSignature().getName()); // 设置被拦截的目标方法名 olog.setMethodParams(Arrays.toString(joinPoint.getArgs())); // 把方法参数转成 JSON 并截断到 2000 字符再存入日志 Object result null; // 用来保存被拦截方法的返回值 try { // 开始执行原业务方法 result joinPoint.proceed(); // 放行原方法调用真正的业务逻辑 return result; // 原方法正常执行完成后把结果返回给调用方 } finally { // 无论原方法正常返回还是抛出异常最终都会执行这里的代码 olog.setReturnValue(result !null?result.toString():void); // 把返回值转成 JSON 并截断到 2000 字符再存入日志 olog.setCostTime(System.currentTimeMillis() - startTime); // 计算本次方法执行耗时并设置到日志对象中 try { // 开始保存日志 //记录操作日志 log.info(保存操作日志{}, olog); operateLogMapper.insert(olog); // 调用 Mapper 把日志记录插入到 operate_log 表中 } catch (Exception e) { // 捕获保存日志时的异常 OperateLogAspect.log.error(保存操作日志失败, e); // 日志保存失败只打印错误日志不能影响原业务方法 } } } /** * 根据项目的登录态获取当前操作人ID。 * 例如从 ThreadLocal、JWT、Session 或 Spring Security 中获取。 */ private Integer getCurrentEmpId() { // 获取当前登录操作人的 ID // TODO: 替换成项目实际获取当前登录用户ID的方式 return 1; // 当前示例暂未实现先返回 null } // 把对象转换成 JSON 字符串 private String toJson(Object value) { try { // JSON 转换可能抛异常所以使用 try-catch return OBJECT_MAPPER.writeValueAsString(value); // 使用 Jackson 把对象转成 JSON 字符串 } catch (JsonProcessingException e) { // 捕获对象转 JSON 时的异常 log.warn(操作日志参数或返回值序列化失败, e); // 序列化失败时打印警告日志 return null; // 序列化失败时返回 null避免影响业务 } } // 截断超长字符串防止超过数据库字段长度 private String truncate(String value, int maxLength) { if (value null || value.length() maxLength) { // 如果字符串为空或者没有超过最大长度 return value; // 直接返回原字符串 } return value.substring(0, maxLength); // 超过长度时只截取前 maxLength 个字符 } }7.引用日志获取当前登录员工1.员工登录成功后哪里存储的有当前登录员工的信息?给客户端浏览器下发的jwt令牌中2.如何从jwt令牌中获取到当前登录的员工信息?获取请求头中传递的jwt令牌并解析3.TokenFilter中已经解析了令牌的信息如何将其传递给AOP程序、Controller、Service呢?ThreadLocalThreadLocalThreadLocal并不是一个Thread而是Thread的局部变量。ThreadLocal为每个线程提供一份单独的存储空间具有线程隔离的效果不同的线程之间不会相互干扰。ThreadLocal常用方法:public void set(T value) 设置当前线程的线程局部变量的值public T get() 返回当前线程所对应的线程局部变量的值public void remove() 移除当前线程的线程局部变量具体操作步骤:1.定义ThreadLocal操作的工具类用于操作前登录员工ID。2.在TokenFilter中解析完当前登录员工ID将其存入ThreadLocal(用完之后需将其删除)。3.在AOP程序中从ThreadLocal中获取当前登录员工的ID。1.定义ThreadLocal操作的工具类package com.itheima.utils; public class CurrentHolder { private static final ThreadLocalInteger 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(); } }2.在TokenFilter中解析完当前登录员工ID将其存入ThreadLocalpackage com.itheima.filter; import com.itheima.utils.CurrentHolder; import com.itheima.utils.JwtUtils; import io.jsonwebtoken.Claims; import jakarta.servlet.*; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import java.io.IOException; Slf4j WebFilter(urlPatterns /*) public class TokenFilter implements Filter { Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) servletRequest; HttpServletResponse response (HttpServletResponse) servletResponse; //1. 获取到请求路径 String requestURI request.getRequestURI(); // /employee/login //2. 判断是否是登录请求, 如果路径中包含 /login, 说明是登录操作, 放行 if (requestURI.contains(/login)){ log.info(登录请求, 放行); filterChain.doFilter(request, response); return; } //3. 获取请求头中的token String token request.getHeader(token); //4. 判断token是否存在, 如果不存在, 说明用户没有登录, 返回错误信息(响应401状态码) if (token null || token.isEmpty()){ log.info(令牌为空, 响应401); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } //5. 如果token存在, 校验令牌, 如果校验失败 - 返回错误信息(响应401状态码) try { Claims claims JwtUtils.parseToken(token); //Integer.valueOf 把对象转换为整数 Integer empId Integer.valueOf(claims.get(id).toString()); CurrentHolder.setCurrentId(empId);//存储当前登录员工的id log.info(当前登录员工id{}, empId); } catch (Exception e) { log.info(令牌非法, 响应401); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } //6. 校验通过, 放行 log.info(令牌合法, 放行); filterChain.doFilter(request, response); //7. 移除当前登录员工的id CurrentHolder.remove();//移除当前登录员工的id } }3.在AOP程序中从ThreadLocal中获取当前登录员工的ID。package com.itheima.aop; // 声明当前类所在的包 // 导入 Jackson 的 JSON 序列化异常类用于捕获对象转 JSON 时的异常 import com.fasterxml.jackson.core.JsonProcessingException; // 导入 Jackson 的 JSON 工具类用于把对象转成 JSON 字符串 import com.fasterxml.jackson.databind.ObjectMapper; // 导入操作日志 Mapper用于把日志保存到 operate_log 表 import com.itheima.mapper.OperateLogMapper; // 导入操作日志实体类 OperateLog import com.itheima.pojo.OperateLog; // 导入 Lombok 的 Slf4j 注解让 Spring 自动生成 log 日志对象 import com.itheima.utils.CurrentHolder; import lombok.extern.slf4j.Slf4j; // 导入 AOP 环绕通知的连接点对象通过它获取方法信息并放行原方法 import org.aspectj.lang.ProceedingJoinPoint; // 导入 AOP 环绕通知注解 import org.aspectj.lang.annotation.Around; // 导入 AOP 切面注解标记当前类是一个切面 import org.aspectj.lang.annotation.Aspect; // 导入 Spring 依赖注入注解让 Spring 自动注入对象 import org.springframework.beans.factory.annotation.Autowired; // 导入 Spring 组件注解把当前类交给 Spring 容器管理 import org.springframework.stereotype.Component; // 导入 Java 本地时间类用于记录操作时间 import java.time.LocalDateTime; import java.util.Arrays; Slf4j // Lombok 自动生成 log 字段方便打印日志 Aspect // 声明当前类是一个 AOP 切面 Component // 将当前切面对象交给 Spring 容器管理 public class OperateLogAspect { // 操作日志切面类 // 创建一个 Jackson 工具对象用来把方法参数和返回值转成 JSON 字符串 private static final ObjectMapper OBJECT_MAPPER new ObjectMapper(); Autowired // 让 Spring 自动注入下面的 Mapper 对象 private OperateLogMapper operateLogMapper; // 操作日志 Mapper用于调用 insert 保存日志 // 环绕通知只拦截带有 com.itheima.annp.LogOperation 注解的方法 Around(annotation(com.itheima.annp.LogOperation)) public Object recordOperateLog(ProceedingJoinPoint joinPoint) throws Throwable { // 环绕通知方法返回原方法执行结果 long startTime System.currentTimeMillis(); // 记录方法开始执行的时间用于后面计算耗时 // 创建日志对象并逐项设置日志信息 OperateLog olog new OperateLog(); // 创建操作日志实体对象 olog.setOperateEmpId(getCurrentEmpId()); // 设置操作人 ID从当前登录用户信息中获取 olog.setOperateTime(LocalDateTime.now()); // 设置操作时间即当前系统时间 olog.setClassName(joinPoint.getTarget().getClass().getName()); // 设置目标类的全类名 olog.setMethodName(joinPoint.getSignature().getName()); // 设置被拦截的目标方法名 olog.setMethodParams(Arrays.toString(joinPoint.getArgs())); // 把方法参数转成 JSON 并截断到 2000 字符再存入日志 Object result null; // 用来保存被拦截方法的返回值 try { // 开始执行原业务方法 result joinPoint.proceed(); // 放行原方法调用真正的业务逻辑 return result; // 原方法正常执行完成后把结果返回给调用方 } finally { // 无论原方法正常返回还是抛出异常最终都会执行这里的代码 olog.setReturnValue(result !null?result.toString():void); // 把返回值转成 JSON 并截断到 2000 字符再存入日志 olog.setCostTime(System.currentTimeMillis() - startTime); // 计算本次方法执行耗时并设置到日志对象中 try { // 开始保存日志 //记录操作日志 log.info(保存操作日志{}, olog); operateLogMapper.insert(olog); // 调用 Mapper 把日志记录插入到 operate_log 表中 } catch (Exception e) { // 捕获保存日志时的异常 OperateLogAspect.log.error(保存操作日志失败, e); // 日志保存失败只打印错误日志不能影响原业务方法 } } } /** * 根据项目的登录态获取当前操作人ID。 * 例如从 ThreadLocal、JWT、Session 或 Spring Security 中获取。 */ private Integer getCurrentEmpId() { // 获取当前登录操作人的 ID // TODO: 替换成项目实际获取当前登录用户ID的方式 return CurrentHolder.getCurrentId(); // 当前示例暂未实现先返回 null } // 把对象转换成 JSON 字符串 private String toJson(Object value) { try { // JSON 转换可能抛异常所以使用 try-catch return OBJECT_MAPPER.writeValueAsString(value); // 使用 Jackson 把对象转成 JSON 字符串 } catch (JsonProcessingException e) { // 捕获对象转 JSON 时的异常 log.warn(操作日志参数或返回值序列化失败, e); // 序列化失败时打印警告日志 return null; // 序列化失败时返回 null避免影响业务 } } // 截断超长字符串防止超过数据库字段长度 private String truncate(String value, int maxLength) { if (value null || value.length() maxLength) { // 如果字符串为空或者没有超过最大长度 return value; // 直接返回原字符串 } return value.substring(0, maxLength); // 超过长度时只截取前 maxLength 个字符 } }小结