## Day13 Web后端开发进阶：Spring AOP

---

#### 什么是AOP

* AOP：Aspect Oriented Programming（<mark>面向切面编程、面向方面编程</mark>），可简单理解为就是面向特定方法编程。
* 场景：案例中部分业务方法运行较慢，定位执行耗时较长的接口，此时需要统计每一个业务方法的执行耗时。
* 优势：
  * 减少重复代码
  * 代码无侵入
  * 提高开发效率
  * 维护方便
* <mark>提示</mark> AOP是一种编程思想，而在Spring框架中对这种思想进行的实现，那我们要学习的就是Spring AOP。

  

<figure><img src="/AiJavaWeb/imgs/jwai13-01.png"><figcaption>图1 统计每一个业务方法的执行耗时</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai13-02.png"><figcaption>图2 AOP示意</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai13-03.png"><figcaption>图3 AOP示意</figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai13-04.png"><figcaption>图4 将一大把蒜苔切成几段或将一大把蒜苔在不同的几个特定段位做一些加工--横切</figcaption></figure>

#### 目录
* AOP基础
* AOP进阶
* AOP案例

### 1. AOP基础

#### ① AOP快速入门


* 需求：统计所有业务层方法的执行耗时。
* ①导入依赖：在pom.xml中引入AOP的依赖


* ②编写AOP程序：针对于特定的方法根据业务需要进行编程

<figure><img src="/AiJavaWeb/imgs/jwai13-05.png"><figcaption>图5 Spring AOP快速入门：统计所有业务层方法的执行耗时</figcaption></figure>

#### AOP快速入门程序

导入springboot-aop-quickstart模块，修改包名和数据库连接参数，编写AOP快速入门程序。

<figure><img src="/AiJavaWeb/imgs/jwai13-06.png"><figcaption>图6 导入springboot-aop-quickstart模块</figcaption></figure>

* ①导入依赖：在pom.xml中引入AOP的依赖

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
```


* ②编写AOP程序：针对于特定的方法根据业务需要进行编程

##### cn/dzj/aop/RecordTimeApsect.java

```java
package cn.dzj.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class RecordTimeApsect {
    @Around("execution(* cn.dzj.service.impl.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //1.记录方法运行的开始时间
        long begin = System.currentTimeMillis();

        //2.执行原始的方法
        Object result = pjp.proceed();

        //3，记录方法运行的结束时间，记录耗时
        long end = System.currentTimeMillis();
        log.info("方法 {} 运行耗时：{}ms",pjp.getSignature().getName(),(end - begin));
        return result;
    }
}
```

<figure><img src="/AiJavaWeb/imgs/jwai13-07.png"><figcaption>图7 <mark>AOP快速入门程序验证</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai13-08.png"><figcaption>图8 小结</figcaption></figure>

#### ② AOP核心概念

* 连接点：JoinPoint，可以被AOP控制的方法（暗含方法执行时的相关信息）
* 通知：Advice，指那些重复的逻辑，也就是共性功能（最终体现为一个方法）
* 切入点：PointCut，匹配连接点的条件，通知仅会在切入点方法执行时被应用
* 切面：Aspect，描述通知与切入点的对应关系（通知+切入点）
* 目标对象：Target，通知所应用的对象



<figure><img src="/AiJavaWeb/imgs/jwai13-09.png"><figcaption>图9 AOP核心概念: </figcaption></figure>

#### 执行流程

<figure><img src="/AiJavaWeb/imgs/jwai13-10.png"><figcaption>图10 执行流程</figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai13-11.png"><figcaption>图11 <mark>AOP执行流程中的动态代理对象</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai13-12.png"><figcaption>图12 切面、连接点、切入点</figcaption></figure>

#### 小结

<figure><img src="/AiJavaWeb/imgs/jwai13-13.png"><figcaption>图13 小结</figcaption></figure>

### 2. AOP进阶

* 通知类型
* 通知顺序
* 切入点表达式
* 连接点

#### ① 通知类型--方法

根据通知方法执行时机的不同，将通知类型分为以下常见的五类：

* @Around：环绕通知，此注解标注的通知方法在目标方法前、后都被执行
* @Before：前置通知，此注解标注的通知方法在目标方法前被执行
* @After ：后置通知，此注解标注的通知方法在目标方法后被执行，无论是否有异常都会执行
* @AfterReturning： 返回后通知，此注解标注的通知方法在目标方法后被执行，有异常不会执行
* @AfterThrowing ： 异常后通知，此注解标注的通知方法发生异常后执行

#### 注意
* <mark>@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行，其他通知不需要考虑目标方法执行</mark>
* <mark>@Around环绕通知方法的返回值，必须指定为Object，来接收原始方法的返回值。</mark>

##### cn/dzj/aop/MyAspect1.java

```java
package cn.dzj.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(* cn.dzj.service.impl.*.*(..))")
    public void before() {
        log.info("前置通知-目标方法运行之前 before...");
    }

    //环绕通知-目标方法运行之前、后运行
    @Around("execution(* cn.dzj.service.impl.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        log.info("环绕通知-目标方法运行之前 around...before...");
        Object result =pjp.proceed();
        log.info("环绕通知-目标方法运行之后 around... after ....  ");
        return result;
    }

    //后置通知-目标方法运行之后运行，无论是否出现异常都会执行
    @After("execution(* cn.dzj.service.impl.*.*(..))")
    public void after() {
        log.info("后置通知-目标方法运行之后 after...");
    }

    // 返回后通知-目标方法运行之后运行，如果出现异常不会运行
    @AfterReturning("execution(* cn.dzj.service.impl.*.*(..))")
    public void afterReturning() {
        log.info("返回后通知-目标方法运行之后，如果出现异常不会运行 afterReturning...");
    }

    //异常后通知-目标方法运行之后运行，只有出现异常才会运行
    @AfterThrowing("execution(* cn.dzj.service.impl.*.*(..))")
    public void afterThrowing() {
        log.info("异常后通知-目标方法运行之后，只有出现异常才会运行 afterThrowing...");
    }
}
```

<figure><img src="/AiJavaWeb/imgs/jwai13-14.png"><figcaption>图14 <mark>通知类型--方法 验证 </mark></figcaption></figure>

#### @PointCut

注解的作用是将公共的切点表达式抽取出来，需要用到时引用该切点表达式即可。

<figure><img src="/AiJavaWeb/imgs/jwai13-15.png"><figcaption>图15 @PointCut</figcaption></figure>

###### cn/dzj/aop/MyAspect1.java 修改如下

```java
package cn.dzj.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(* cn.dzj.service.impl.*.*(..))")
    private void pt() {}

    //前置通知-目标方法运行之前运行
    //@Before("execution(* cn.dzj.service.impl.*.*(..))")
    @Before("pt()")
    public void before() {
        log.info("使用@Pointcut注解统一注解切入点");
        log.info("前置通知-目标方法运行之前 before...");
    }

    //环绕通知-目标方法运行之前、后运行
    //@Around("execution(* cn.dzj.service.impl.*.*(..))")
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        log.info("环绕通知-目标方法运行之前 around...before...");
        Object result =pjp.proceed();
        log.info("环绕通知-目标方法运行之后 around... after ....  ");
        return result;
    }

    //后置通知-目标方法运行之后运行，无论是否出现异常都会执行
    //@After("execution(* cn.dzj.service.impl.*.*(..))")
    @After("pt()")
    public void after() {
        log.info("后置通知-目标方法运行之后 after...");
    }

    // 返回后通知-目标方法运行之后运行，如果出现异常不会运行
    //@AfterReturning("execution(* cn.dzj.service.impl.*.*(..))")
    @AfterReturning("pt()")
    public void afterReturning() {
        log.info("返回后通知-目标方法运行之后，如果出现异常不会运行 afterReturning...");
    }

    //异常后通知-目标方法运行之后运行，只有出现异常才会运行
    //@AfterThrowing("execution(* cn.dzj.service.impl.*.*(..))")
    @AfterThrowing("pt()")
    public void afterThrowing() {
        log.info("异常后通知-目标方法运行之后，只有出现异常才会运行 afterThrowing...");
    }
}
```



<figure><img src="/AiJavaWeb/imgs/jwai13-16.png"><figcaption>图16 <mark>通知类型--方法 验证2 </mark></figcaption></figure>

#### 小结

<figure><img src="/AiJavaWeb/imgs/jwai13-17.png"><figcaption>图17 小结</figcaption></figure>

#### ② 通知顺序

* 当有多个切面的切入点都匹配到了目标方法，目标方法运行时，多个通知方法都会被执行。
* 执行顺序：
  * 不同切面类中，默认按照切面类的类名字母排序：
    * 目标方法前的通知方法：字母排名靠前的先执行
    * 目标方法后的通知方法：字母排名靠前的后执行
  * 用 @Order(数字) 加在切面类上来控制顺序
    * 目标方法前的通知方法：数字小的先执行
    * 目标方法后的通知方法：数字小的后执行

##### cn/dzj/aop/MyAspect2.java

```java
package cn.dzj.aop;

import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@Component
@Aspect
public class MyAspect2 {
    //前置通知
    @Before("execution(* cn.dzj.service.impl.*.*(..))")
    public void before(){
        log.info("MyAspect2 -> 前置通知 before ...");
    }

    //后置通知
    @After("execution(* cn.dzj.service.impl.*.*(..))")
    public void after(){
        log.info("MyAspect2 -> 后置通知 after ...");
    }

    //环绕通知
    @Around("execution(* cn.dzj.service.impl.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        log.info("MyAspect2 -> 环绕通知之前 around... before...");
        Object result =pjp.proceed();
        log.info("MyAspect2 -> 环绕通知之后 around... after ....  ");
        return result;
    }
}
```

##### cn/dzj/aop/MyAspect3.java

```java
package cn.dzj.aop;

import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@Component
@Aspect
public class MyAspect3 {
    //前置通知
    @Before("execution(* cn.dzj.service.impl.*.*(..))")
    public void before(){
        log.info("MyAspect3 ->前置通知 before ...");
    }

    //后置通知
    @After("execution(* cn.dzj.service.impl.*.*(..))")
    public void after(){
        log.info("MyAspect3 -> 后置通知 after ...");
    }

    //环绕通知
    @Around("execution(* cn.dzj.service.impl.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        log.info("MyAspect3 -> 环绕通知之前 around... before...");
        Object result =pjp.proceed();
        log.info("MyAspect3 -> 环绕通知之后 around... after ....  ");
        return result;
    }
}
```

##### cn/dzj/aop/MyAspect4.java

```java
package cn.dzj.aop;

import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@Component
@Aspect
public class MyAspect4 {
    //前置通知
    @Before("execution(* cn.dzj.service.impl.*.*(..))")
    public void before(){
        log.info("MyAspect4 -> 前置通知 before ...");
    }

    //后置通知
    @After("execution(* cn.dzj.service.impl..*(..))")
    public void after(){
        log.info("MyAspect4 -> 后置通知 after ...");
    }

    //环绕通知
    @Around("execution(* cn.dzj.service.impl.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        log.info("MyAspect4 -> 环绕通知之前 around... before...");
        Object result =pjp.proceed();
        log.info("MyAspect4 -> 环绕通知之后 around... after ....  ");
        return result;
    }
}
```

注释掉 MyAspect1和RecordTimeApsect的//@Aspect测试

<figure><img src="/AiJavaWeb/imgs/jwai13-18.png"><figcaption>图18 <mark>通知顺序验证1</mark></figcaption></figure>


<figure><img src="/AiJavaWeb/imgs/jwai13-19.png"><figcaption>图19 <mark>通知顺序验证2</mark></figcaption></figure>


<figure><img src="/AiJavaWeb/imgs/jwai13-20.png"><figcaption>图20 通知顺序</figcaption></figure>



#### ③ 切入点表达式

* 介绍：描述切入点方法的一种表达式。
* 作用：用来决定项目中的哪些方法需要加入通知
* 常见形式：
  * execution(……)：根据方法的签名来匹配
  * @annotation(……) ：根据注解匹配

<figure><img src="/AiJavaWeb/imgs/jwai13-21.png"><figcaption>图21 切入点表达式常见形式</figcaption></figure>



#### 切入点表达式-execution

execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配，语法为：

* 其中带 ? 的表示可以省略的部分
* 访问修饰符：可省略（比如: public、protected）
* 包名.类名： 可省略 <mark>建议不省略</mark>
* throws 异常：可省略（注意是方法上声明抛出的异常，不是实际抛出的异常）

##### 可以使用通配符描述切入点
- *：单个独立的任意符号，可以通配任意返回值、包名、类名、方法名、任意类型的一个参数，也可以通配包、类、方法名的一部分
- ..：多个连续的任意符号，可以通配任意层级的包，或任意类型、任意个数的参数

<figure><img src="/AiJavaWeb/imgs/jwai13-22.png"><figcaption>图22 切入点表达式-execution</figcaption></figure>

##### cn/dzj/aop/MyAspect5.java

```java
package cn.dzj.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class MyAspect5 {
    //@Before("execution(void cn.dzj.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
    //@Before("execution(void delete(java.lang.Integer))"）//包名.类名强烈不建议省略
    //@Before("execution(* cn.dzj.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
    //@Before("execution(* com.*.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
    //@Before("execution(* cn.dzj.service.impl.*.delete(java.lang.Integer))")
    //@Before("execution(* cn.dzj.service.impl.*.*(java.lang.Integer))")
    //@Before("execution(* cn.dzj.service.impl.*.*(*))")
    //@Before("execution(* cn.dzj.service.impl.*.del*(*))")
    //@Before("execution(* cn.dzj.service.impl.*.*e(*))")
    //@Before("execution(* cn..service.impl.DeptServiceImpl.*(..))")
    //@Before("execution(* cn.dzj.service.*.*(..))")

    //匹配list与delete方法
    //@Before("execution(* cn.dzj.service.impl.DeptServiceImpl.list(..)) ||"+
    //        "execution(* cn.dzj.service.impl.DeptServiceImpl.delete(..))")
    @Before("@annotation(cn.dzj.anno.LogOperation)")
    public void before(){
        log.info("MyAspect5->before...");
    }
}
```

<figure><img src="/AiJavaWeb/imgs/jwai13-23.png"><figcaption>图23 <mark>切入点表达式-execution验证1</mark></figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai13-24.png"><figcaption>图24 <mark>切入点表达式-execution验证2</mark></figcaption></figure>



<figure><img src="/AiJavaWeb/imgs/jwai13-25.png"><figcaption>图25 <mark>切入点表达式-execution验证3</mark></figcaption></figure>

##### 小结

execution切入点表达式的完整语法 ?

* execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)

通配符有哪些 ?

* \*：单个独立的任意符号
* .. ：多个连续的任意符号

书写建议

* 所有业务方法名在命名时尽量规范，方便切入点表达式快速匹配。如：findXxx，updateXxx。
* 描述切入点方法通常基于接口描述，而不是直接描述实现类，增强拓展性。
* 在满足业务需要的前提下，尽量缩小切入点的匹配范围。如：包名尽量不使用..，使用*匹配单个包。

<figure><img src="/AiJavaWeb/imgs/jwai13-26.png"><figcaption>图26 @execution小结</figcaption></figure>

#### 切入点表达式-@annotation

<figure><img src="/AiJavaWeb/imgs/jwai13-27.png"><figcaption>图26 @annotation</figcaption></figure>

##### cn/dzj/anno/LogOperation.java

```java
package cn.dzj.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
}
```

##### cn/dzj/service/impl/DeptServiceImpl.java  修改代码 list和delete方法加注@LogOperation

```java
    @LogOperation
    @Override
    public List<Dept> list() {
        // int i = 1/0;
        List<Dept> deptList = deptMapper.list();
        return deptList;
    }
    @LogOperation
    @Override
    public void delete(Integer id) {
        deptMapper.delete(id);
    }
```

<figure><img src="/AiJavaWeb/imgs/jwai13-28.png"><figcaption>图28 <mark>@annotation验证</mark></figcaption></figure>



#### ④ 连接点

在Spring中用JoinPoint抽象了连接点，用它可以获得方法执行时的相关信息，如目标类名、方法名、方法参数等。

* 对于 @Around 通知，获取连接点信息只能使用  ProceedingJoinPoint
* 对于其它四种通知，获取连接点信息只能使用 JoinPoint ，它是 ProceedingJoinPoint 的父类型

<figure><img src="/AiJavaWeb/imgs/jwai13-29.png"><figcaption>图29 连接点</figcaption></figure>

##### cn/dzj/aop/MyAspect6.java  针对借口AOP

```java
package cn.dzj.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Slf4j
@Component
@Aspect
public class MyAspect6 {
    //环绕通知
    @Around("execution(* cn.dzj.service.*.*(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("环绕通知之前 around before");
        Object ret = pjp.proceed();
        log.info("环绕通知之后 around after");

        //1.获取目标对象
        Object target =pjp.getTarget();
        log.info("环绕通知之后 获取目标对象：{}", target);

        //2．获取目标类
        String className =pjp.getTarget().getClass().getName();
        log.info("环绕通知之后 获取目标类：{}",className);

        //3．获取目标方法
        String methodName=pjp.getSignature().getName();
        log.info("环绕通知之后 获取目标方法：{}",methodName);

        //4，获取目标方法参数
        Object[] args=pjp.getArgs();
        log.info("环绕通知之后 获取目标方法参数：{}", Arrays.toString(args));

        //5，获取目标方法的返回值
        log.info("环绕通知之后 获取目标方法的返回值：{}", ret.toString());
        return ret;
    }
    //前置通知
    @Before("execution(* cn.dzj.service.*.*(..))")
    public void before(JoinPoint joinPoint) {
        log.info("前置通知： before");
        //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));
    }
}

```



<figure><img src="/AiJavaWeb/imgs/jwai13-30.png"><figcaption>图30 <mark>连接点验证1</mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai13-31.png"><figcaption>图31 <mark>连接点验证2</mark></figcaption></figure>





### 3. AOP案例

#### 案例：将案例中增、删、改相关接口的操作日志记录到数据库表中

<figure><img src="/AiJavaWeb/imgs/jwai13-32.png"><figcaption>图32 案例分析</figcaption></figure>



#### ① 将案例中增、删、改相关接口的操作日志记录到数据库表中

##### pom.xml

若使用 Spring Boot，无需额外引入spring-boot-starter-web已包含 AOP 相关包

```xml
<!-- AOP起步依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
```

##### 创建操作日志表

```sql
-- 操作日志表
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 '操作日志表';

```

##### cn/dzj/anno/Log.java

```java
package cn.dzj.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
```

##### cn/dzj/pojo/OperateLog.java

```java
package cn.dzj.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; //操作耗时
}
```

##### cn/dzj/mapper/OperateLogMapper.java

```java
package cn.dzj.mapper;

import cn.dzj.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);

}
```

##### AI提问词

```java
假如你是一名java开发工程师, 请帮我基于Spring AOP实现记录系统所有增、删、改功能接口的操作日志。具体信息如下：
1. 日志信息包含：操作人、操作时间、目标类的全类名、目标方法的方法名、方法运行时参数、返回值、方法执行时长
2. 功能接口所在包为 cn.dzj.controller
3. 日志表为 operate_log 表，对应的实体类为 OperateLog。 
   具体表结构如下：
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 '操作日志表';

  实体类如下: 
  @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; //操作耗时
  }
4. 并且已经提供了OperateLogMapper接口来操作 operate_log, 并在其中已经定义好了 insert 方法用来保存日志数据.
@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);
}
```

##### cn/dzj/aop/OperateLogAspect.java   豆包生成的代码

```java
package cn.dzj.aop;

import cn.dzj.mapper.OperateLogMapper;
import cn.dzj.pojo.OperateLog;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;

/**
 * 操作日志切面类，用于记录系统增删改接口的操作日志
 */
@Slf4j
@Aspect
@Component
public class OperateLogAspect {

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Autowired
    private ObjectMapper objectMapper;

    /**
     * 环绕通知：拦截cn.dzj.controller包下所有增删改方法
     * 匹配以add、save、update、delete开头的方法
     */
    @Around("execution(* cn.dzj.controller.*.*(..)) && " +
            "(execution(* add*(..)) || (execution(* save*(..)) || execution(* update*(..)) || execution(* delete*(..))))")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 记录方法执行前的信息
        LocalDateTime operateTime = LocalDateTime.now();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String methodParams = getParamsAsString(args);

        // 获取当前操作人ID（实际项目中需从登录上下文获取）
        Integer operateEmpId = getCurrentUserId();

        // 2. 执行目标方法并记录时间
        long startTime = System.currentTimeMillis();
        Object returnValue = null;
        try {
            returnValue = joinPoint.proceed(); // 执行原方法
            return returnValue;
        } finally {
            // 3. 记录方法执行后的信息并保存日志
            long costTime = System.currentTimeMillis() - startTime;
            String returnValueStr = getReturnValueAsString(returnValue);

            // 构建日志对象
            OperateLog olog = new OperateLog();
            // 列编辑：使用Alt + 鼠标左键拖动（Windows），在log前面线下拖动
            olog.setOperateEmpId(operateEmpId);
            olog.setOperateTime(operateTime);
            olog.setClassName(className);
            olog.setMethodName(methodName);
            olog.setMethodParams(methodParams);
            olog.setReturnValue(returnValueStr);
            olog.setCostTime(costTime);

            // 保存日志
            operateLogMapper.insert(olog);
            log.info("操作日志：{}",olog);
        }
    }

    /**
     * 将方法参数转换为字符串
     */
    private String getParamsAsString(Object[] args) {
        if (args == null || args.length == 0) {
            return "无参数";
        }
        try {
            return objectMapper.writeValueAsString(args);
        } catch (JsonProcessingException e) {
            // 序列化失败时使用默认的toString方法
            return Arrays.toString(args);
        }
    }

    /**
     * 将返回值转换为字符串
     */
    private String getReturnValueAsString(Object returnValue) {
        if (returnValue == null) {
            return "无返回值";
        }
        try {
            return objectMapper.writeValueAsString(returnValue);
        } catch (JsonProcessingException e) {
            return returnValue.toString();
        }
    }

    /**
     * 获取当前登录用户ID
     * 实际项目中应从SecurityContext、ThreadLocal或Session中获取
     */
    private Integer getCurrentUserId() {
        // 这里仅为示例，实际需替换为真实的用户ID获取逻辑
        // 例如:
        // return SecurityContextHolder.getContext().getAuthentication().getPrincipal().getId();
        return 1; // 模拟当前登录用户ID
    }
}
```

前端http://localhost:90中添加、修改、删除部门后，日志文件中记录如下

<figure><img src="/AiJavaWeb/imgs/jwai13-33.png"><figcaption>图33 <mark>案例切入点验证 </mark></figcaption></figure>

<figure><img src="/AiJavaWeb/imgs/jwai13-34.png"><figcaption>图34 <mark>案例初步测试验证 </mark></figcaption></figure>

#### 使用原视频的的代码

##### cn/dzj/aop/OperationLogAspect.java

```java
package cn.dzj.aop;

import cn.dzj.mapper.OperateLogMapper;
import cn.dzj.pojo.OperateLog;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Arrays;

@Slf4j
@Aspect
@Component
public class OperationLogAspect {

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(cn.dzj.anno.Log)")
    public Object logOperation(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        // 执行目标方法
        Object result = joinPoint.proceed();
        // 计算耗时
        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;

        // 构建日志实体
        OperateLog olog = new OperateLog();
        olog.setOperateEmpId(getCurrentUserId()); // 这里需要你根据实际情况获取当前用户ID
        olog.setOperateTime(LocalDateTime.now());
        olog.setClassName(joinPoint.getTarget().getClass().getName());
        olog.setMethodName(joinPoint.getSignature().getName());
        olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
        olog.setReturnValue(result != null ? result.toString() : "void");
        olog.setCostTime(costTime);

        // 保存日志
        log.info("记录操作日志: {}", olog);
        operateLogMapper.insert(olog);

        return result;
    }

    private Integer getCurrentUserId() {
        //return CurrentHolder.getCurrentId();
        // 这里仅为示例，实际需替换为真实的用户ID获取逻辑
        return 1; // 模拟当前登录用户ID
    }
}
```



<figure><img src="/AiJavaWeb/imgs/jwai13-35.png"><figcaption>图35 <mark>切入点注解</mark></figcaption></figure>

注释OperateLogAspect的//@Aspect，前端http://localhost:90中添加666、修改666888、删除部门后，日志文件中记录如下

<figure><img src="/AiJavaWeb/imgs/jwai13-36.png"><figcaption>图36 <mark>案例初步测试验证2</mark></figcaption></figure>



#### ② 获取当前登录员工id

员工登录成功后，哪里存储的有当前登录员工的信息？

* 给客户端浏览器下发的jwt令牌中

如何从jwt令牌中获取到当前登录的员工信息？

* 获取请求头中传递的jwt令牌，并解析

TokenFilter中已经解析了令牌的信息，如何将其传递给AOP程序、Controller、Service呢?

* Prompt：我在令牌校验过滤器TokenFilter中获取了jwt令牌，并对其进行解析获取当了当前登录员工的ID，如何将这个ID传递给AOP程序、Controller、Service中呢？

<figure><img src="/AiJavaWeb/imgs/jwai13-37.png"><figcaption>图37 如何获取当前登录员工id</figcaption></figure>



#### ThreadLocal

* ThreadLocal并不是一个Thread，而是Thread的局部变量。
* ThreadLocal为每个线程提供一份单独的存储空间，<mark>具有线程隔离的效果，不同的线程之间不会相互干扰</mark>。

<figure><img src="/AiJavaWeb/imgs/jwai13-38.png"><figcaption>图38 ThreadLocal</figcaption></figure>

* ThreadLocal常用方法：
  * public void set(T value) 	设置当前线程的线程局部变量的值
  * public T get() 		返回当前线程所对应的线程局部变量的值
  * public void remove()        	移除当前线程的线程局部变量

##### src/test/java/cn/dzj/ThreadLocalTest.java

```java
package cn.dzj;
public class ThreadLocalTest {
    private static ThreadLocal<String> local = new ThreadLocal<>();
    public static void main(String[] args) {
        local.set("Main Message");
        //创建线程
        new Thread(new Runnable(){
            @Override
            public void run(){
                local.set("Sub Message");
                System.out.println(Thread.currentThread().getName() + "：" + local.get());
            }
            }).start();
        System.out.println(Thread.currentThread().getName() + ": " + local.get());
        local.remove();
        System.out.println(Thread.currentThread().getName() + ": " + local.get());
    }
}
```

<figure><img src="/AiJavaWeb/imgs/jwai13-39.png"><figcaption>图39  ThreadLocal相互隔离</figcaption></figure>

#### 获取当前登录员工

<figure><img src="/AiJavaWeb/imgs/jwai13-40.png"><figcaption>图40  获取当前登录员工具体步骤</figure>

具体操作步骤：

* 定义ThreadLocal操作的工具类，用于操作当前登录员工ID。
* 在TokenFilter中，解析完当前登录员工ID，将其存入ThreadLocal（用完之后需将其删除）。
* 在AOP程序中，从ThreadLocal中获取当前登录员工的ID。

##### cn/dzj/utils/CurrentHolder.java

```java
package cn.dzj.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();
    }
}
```

##### cn/dzj/interceptor/TokenInterceptor.java 修改令牌解析代码

```java
        //5.如果token存在，校验令牌，如果校验失败->返回错误信息（响应401状态码）
        try {
            //JwtUtils.parseToken( token);
            Claims claims=JwtUtils.parseToken(token);
            Integer empId =Integer.valueOf(claims.get("id").toString());
            CurrentHolder.setCurrentId(empId);//存入
            log.info("当前登录员工ID：{}，将其存入ThreadLocal",empId);
        } catch (Exception e) {
            log.info("令牌非法，响应401");
            response.setStatus(401);
            return false;
        }
```

##### cn/dzj/aop/OperationLogAspect.java  修改getCurrentUserId()方法

```java
    private Integer getCurrentUserId() {
        return CurrentHolder.getCurrentId();
        // 这里仅为示例，实际需替换为真实的用户ID获取逻辑
        //return 1; // 模拟当前登录用户ID
    }
```

前端http://localhost:90中，用【林冲】用户名登录，添加、修改、删除部门后，日志文件中记录如下

<figure><img src="/AiJavaWeb/imgs/jwai13-41.png"><figcaption>图41 <mark>日志文件记录验证</mark></figcaption></figure>

#### 过滤器获取用户id

##### cn/dzj/filter/TokenFilter.java  修改令牌解析代码

```java
        //5.如果token存在，校验令牌，如果校验失败->返回错误信息（响应401状态码）
        try {
            // JwtUtils.parseToken( token);
            Claims claims=JwtUtils.parseToken(token);
            Integer empId =Integer.valueOf(claims.get("id").toString());
            CurrentHolder.setCurrentId(empId);//存入
            log.info("当前登录员工ID：{}，将其存入ThreadLocal",empId);
        } catch (Exception e) {
            log.info("令牌非法，响应401");
            response.setStatus(401);
            return;
        }
```



修改cn/dzj/config/WebConfig.java代码：//@Configuration，// @Autowired；前端http://localhost:90中，用【吴用】用户名登录，添加、修改、删除部门后，日志文件中记录如下

<figure><img src="/AiJavaWeb/imgs/jwai13-42.png"><figcaption>图42 <mark>日志文件记录验证2</mark></figcaption></figure>

#### 小结

1.什么是ThreadLocal？

* ThreadLocal其实是线程的局部变量，为每个线程提供单独一份存储空间，具有线程隔离的效果，不同的线程之间不会相互干扰

2.ThreadLocal的应用场景

* 在同一个线程/同一个请求中，进行数据共享。

<figure><img src="/AiJavaWeb/imgs/jwai13-43.png"><figcaption>图43 ThreadLocal小结</figcaption></figure>