SpringBootWeb 篇-深入了解 AOP 面向切面编程与 AOP 记录操作日志案例

🔥博客主页: 【小扳_-CSDN博客】**
❤感谢大家点赞👍收藏⭐评论✍**

文章目录

[1.0 AOP 概述](#1.0 AOP 概述)

[1.1 构造简单 AOP 类](#1.1 构造简单 AOP 类)

[2.0 AOP 核心概念](#2.0 AOP 核心概念)

[2.1 AOP 执行流程](#2.1 AOP 执行流程)

[3.0 AOP 通知类型](#3.0 AOP 通知类型)

[4.0 AOP 通知顺序](#4.0 AOP 通知顺序)

[4.1 默认按照切面类的类名字母排序](#4.1 默认按照切面类的类名字母排序)

[4.2 用 @Order(数字) 注解加在切面类上来控制顺序](#4.2 用 @Order(数字) 注解加在切面类上来控制顺序)

[5.0 AOP 切入点表达式](#5.0 AOP 切入点表达式)

[5.1 使用 execution() 创建切入点表达式](#5.1 使用 execution() 创建切入点表达式)

[5.2 使用 @annotation 创建切入点表达式](#5.2 使用 @annotation 创建切入点表达式)

[6.0 AOP 连接点](#6.0 AOP 连接点)

[7.0 AOP 案例 - 记录操作日志](#7.0 AOP 案例 - 记录操作日志)


1.0 AOP 概述

AOP,Aspect Oriented Programming 面向切面编程,在 AOP 中,横切关注点被称为切面(Aspect),切面通过特定的注入方式被应用到程序的不同部分,从而实现对这些部分的增强或修改。AOP 能够帮助开发者更好地管理程序的复杂性,提高代码的重用性和易读性。

简单来说,就是面向特定的方法编程,也或者说给原始的方法进行升级改造。这样原始的方法就不需要进行改变,从而实现方法升级了。如日志记录、权限控制等功能。通过AOP,可以实现方法的升级改造,提高代码的可维护性和可重用性。

1.1 构造简单 AOP 类

1)首先导入 AOP 依赖:

在 pom.xml 中导入 AOP 的依赖。

XML 复制代码
    <!--AOP依赖-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

2)编写 AOP 程序:

针对特定的方法业务需要进行编程。

首先创建类,在类上加上 @Component 注解进行控制反转,成为 IOC 容器中的 Bean 对象。继续在类上加上 @Aspect 注解,代表当前类不是普通类而是 AOP 类。在方法上加上通知类型,根据切入点表达式来筛选出连接点。

代码演示:

java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class demo1 {
    @Around("execution(* org.example.controller.DeptController.getList())")
    public Object demo(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("正在执行目标代码之前的代码");
        Object result = joinPoint.proceed();
        System.out.println("正在执行目标代码之后的代码");
        return result;
    }
}

2.0 AOP 核心概念

连接点:JoinPoint,可以被 AOP 控制的方法(暗含方法执行时的相关信息)。

通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)。

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

切面:Aspect,描述通知与切入点的对应关心(通知+切入点)。

目标对象:Target,通知所应用的对象。

连接点与切入点的区别:

简而言之,连接点是具体的程序执行事件,而切入点是一种筛选连接点的机制,可以帮助我们选择在哪些连接点应用切面逻辑。对于切入点来说,是通过切入点表达式来描述切入点。

2.1 AOP 执行流程

1)首先定义切面:

开发人员定义一个切面,包含通知和切入点的定义。通知定义了切面逻辑,包括前置通知、后置通知、环绕通知等。切入点定义了在哪些连接点上应用切面逻辑。

2)创建目标对象和代理对象:

确定目标对象,即需要进行增强的对象。AOP 框架会创建一个代理对象来包含目标对象和切面。应用程序中会通过代理对象来调用目标对象的方法。

3)选择连接点:

在应用程序执行过程中,AOP框架根据切入点的定义选择适当的连接点。连接点是指程序执行过程中可以被增强的具体事件。

4)执行切面逻辑:

对于选择的连接点,AOP 框架会在该连接点上执行相应的增强逻辑,即通知。根据通知的类型,在连接点执行前、执行后或执行前后都可能执行切面逻辑。

5)织入切面:

织入是将切面与应用程序的目标对象结合起来创建代理对象的过程。AOP 框架会动态地将切面织入到目标对象的方法调用中,从而实现横切关注点的功能。织入可以发生在编译时、加载时、运行时或动态切入时。

6)执行增强后的程序:

当应用程序使用代理对象调用目标对象的方法时,会触发代理对象的增强逻辑。代理对象会在适当的连接点上执行切面逻辑,从而实现对应用程序的增强功能。

简单来说,当程序运行时,执行到了与切入点匹配适合的连接点,也就是匹配到对应的方法时,那么就会由代理对象替代原始的方法,代理对象的方法包含了切面方法和原始的方法,也就是包含了通知与原始方法的代码,到最后,当程序执行原始方法的方法名的时候,不会继续往下执行原始方法里面的内容了,会执行代理对象中的方法。

3.0 AOP 通知类型

1)@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行。

2)@Before:前置通知,此注解标注的通知方法在目标方法前被执行。

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

4)@AfterReturning:返回后通知,此注解标注的通知的方法在目标方法后被执行,有异常不会被执行,也就是说,当目标方法出现异常时,那么该通知方法就不会执行。

5)@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行。

需要注意的是:

@Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行。@Around 环绕通知方法的返回值,必须指定为 Object ,来接收原始方法的返回值。

4.0 AOP 通知顺序

如果有多个通知类型都绑定在同一个连接点上,其执行顺序可能会有所不同。因此,在配置 AOP 时,需要谨慎考虑通知的顺序以保证业务逻辑的正确执行。

也就是说,当连接点匹配到多个通知类型时,是按照什么顺序执行的呢?

1)默认按照切面类的类名字母排序

2)用 @Order(数字) 注解加在切面类上来控制顺序

4.1 默认按照切面类的类名字母排序

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

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

代码演示:

demo1 切面类:

java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class demo1 {

    @Around("execution(* org.example.controller.DeptController.getList())")
    public Object demo(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("正在执行 demo1 切面类");

        Object result = joinPoint.proceed();

        System.out.println("正在执行 demo1 切面类");

        return result;

    }
}

demo2 切面类:

java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class demo2 {

    @Around("execution(* org.example.controller.DeptController.getList())")
    public Object demo(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("正在执行 demo2 切面类");

        Object result = joinPoint.proceed();

        System.out.println("正在执行 demo2 切面类");

        return result;

    }

}

demo3 切面类:

java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class demo3 {

    @Around("execution(* org.example.controller.DeptController.getList())")
    public Object demo(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("正在执行 demo3 切面类");

        Object result = joinPoint.proceed();

        System.out.println("正在执行 demo3 切面类");

        return  result;
    }
}

运行结果:

4.2 用 @Order(数字) 注解加在切面类上来控制顺序

目标方法前的通知方法:数字小的先执行。

目标方法后的通知方法:数字小的后执行。

代码演示:

demo1 切面类:

java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Order(3)
public class demo1 {

    @Around("execution(* org.example.controller.DeptController.getList())")
    public Object demo(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("正在执行 demo1 切面类");

        Object result = joinPoint.proceed();

        System.out.println("正在执行 demo1 切面类");

        return result;

    }
}

demo2 切面类:

java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Order(2)
public class demo2 {

    @Around("execution(* org.example.controller.DeptController.getList())")
    public Object demo(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("正在执行 demo2 切面类");

        Object result = joinPoint.proceed();

        System.out.println("正在执行 demo2 切面类");

        return result;
    }
}

demo3 切面类:

java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Aspect
@Order(1)
public class demo3 {

    @Around("execution(* org.example.controller.DeptController.getList())")
    public Object demo(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("正在执行 demo3 切面类");

        Object result = joinPoint.proceed();

        System.out.println("正在执行 demo3 切面类");

        return  result;
    }
}

运行结果:

5.0 AOP 切入点表达式

切入点表达式是描述切入点方法的一种表达式,用来筛选连接点也就是选择目标方法,主要用来决定项目中的哪些方法需要加入通知。

常见的形式:

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

2)@annotation():根据注解匹配。

补充:什么是方法签名?

方法签名是一个方法在源代码中的表示,它由方法的名称、返回类型、参数列表以及可能的抛出异常列表组成。而权限修饰符是不属于方法签名的一部分。

5.1 使用 execution() 创建切入点表达式

1)根据方法的签名来匹配连接点。

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

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

其中访问修饰符、包名.类名、throws 异常这些部分代码是可以省略的。需要注意的是 throws 异常是方法上声明抛出的异常,不是实际抛出的异常。

2)可以使用通配符描述切入点:

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

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

execution(* org.example.controller.DeptController.getList()) 切入点为:在任意返回值类型下的 org.example.controller 包下的 DeptController 类下的 getList 没有参数的方法。

还可以根据业务需要可以使用 && 、 ||、 ! 来组合比较复杂的切入点表达式。

5.2 使用 @annotation 创建切入点表达式

用于匹配标识有特定注解的方法。

1)首先创建一个注解

代码演示:

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

在注解上加上两个元注解 @Retention 用来表示该注解什么时候生效,会被保留到运行时,可以通过反射机制在运行时获取注解信息。@Target 指定了注解可以应用的目标类型。

2)接着手动给连接点也就是目标方法上加上自定义的注解,最后在 AOP 通知类型的注解属性中添加自定义注解的全类名。

代码演示:

java 复制代码
    @Around("@annotation(org.example.Anto.Log)")
    public Object log(ProceedingJoinPoint proceedingJoinPoint){
        //目标方法执行之前,需要执行的代码

        proceedingJoinPoint.proceed();

        //目标方法执行之后,需要执行的代码
}

6.0 AOP 连接点

连接点简单来说就是 AOP 所控制的方法。

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

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

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

代码演示:

java 复制代码
//获取目标类的类名
String className = proceedingJoinPoint.getTarget().getClass().getName();

//获取目标方法名
String methodName = proceedingJoinPoint.getSignature().getName();

//获取目标方法的方法参数
Object[] args = proceedingJoinPoint.getArgs();

//获得目标方法的返回值
Object result = proceedingJoinPoint.proceed();

7.0 AOP 案例 - 记录操作日志

实现将每一次操作的操作信息记录到数据库中。

实现思路:

先创建数据库:

实现类:

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OperateLog {
    private Integer id;
    private  Integer operateUser;
    private LocalDateTime operateTime;
    private String className;
    private String methodName;
    private String methodParams;
    private String returnValue;
    private Long costTime;
}

AOPMapper 接口:

java 复制代码
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.example.Pojo.OperateLog;

@Mapper
public interface AOPMapper {


    @Insert("insert into operate_log(operate_time,class_name,method_name,method_params,return_value,cost_time) " +
            "values (#{operateTime},#{className},#{methodName},#{methodParams},#{returnValue},#{costTime})")
    public void log(OperateLog operateLog);
}

定义 AOP 切面类:

java 复制代码
import com.alibaba.fastjson.JSONObject;
import io.jsonwebtoken.Claims;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.example.Pojo.OperateLog;
import org.example.Utilities.JWT;
import org.example.mapper.AOPMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;

@Component
@Aspect
public class AOPLog {

    @Autowired
    HttpServletRequest request;

    @Autowired
    AOPMapper aopMapper;
    
    @Around("@annotation(org.example.Anto.Log)")
    public Object log(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        String jwt = request.getHeader("token");
        Claims claims = JWT.parse(jwt);

        LocalDateTime operateTime = LocalDateTime.now();

        //获取目标类的类名
        String className = proceedingJoinPoint.getTarget().getClass().getName();

        //获取目标方法名
        String methodName = proceedingJoinPoint.getSignature().getName();

        //获取目标方法的方法参数
        Object[] args = proceedingJoinPoint.getArgs();
        String methodParams = Arrays.toString(args);
        System.out.println("方法执行之前");

        long start = System.currentTimeMillis();
        //获得目标方法的返回值
        Object result = proceedingJoinPoint.proceed();
        long end = System.currentTimeMillis();

        String returnValue = JSONObject.toJSONString(result);
        Long costTime = end - start;

        OperateLog operateLog = new OperateLog(null,null,operateTime,className,methodName,methodParams,returnValue,costTime);

        aopMapper.log(operateLog);
        System.out.println(operateLog);
        System.out.println("方法执行之后");
        return result;
    }
}

自定义的注解:

java 复制代码
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

}

接着将 @Log 注解加到需要进行操作时记录的方法上即可。

相关推荐
Fairy_sevenseven几秒前
【二十八】【QT开发应用】模拟WPS Tab
开发语言·qt·wps
_GR7 分钟前
每日OJ题_牛客_牛牛冲钻五_模拟_C++_Java
java·数据结构·c++·算法·动态规划
容器( ु⁎ᴗ_ᴗ⁎)ु.。oO8 分钟前
MySQL事务
数据库·mysql
蜡笔小新星8 分钟前
Python Kivy库学习路线
开发语言·网络·经验分享·python·学习
凯子坚持 c8 分钟前
C语言复习概要(三)
c语言·开发语言
无限大.20 分钟前
c语言200例 067
java·c语言·开发语言
余炜yw21 分钟前
【Java序列化器】Java 中常用序列化器的探索与实践
java·开发语言
攸攸太上22 分钟前
JMeter学习
java·后端·学习·jmeter·微服务
篝火悟者23 分钟前
问题-python-运行报错-SyntaxError: Non-UTF-8 code starting with ‘\xd5‘ in file 汉字编码问题
开发语言·python
Kenny.志25 分钟前
2、Spring Boot 3.x 集成 Feign
java·spring boot·后端