Spring AOP编程

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

**场景:**想查看一个方法的执行耗时,是在方法执行前记录一次时间,在方法执行完再记录一次时间,而如果有多个方法需要统计时间,会出现很多重复性代码,而且修改时需要对原方法进行修改。

**AOP的优势:**减少重复代码、代码无入侵、提高效率、维护方便等

应用场景

  • 前置:
    • 权限检验
    • 日志记录等
  • 后置:
    • 资源释放
    • 性能监控等
  • 环绕:
    • 事务管理等

核心概念

  • 连接点(JoinPoint):可被AOP控制的方法(一般所有public方法均可以);
  • 通知(Advice):指那些重复的公共逻辑操作,即自定义的方法逻辑操作(比如方法的日志记录,对于日志记录的信息构造以及持久化等操作,就属于通知);
  • 切入点(Pointcut):也就是被增强的连接点(即真正被AOP通知所执行的方法);
  • 切面(Aspect):一个容器,包含多个切入点和通过(也就是被 @Aspect 注解的AOP类,该类可以有多个方法,不同方法可以有不同的操作逻辑,可以针对不同的切入点);
  • 目标对象(Target):表示通知所应用的对象,也就是切入点所在方法的类的对象(即被代理的原始业务对象);
  • 织入(Weaving):将通知嵌入目标对象并创建代理的过程。

AOP执行流程(底层实现:动态代理)

通知类型

  • @Around :环绕通知,在目标方法执行前后都会被执行,返回值为Object;
  • @Before :前置通知,目标方法前执行,无论异常与否都执行,无返回值;
  • @After :后置通知,目标方法后执行,无论异常与否都执行,无返回值;
  • @AfterReturning :返回后通知,目标方法执行后被执行,有异常不执行,可以为void、Object或者自定义类型;
  • @AfterThrowing :异常后通知,通知方法发生异常后执行,否则不执行,无返回值。

注意:

  1. @Around 需要自己在通知方法中声明 ProceedingJoinPoint 类型对象pjp,然后自己通过 [res = ]pjp.proceed(); 方法来让原方法执行,其他通知则不需要考虑;
  2. @Around 的方法的返回值必须指定为Object,用来接收原始方法的返回值。

通知顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,各个通知方法都会被执行。

  • 不同切面类中:默认按切面类的类名字母排序;

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

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

      //示例
      @Component
      @Aspect
      public class AspectA {
      @Before("com.test.PointCutConfig.cut()")
      public void beforeA(){
      System.out.println("切面A 前置");
      }

      复制代码
      @After("com.test.PointCutConfig.cut()")
      public void afterA(){
          System.out.println("切面A 后置");
      }

      }

      @Component

      @Aspect
      public class AspectB {
      @Before("com.test.PointCutConfig.cut()")
      public void beforeB(){
      System.out.println("切面B 前置");
      }

      复制代码
      @After("com.test.PointCutConfig.cut()")
      public void afterB(){
          System.out.println("切面B 后置");
      }

      }

      /*

      1. 都是同一个切入点,则所有通知都会生效
      2. 因为AspectA的字母序列在AspectB之前,即A前B后。
        则对于前置而言是A先执行,后才是B,对于后置而言,则是B先执行,后才是A

      执行顺序:
      切面A 前置
      切面B 前置
      =====目标方法执行=====
      切面B 后置
      切面A 后置
      */

  • @Order(num) 加在切面类上:

    • 目标方法前的,数字小先执行;

    • 目标方法后的,数字大先执行。

      //示例
      @Order(1) // 数值小
      public class AspectA {
      ... //与上面示例内容相同
      }

      @Order(2) // 数值大
      public class AspectB {
      ...
      }

      /*
      因为使用Order注解标注,A的数字比B小,即A在前B在后,因此:
      对于前置,数字小(在前)的先执行,对于后置,数字大(在后)的先执行

      执行顺序:
      切面A 前置
      切面B 前置
      =====目标方法执行=====
      切面B 后置
      切面A 后置
      */

**总结:**前置在前先执行,后置在后先执行

切面类在前(字母排序在前/数字排序在前,即小在前)的前置方法先执行;切面类在后的后置方法先执行。

切点表达式

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

  1. execution([访问修饰符]? 返回值 [包名.类名.]?方法名(方法形参) [throws 异常]?) :根据方法签名来匹配,可通过 || 或者 && 来连接多个execution(...)进行匹配。其中?表示前面[ ]块可省略。
    • 访问修饰符(可选)Spring AOP 仅支持代理 public 方法,直接省略不写
    • 返回值类型 (必填)匹配方法返回值:void(无返回)、String(字符串)、*(任意返回值)
    • 包名/类名(可选)定位方法所在的包和类,支持通配符
    • 方法名(必填)要匹配的方法名称,支持通配符
    • 方法形参 (必填)() = 无参方法;(..) = 任意参数;(String) = 仅 String 参数
    • throws 异常(可选)几乎不用,直接忽略
  2. @annotation(注解类的引用,eg:com.demo.anno.LogOperation) :根据注解匹配,匹配标识有特定注解的方法。

通配符

  • * :任意位置都能用,表示单个独立的任意符合,可通配上面任意一个参数;
  • .. :只能出现在包或参数两个位置,表示多个连续任意符号,可通配任意层级包、任意类型、任意个数的参数。

简单示例

1、项目结构

复制代码
com.example.demo
├── annotation/    自定义注解(用于@annotation匹配)
├── service/       业务类(目标方法,被增强的对象)
├── aspect/        切面类(编写切点表达式+通知)
└── DemoApplicationTests  测试类

2、自定义注解(用于注解匹配)

复制代码
package com.example.demo.annotation;
import java.lang.annotation.*;

// 自定义注解:贴在方法上,AOP就会匹配这个方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}

3、业务类(目标方法,被匹配的对象)

复制代码
package com.example.demo.service;
import com.example.demo.annotation.MyLog;
import org.springframework.stereotype.Service;

@Service
public class UserService {
    // 无参方法
    public void addUser() {
        System.out.println("执行:添加用户");
    }

    // 有参方法
    public String getUserById(Integer id) {
        System.out.println("执行:根据ID查询用户");
        return "用户" + id;
    }

    // 贴了自定义注解的方法
    @MyLog
    public void deleteUser() {
        System.out.println("执行:删除用户");
    }
}

4、切面类(核心:所有切点表达式 + 通知)

复制代码
package com.example.demo.aspect;

import com.example.demo.annotation.MyLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TestAspect {

    // ===================== 1. 基础 execution 表达式 =====================
    // 匹配:UserService类中 无参、无返回值 的 addUser() 方法
    @Before("execution(void com.example.demo.service.UserService.addUser())")
    public void test1(JoinPoint jp) {
        System.out.println("[匹配无参方法] 前置通知");
    }

    // ===================== 2. 通配符 * :匹配任意返回值 =====================
    // 匹配:UserService中 任意返回值、getUserById(Integer) 方法
    @Before("execution(* com.example.demo.service.UserService.getUserById(Integer))")
    public void test2() {
        System.out.println("[匹配指定参数方法] 前置通知");
    }

    // ===================== 3. 通配符 .. :匹配任意参数 =====================
    // 匹配:UserService中 任意返回值、任意参数 的 所有方法
    @Before("execution(* com.example.demo.service.UserService.*(..))")
    public void test3() {
        System.out.println("[匹配类下所有方法] 前置通知");
    }

    // ===================== 4. 通配符 .. :匹配任意层级包 =====================
    // 匹配:com.example.demo 包及其子包下 所有类的所有方法
    @Before("execution(* com.example.demo..*.*(..))")
    public void test4() {
        System.out.println("[匹配全包所有方法] 前置通知");
    }

    // ===================== 5. 通配符 * :匹配方法名前缀 =====================
    // 匹配:UserService中 以 get 开头的任意方法
    @Before("execution(* com.example.demo.service.UserService.get*())")
    public void test5() {
        System.out.println("[匹配方法名前缀] 前置通知");
    }

    // ===================== 6. @annotation :匹配注解 =====================
    // 匹配:所有添加了 @MyLog 注解的方法
    @Before("@annotation(com.example.demo.annotation.MyLog)")
    public void test6() {
        System.out.println("[匹配注解方法] 前置通知");
    }

    // ===================== 7. 组合匹配:&& 且 =====================
    // 匹配:UserService中 以 delete 开头 + 任意参数 的方法,其中execution(* *(..))表示任意返回值类型+任意方法+任意参数类型的方法(所有方法恒成立)
    @Before("execution(* com.example.demo.service.UserService.delete*(..)) && execution(* *(..))")
    public void test7() {
        System.out.println("[组合匹配] 前置通知");
    }
}

注意

如果需要复用切点表达式,可以使用 @Pointcut(xxx) 将公共的切点表达式抽取出来,需要时直接引用即可

复制代码
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TestAspect {
    //抽取需要重复使用的切点表达式
    @Pointcut("execution(* com.example.demo.service.UserService.*(..))")
    public void userServicePointcut(){}
    //public:表示在其他外部切面类中也可以使用该表达式
    //private:表示当前切点表达式只能在当前切面类中使用
    
    //使用
    @Around("userServicePointcut()")
    public Object testFunc(){
        ...
    }
}

连接点对象

父类为 JoinPoint 类型,子类为 ProceedingJoinPoint 类型,用它可以获得方法执行时的相关信息,比如目标类名、方法名、参数等。

对于环绕通知,只能使用 ProceedingJoinPoint ,而对于其他四种类型的通知,只能使用 JoinPoint (环绕通知是必须在形参中声明 ProceedingJoinPoint ,而其他通知如果需要获取相关信息,才需要在形参中声明 JoinPoint

常见方法

  • getTarget() :获取目标对象;
  • getTarget().getClass().getName() :获取目标类;
  • getSignature().getName() :获取目标方法名;
  • getArgs() :获取目标方法参数。

注解匹配使用示例-日志记录

1、引入依赖

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

2、编写所需要的注解类

在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 AdminLog {
}

3、编写AOP类对特定方法根据需要进行编程

如果是使用 @execution(xxx) ,则只需将通知类型中的内容更换即可

复制代码
import com.hyltest.mapper.AdminMapper;
import com.hyltest.mapper.OperateLogMapper;
import com.hyltest.pojo.entity.OperateLog;
import com.hyltest.utils.CurrentHolder;
import lombok.RequiredArgsConstructor;
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;

import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;

@RequiredArgsConstructor
@Slf4j
@Aspect
@Component
public class OperateLogAspect {

    private final AdminMapper adminMapper;
    private final OperateLogMapper operateLogMapper;

    @Around("@annotation(com.hyltest.anno.AdminLog)")  //通过注解的形式将方法操作作为注解的操作
    public Object recordAdminOperateLog(ProceedingJoinPoint pjp) throws Throwable{
        OperateLog operateLog = new OperateLog();
        //-----**通过pjp获取想要的数据**------
        //获取执行的方法名
        String methodName = pjp.getSignature().getName();
        //获取目标类
        String className = pjp.getTarget().getClass().getSimpleName();
        //获取当前操作时间
        operateLog.setCreateTime(LocalDateTime.now());
        //获取当前操作人id
        Integer id = CurrentHolder.getCurrentId();
        //根据id获取当前操作人名字
        String name = adminMapper.getNameById(id);
        //组装操作日志
        operateLog.setAdminId(id);
        operateLog.setMethod(name+"操作了"+className+"."+methodName+"方法");

        Object result = null;
        try {
            // **当公共逻辑执行完,放行去执行原方法**
            result = pjp.proceed();  //获得原方法的返回值
            return result;
        } catch (Exception e) {
            // 继续抛出异常,不影响业务逻辑
            throw e;
        } finally {
            // 异步保存日志,避免影响主业务流程
            CompletableFuture.runAsync(() -> {
                try {
                    //保存操作日志
                    operateLogMapper.insertNewOperateLog(operateLog);
                } catch (Exception ex) {
                    // 记录日志保存失败,但不影响业务
                    log.error("保存操作日志失败: {}", ex.getMessage(), ex);
                }
            });
        }
    }

}

注意:

  • AOP的方法逻辑就是将公共的操作代码提取出来;
  • AOP类需要在类上加上 @Component@Aspect 注解,其中component表示将当前AOP类交给spring容器管理,aspect用于声明当前类是一个AOP类;
  • 通过 pjp.getSignature() 获得的对象是连接点JointPoint对象,为什么是连接点对象而不是切入点对象?因为切入点是包含在连接点中的,有点类似于子类与父类的关系,编程时要依赖上层接口而不是具体实现类;
  • AOP类的方法的返回值必须为Object;
  • Around注解的方法必须在方法形参上声明ProceedJoinPoint对象,显示调用proceed方法才能执行原方法;

4、在需要的类/方法上添加注解

以controller的某一个方法为例

复制代码
    @AdminLog  //自定义注解
    @PostMapping("/addAdmin")
    public Result addAdmin(@RequestBody Admin admin) {
        log.info("新增管理员信息:admin={}", admin);
        adminService.addAdmin(admin);
        return Result.success();
    }
相关推荐
读书札记20221 小时前
C++ switch..case语句中变量跨域问题探讨及解决方法
开发语言·c++
ljt27249606611 小时前
Vue笔记(二)--组件的属性和方法
前端·vue.js·笔记
Sam_Deep_Thinking1 小时前
拼单功能的设计实战
java·架构
Boop_wu1 小时前
[Java项目] Spring Boot + WebSocket 实现网页在线聊天室|完整项目架构与实战讲解
spring boot·websocket·java-ee·mybatis
neo_Ggx231 小时前
Linux 日志检索速查:按时间、接口、Trace ID 查询完整请求链路
java·linux·服务器
ch.ju1 小时前
Java程序设计(第3版)第四章——什么是对象
java·开发语言
努力努力再努力wz1 小时前
【Redis入门系列】Redis基础命令详解:从客户端连接到数据读写、key 管理与过期机制
c语言·开发语言·数据结构·数据库·c++·redis·缓存
谙弆悕博士1 小时前
【附C源码】C语言实现散列表
c语言·开发语言·数据结构·算法·散列表·数据结构与算法
Lucky_ldy1 小时前
C语言学习:自定义类型-结构体
c语言·开发语言·学习