SpringAOP

一、AOP简介

**AOP:**Aspect Oriented Programming(面向切面编程、面向方面编程),就是面向特定方法编程。

**使用场景:**当部分业务方法运行较慢,要定位到 耗时较长的方法,此时需要统计每个方法消耗时长。

**原始方法:**我们要计算一个方法运行时长就在方法开头和结尾计时再做差即可

但这样做需要给每个方法都单独写,太耗时了。**为了减少重复代码,提高开发效率,维护方便,**Sspring中提供了AOP。

二、AOP基本使用

我们现在的需求是统计所有业务层的执行耗时。

**step1:**在pom.xml引入AOP依赖

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

**step2:**创建一个AOP类交给IOC容器管理,我们需要在AOP类的方法中实现下图功能

@Aspect标识当前是一个AOP类

@Component将该类交给IOC容器管理

@Around(指定执行AOP的方法)"execution(* com.itheima.service.impl.*.*(..))"

java 复制代码
package com.itheima.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 //表示当前是一个AOP类
@Component
public class RecordTimeAspect {

    @Around("execution(* com.itheima.service.impl.*.*(..))")// 表示com.itheima.service.impl包下的所有类的所有方法
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //1.记录开始时间
        long begin = System.currentTimeMillis();

        //2.执行原始方法
        Object result = pjp.proceed();// 因为原始方法返回结果可能各式各样,所以用Object接收

        //3.记录结束时间
        long end = System.currentTimeMillis();
        log.info("方法 {} 执行耗时:{}", pjp.getSignature(), end - begin);
        return result;
    }
}

三、AOP核心概念

连接点: JoinPoint,可以被AOP控制的方法

**通知:**Advice,指那些重复的逻辑,也就是共性功能

eg:我们上面案例每个方法都要进行开始和结束时间的记录

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

eg:@Around("execution(* com.itheima.service.impl.*.*(..))")切入点表达式

**切面:**Aspect,描述通知与切入点的对应关系

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

AOP执行流程

AOP方法的@Around指定了目标对象,会基于动态代理技术为目标对象生成一个代理对象,代理对象中的方法会根据通知 里的内容进行生成,只需要把通知里的joinPoint.proceed() 换成调用目标对象的方法即可,最后将代理对象注入IOC容器,最后运行实际调用的代理对象的方法

四、通知类型

|-----------------|--------------------------------------|
| Spring AOP 通知类型 ||
| @Around | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
| @Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
| @After | 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
| @AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
| @AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |

注意:

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

我们又发现,我们每次写切入点方法,都是重复的,因此Spring提供了**@pointCut**注解,该注解的作用是将公共的切入点表达式抽取出来,需要用到是引入该切入点表达式即可

java 复制代码
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.*.*(..))")
    private void pt(){}

    //前置通知
    @Before("pt()")
    public void before(){
        log.info("before....");
    }

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        log.info("around...");
        Object result = pjp.proceed();
        log.info("around....after....");
        return result;
    }

    @After("pt()")
    public void after() {
        log.info("after....");
    }

    //后置通知
    @AfterReturning("pt()")
    public void afterReturning() {
        log.info("afterReturning....");
    }

    @AfterThrowing("pt()")
    public void afterThrowing() {
        log.info("afterThrowing....");
    }
}

五、通知顺序

当有多个AOP执行时就需要分先后顺序,不同切面类中,默认按照切面类的类名字母排序:

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

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

如果我们想要控制通知的执行顺序有两种方法:

  • 修改切面类的类名(不推荐)
  • 使用Spring提供的@Order(数字):目标方法前的通知方法:数字小的先执行,目标方法后反之
java 复制代码
@Slf4j
@Component
@Aspect
@Order(2)  //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
    //前置通知
    @Before("execution(* com.itheima.service.*.*(..))")
    public void before(){
        log.info("MyAspect2 -> before ...");
    }

    //后置通知 
    @After("execution(* com.itheima.service.*.*(..))")
    public void after(){
        log.info("MyAspect2 -> after ...");
    }
}

六、切入点表达式

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

(一)execution

java 复制代码
execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、protected)

  • 包名.类名: 可省略

  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

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

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

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

注意: 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。就是简单的把两个切入表达式间加逻辑运算符

java 复制代码
execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))

但是execution切入点表达式,如果要匹配多个无规则的方法,需要很多个表达式,非常麻烦,所以我们有引入了一个注释**@annotation**

(二)annotation注解

我们可以自定义一个注解

**step1:**编写自定义注解

@Target指定注解的作用返回

@Retention只当注解的运行时机

step2:在业务类做为连接点的方法上添加自定义注解

七、连接点

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

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

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

八、案例

(一)需求

需求:将案例(Tlias智能学习辅助系统)中增、删、改相关接口的操作日志记录到数据库表中

操作日志信息包含:

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

采用哪种通知类型?

@Around环绕通知

切入点表达式该怎么写?

匹配增删改查方法,由于方法名定义的比较规范,分别为save、delete、update。

**step1:**我们需要一个存储操作日志的数据库表

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 '操作日志表';

**step2:**有了数据库表,我们就需要一个类来封装它

java 复制代码
package com.sjy.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; //操作耗时
}

**step3:**由于我们只需要增删改查操作进行日志输出,需要较为复杂的execution切入点表达式,所以我们可以使用注解的方式。我们来自定义一个注解。

java 复制代码
package com.sjy.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 {
}

step4:接下来要为增删改操作添加记录日志的操作,我们就需要定义一个AOP类。定义一个aroud方法连接点由注释决定,通知内容进行操作的信息获取并存入数据库

java 复制代码
package com.sjy.aop;

import com.sjy.mapper.OperateLogMapper;
import com.sjy.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 OperationAspect {

    @Autowired
    private OperateLogMapper operateLogMapper;

    // 环绕通知
    @Around("@annotation(com.sjy.anno.Log)")
    public Object around(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()); // 需要实现 getCurrentUserId 方法
        olog.setOperateTime(LocalDateTime.now());
        olog.setClassName(joinPoint.getTarget().getClass().getName());
        olog.setMethodName(joinPoint.getSignature().getName());
        olog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
        olog.setReturnValue(result.toString());
        olog.setCostTime(costTime);

        // 插入日志
        operateLogMapper.insert(olog);
        return result;
    }

    private Integer getCurrentUserId() {
        return 1;
    }
}
相关推荐
张涛酱1074562 小时前
Spring AI 2.0.0-M3 新特性解析:MCP核心集成与重大升级
java
小刘不想改BUG2 小时前
LeetCode 138.随机链表的复制 Java
java·leetcode·链表·hash table
NGC_66112 小时前
Java 死锁预防:从原理到实战,彻底规避并发陷阱
java·开发语言
卓怡学长2 小时前
m277基于java web的计算机office课程平台设计与实现
java·spring·tomcat·maven·hibernate
季明洵2 小时前
Java简介与安装
java·开发语言
沉鱼.442 小时前
枚举问题集
java·数据结构·算法
林夕sama2 小时前
多线程基础(五)
java·开发语言·前端
Zzxy2 小时前
HikariCP连接池
java·数据库
罗超驿2 小时前
Java数据结构_栈_算法题
java·数据结构·