Aop

Aop

面向切面,面向方面编程,其实就是面向特定的方法编程

优势

  • 代码无侵入
  • 减少重复代码
  • 提高开发效率
  • 维护方便

实现

动态代理是面向切面编程最主流的实现,而Spring AOP是spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程

  1. 导入Aop依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 编写Aop程序:针对特定方法根据业务需要进行编程
package com.springboot.AOP;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

//1.将该类交给IOC容器管理
@Component
//2.加入AOP类注解
@Aspect
public class TimeAspect {
    //指定作用范围
    @Around("com.springboot.AOP.TextAspect.pt()")//切入点表达式
    //记录方法运行时间
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("TimeAspect.checkTime()");
        //1. 记录开始时间
        long start = System.currentTimeMillis();
        //2.调用原始方法运行
        Object result = joinPoint.proceed();
        //3.记录结束时间
        long end = System.currentTimeMillis();
        //4.计算总耗时
        long time = end - start;
        System.out.println(joinPoint.getSignature() + ": " + time + "ms");
        //5.返回结果 相当于拦截 因此一定要将结果返回出去
        return result;
    }
}

通知类型

  • @Around环绕通知 :此注解标注的通知方法在目标方法前、后都被执行 程序发生异常环绕后通知不会执行 需要返回值
  • @Before前置通知 :此注解标注的通知方法在目标方法前执行
  • @After后置通知 :此注解标注的通知方法在目标方法后执行,无论是否有异常都会执行
  • @AfterReturning返回通知 :此注解标注的通知方法在目标方法正常返回后执行,有异常不会执行
  • @AfterThrowing异常通知 :此注解标注的通知方法在目标方法发生异常后执行

注意

  • @Around环绕通知需要自己调用 Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,并把结果返回
package com.springboot.AOP;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

//切入点通知类型测试
@Component
@Aspect
public class TextAspect {
    //抽离切入点表达式
    @Pointcut("execution(* com.springboot.Server.*.*(..))")
    public void pt() {
    }

    //前置通知
    @Before("pt()")
    public void before() {
        System.out.println("before");
    }

    //环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("around环绕通知前置");
        Object proceed = joinPoint.proceed();
        System.out.println("around环绕通知后置");
        return proceed;
    }

    //后置通知
    @After("pt()")
    public void after() {
        System.out.println("after");
    }

    //返回通知 有异常不会执行
    @AfterReturning("pt()")
    public void afterReturning() {
        System.out.println("afterReturning");
    }

    //异常通知 有异常才会执行
    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("afterThrowing");
    }
}

通知的执行顺序

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

执行顺序

不同切面类中,默认按切面类的类名字母排序

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

使用@Order注解指定通知的执行顺序

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

切入点表达式

  • 定义:描述切入点方法的一种表达式
  • 作用:主要用来决定项目中的那些方法需要加入通知

常见形式

  1. execution(...):根据方法的签名来匹配
  2. @annotation(...):根据方法上的注解来匹配

execution表达式

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

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

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

  • 访问修饰符:publicprotectedprivatedefault
  • 包名.类名:可省略
  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

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

  • *:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
execution(* com.springboot.Server.*.*(..)) //匹配com.springboot.Server包下的任意类的任意方法
  • ..:多个连续的任意符号,可以通配任意数量的包、类、方法名、参数
execution(* com.springboot.Server..*.*(..)) //匹配com.springboot.Server包及其子包下的任意类的任意方法

完整的切入点表达式

//方法参数需要使用全类名
//基于实现类
execution(public void com.springboot.Server.impl.UserServiceImpl.saveUser(java.lang.String))

//基于接口
execution(public void com.springboot.Server.UserService.saveUser(java.lang.String))

匹配两个切入点表达式

@Pointcut("execution(* com.springboot.Server.*.*(..)) || execution(* com.springboot.Server2.*.*(..))")

书写建议

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是 update开头
  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 .,使用*匹配单个包。

@annotation切入点表达式

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

  1. 自定义注解
package com.springboot.AOP;

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 MyLog {
}
  1. 绑定切入点
//1.将该类交给IOC容器管理
@Component
//2.加入AOP类注解
@Aspect
public class TimeAspect {
    //指定作用范围
    @Around("@annotation(com.springboot.AOP.MyLog)")//切入点表达式
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        ---
    }
}
  1. 使用
public class DeptServerImp implements DeptServer {
    //作用切入点
    @MyLog
    @Override
    public List<Dept> list() {
        return deptMapper.list();
    }
}

获取连接点的相关信息

  • 在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
  • 对于 @Around 通知,获取连接点信息只能使用 ProceedingJoinPoint获取
  • 对于其他四种通知,获取连接点信息只能使用 JoinPoint,它是 ProceedingJoinPoint 的父类型
package com.springboot.AOP;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

//切入点通知类型测试
@Component
@Aspect
public class TextAspect {
    //抽离切入点表达式
    @Pointcut("execution(* com.springboot.Server.*.*(..))")
    public void pt() {
    }

    //前置通知
    @Before("pt()")
    public void before() {
        System.out.println("before");
    }

    //环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取目标对象类名
        String className = joinPoint.getTarget().getClass().getName();
        //获取目标方法名
        String methodName = joinPoint.getSignature().getName();
        //获取目标传入的参数
        Object[] args = joinPoint.getArgs();
        System.out.println("参数" + Arrays.toString(args));
        //放行 目标方法执行
        joinPoint.proceed();
        //带参数
        Object proceed = joinPoint.proceed(args);
        return proceed;
    }

    //后置通知
    @After("pt()")
    public void after() {
        System.out.println("after");
    }

    //返回通知 有异常不会执行
    @AfterReturning("pt()")
    public void afterReturning() {
        System.out.println("afterReturning");
    }

    //异常通知 有异常才会执行
    @AfterThrowing("pt()")
    public void afterThrowing() {
        System.out.println("afterThrowing");
    }
}

使用切入点记录操作日志

package com.springboot.AOP;

import com.alibaba.fastjson.JSONObject;
import com.springboot.Mapper.EmpMapper;
import com.springboot.Mapper.LogMapper;
import com.springboot.Pojo.Emp;
import com.springboot.Pojo.MyLog;
import com.springboot.Utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;

//日志切面类
@Slf4j
@Component
//切入点
@Aspect
public class LogAsept {
    @Autowired
    private LogMapper logMapper;

    @Autowired
    private HttpServletRequest request;
    @Autowired
    private JwtUtils jwtUtils;
    @Autowired
    private EmpMapper empMapper;

    //连接点
    @Around("@annotation(com.springboot.AOP.MyLog)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //记录开始时间
        long start = System.currentTimeMillis();
        //获取操作人id-当前登录员工的·id
        //获取请求头中的jwt令牌
        String authorization = request.getHeader("Authorization");
        String token = authorization.replace("Bearer ", "");
        //解析jwt令牌
        String operateUserName = (String) jwtUtils.parseJWT(token).get("username");
        //通过用户名获取人员信息
        Emp emp = empMapper.findEmpByUsername(operateUserName);
        //获取人员id
        Integer operateUser = emp.getId();
        //获取操作时间
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String operateTime = LocalDateTime.now().format(dateTimeFormatter);
        //获取操作类名
        String className = joinPoint.getTarget().getClass().getName();
        //获取操作方法名
        String methodName = joinPoint.getSignature().getName();
        //获取操作参数
        String methodParams = Arrays.toString(joinPoint.getArgs());
        //获取返回值
        Object result = joinPoint.proceed();
        String returnValue = JSONObject.toJSONString(result);
        //获取操作状态
        long end = System.currentTimeMillis();
        //获取操作耗时
        long time = end - start;
        //记录操作日志
        MyLog myLog = new MyLog(operateUser, operateUserName, operateTime, className, methodName, methodParams, returnValue, time, (byte) 1, "123");
        logMapper.insert(myLog);
        return result;
    }
}

使用切入点自动插入实体参数

/**
 * 自定义切面 来实现公共字段自动填充
 */
@Aspect //加入切面注解
@Component //加入组件注解 将自定义切面加入到IOC容器中
@Slf4j
public class AutoFillAspect {
    /**
     * 加入切入点
     * 说明对哪些类的哪些方法进行拦截 哪些方法需要执行此方法
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    //execution(* com.sky.mapper.*.*(..)):匹配com.sky.mapper包及其子包下的所有方法
    //@annotation(com.sky.annotation.AutoFill) :匹配加了AutoFill注解的方法
    public void autoFillPointCut() {
    }

    //定义通知
    @Before("autoFillPointCut()")//指定切入点
    public void autoFill(JoinPoint joinPoint) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        log.info("开始进行公共字段填充");

        //获取当前被拦截的方法上加入@AutoFill注解的value值
        //获取方法签名对象 将对象强制转换为方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取注解
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
        //获取注解的操作类型
        OperationType value = autoFill.value();

        //获取到当前被拦截方法的参数--实体对象
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length == 0) return;
        //获取实体对象 约定方法的第一个参数必须为数据库实体对象
        Object entity = joinPoint.getArgs()[0];

        //为实体对象的公共字段赋值 通过反射
        //获取时间
        LocalDateTime now = LocalDateTime.now();
        //获取操作人员id
        Long currentId = BaseContext.getCurrentId();
        //通过反射得到setUpdateTime方法
        Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
        Method setUpdateUser = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
        //通过反射为对象属性赋值
        setUpdateTime.invoke(entity, now);
        setUpdateUser.invoke(entity, currentId);

        //根据不同的操作类型为对应实体的属性赋值
        if (value == OperationType.INSERT) {
            Method setCreateTime = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
            Method setCreateUser = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
            //通过反射为对象属性赋值
            setCreateTime.invoke(entity, now);
            setCreateUser.invoke(entity, currentId);
        }
    }
}
上次更新 2025/6/25 20:56:34