AOP面向切面(方法)编程

AOP面向切面(方法)编程

快速入门:以下示例是计算DeptServiceImpl每一个方法执行的时间

java 复制代码
package com.example.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.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect  标记这是一个切面类
@Component
public class MyAspect {
    // 提取公共的切入点表达式,这里表明DeptServiceImpl类中的所有方法
    @Pointcut("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    private void pt() {
    }

    @Around("pt()")
    public Object TimeAspect(ProceedingJoinPoint joinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info(joinPoint.getSignature() + "执行耗时:{}", end - begin);
        return result;
    }
}

效果如下

我们创建一个切面类,当目标对象(使用@Pointcut指定的对象)的方法被调用时,AOP框架(如Spring AOP)会生成目标对象的代理对象(Proxy)。这个代理对象会拦截对目标对象方法的调用,然后执行这个代理对象中的函数(称为通知),这个代理对象中的函数就是我们在切面类中定义的函数(例如这里的TimeAspect),当通过代理对象调用方法时,代理对象会先执行切面类中定义的通知(如前置通知后置通知,环绕通知等),这里的@Around就是环绕通知,然后再执行目标对象的原方法。而不是直接调用我们原来的函数,这样就可以在原来的函数执行前后插入我们自己的代码,这就是AOP的原理

执行流程:调用者 -> 代理对象 -> 切面类中的通知 -> 原方法 -> 切面类中的通知 -> 返回结果

再例如下面这些通知

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

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class AopTest{
    // 提取公共的切入点表达式,这里表明DeptServiceImpl类中的所有方法
    @Pointcut("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    private void pointcut() {
    }

    @Around("pointcut()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("around环绕通知");
        return joinPoint.proceed();
    }

    @Before("pointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        // 方法执行前的逻辑
        log.info("before前置通知");
    }

    @After("pointcut()")
    public void afterAdvice(JoinPoint joinPoint) {
        // 方法执行后的逻辑
        log.info("after后置通知");
    }

    @AfterReturning("pointcut()")
    public void afterReturningAdvice(JoinPoint joinPoint) {
        // 方法返回后的逻辑
        log.info("afterReturning返回通知");
    }

    @AfterThrowing("pointcut()")
    public void afterThrowingAdvice(JoinPoint joinPoint) {
        // 方法抛出异常后的逻辑
        log.info("afterThrowing异常通知");
    }

}

一、通知类型

AOP提供了五种通知方式,分别是:

  • @Around:环绕通知,在目标方法执行前后都执行,这里的前后通知不是执行两次,而是在Object result = joinPoint.proceed()前后都能添加逻辑,比较特殊
  • @Before:前置通知,在目标函数执行前被执行
  • @After :后置通知,在目标函数执行后执行,不论目标方法是否正常返回或抛出异常都会执行。
  • @AfterReturning :后置通知,在目标函数正常返回时(不报错)才执行,如果目标函数报错了就不执行
  • @AfterThrowing : 后置通知,只有在目标函数报错时才执行,如果不报错就不执行,与@AfterReturning相反

二、通知顺序

如果有多个切面类,则按切面类名排序

  • 前置通知:字母排序靠前的先执行
  • 后置通知:字母靠前的后执行

可以这么理解,这就是一个栈,排序靠前的先进栈,然后后出站,先进后出

  • 使用@Order(数字)放在切面类上来控制顺序
java 复制代码
@Order(2)
@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class AopTest

@Order(1)
@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class BopTest

三、切入点表达式

  • 基本概念

使用示例:

java 复制代码
@Pointcut("execution(* com.example.service.*.*(..))")
public void allMethodsInServicePackage() {
    // 切入点表达式匹配 com.example.service 包下的所有类的所有方法
}

@Pointcut("execution(* com.example.service.MyService.*(..))")
public void allMethodsInMyService() {
    // 切入点表达式匹配 com.example.service.MyService 类中的所有方法
}

@Pointcut("execution(* com.example.service.MyService.myMethod(..))")
public void specificMethod() {
    // 切入点表达式匹配 com.example.service.MyService 类中的 myMethod 方法
}

@Pointcut("execution(* com.example.service.MyService.myMethod(String, ..))")
public void specificMethodWithStringParam() {
    // 切入点表达式匹配 com.example.service.MyService 类中的 myMethod 方法,
    // 该方法的第一个参数类型是 String,其他参数任意
}

@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(org.springframework.transaction.annotation.Transactional)")
public void allTransactionalMethodsInServicePackage() {
    // 切入点表达式匹配 com.example.service 包下的所有标注了 @Transactional 注解的方法
}
  • 注意事项

注意,切入点表达式尽量缩小范围,范围过大会导致程序运行效率较低

通过上述切入点表达式,我们会发现execution在指定特定的多个方法时就比较麻烦,需要使用&&,||等,不利于使用,下面介绍更利于特定方法使用的方式

  • 使用自定义注释
java 复制代码
package com.example.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 {
}

创建方式:

我们只需要在目标方法上使用自定义注解,就能使用AOP代理了

  • 在切面类中指定自定义注释的全类名
java 复制代码
package com.example.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.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class MyAspect {
    @Around("@annotation(com.example.anno.MyLog)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        return result;
    }
}
  • 添加注释
java 复制代码
public class MyService {
    @MyLog
    public void method1() {
        // 方法实现
    }

    @MyLog
    public void method2() {
        // 方法实现
    }
}

四、连接点

由于@Around的使用比较特殊,只能通过ProceedingJoinPoint对象获取相关信息,而其他通知只能使用JoinPoint来获取

  • ProceedingJoinPoint
java 复制代码
@Around("execution(* com.itheima.service.DeptService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    // 获取目标类名
    String className = joinPoint.getTarget().getClass().getName();
    System.out.println("Target Class: " + className);

    // 获取目标方法签名
    Signature signature = joinPoint.getSignature();
    System.out.println("Method Signature: " + signature);

    // 获取目标方法名
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Method Name: " + methodName);

    // 获取目标方法运行参数
    Object[] args = joinPoint.getArgs();
    System.out.println("Method Arguments: " + Arrays.toString(args));

    // 执行原始方法,获取返回值
    Object res = joinPoint.proceed();
    System.out.println("Method Result: " + res);

    return res;
}
  • JoinPoint
java 复制代码
@Before("execution(* com.itheima.service.DeptService.*(..))")
public void beforeAdvice(JoinPoint joinPoint) {
    // 获取目标类名
    String className = joinPoint.getTarget().getClass().getName();
    System.out.println("Target Class: " + className);

    // 获取目标方法签名
    Signature signature = joinPoint.getSignature();
    System.out.println("Method Signature: " + signature);

    // 获取目标方法名
    String methodName = joinPoint.getSignature().getName();
    System.out.println("Method Name: " + methodName);

    // 获取目标方法运行参数
    Object[] args = joinPoint.getArgs();
    System.out.println("Method Arguments: " + Arrays.toString(args));
}

对比

  • JoinPoint 适用于所有类型的通知(前置通知、后置通知、返回通知、异常通知),但它没有 proceed() 方法,因此无法控制目标方法的执行。
  • ProceedingJoinPoint 继承自 JoinPoint,仅适用于环绕通知。它包含 proceed() 方法,可以在通知中执行目标方法,并且在方法执行的前后插入逻辑。

总结起来,JoinPointProceedingJoinPoint 都可以用来获取目标方法的各种信息,但只有 ProceedingJoinPoint 可以控制目标方法的执行。

使用示例:

  • 自定义注释
java 复制代码
package com.example.anno;

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 {
}
  • 记录员工操作日记
java 复制代码
package com.example.aop;

import com.alibaba.fastjson.JSONObject;
import com.example.mapper.OperateLogMapper;
import com.example.pojo.OperateLog;
import com.example.utils.JwtUtils;
import io.jsonwebtoken.Claims;
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 javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;

@Slf4j
@Aspect // 标记这是一个切面类
@Component
public class MyAspect {
    @Autowired
    private HttpServletRequest request;
    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(com.example.anno.MyLog)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取token
        String token = request.getHeader("token");
        // 解析token
        Claims claims = JwtUtils.parseJWT(token);
        // 获取用户id
        Integer operateUser = (Integer) claims.get("id");
        // 当前操作时间
        LocalDateTime operateTime = LocalDateTime.now();
        //操作的类名
        String className = joinPoint.getTarget().getClass().getName();
        //操作的方法名
        String methodName = joinPoint.getSignature().getName();
        //操作方法的参数
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);

        // 调用目标方法
        long begin = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        //操作的返回值
        String returnValue = JSONObject.toJSONString(result);
        //操作的耗时
        long costTime = end - begin;
        //记录日志
        OperateLog log = new OperateLog(null, operateUser, operateTime, className, methodName, methodParams, returnValue, costTime);
        operateLogMapper.insert(log);
        return result;
    }
}

效果图

相关推荐
希忘auto1 小时前
详解Servlet的使用
java·servlet·tomcat
ygl61503732 小时前
Vue3+SpringBoot3+Sa-Token+Redis+mysql8通用权限系统
java·spring boot·vue
Code哈哈笑2 小时前
【Java 学习】构造器、static静态变量、static静态方法、static构造器、
java·开发语言·学习
是老余2 小时前
Java三大特性:封装、继承、多态【详解】
java·开发语言
鸽鸽程序猿2 小时前
【JavaEE】Maven的介绍及配置
java·java-ee·maven
尘浮生3 小时前
Java项目实战II基于微信小程序的南宁周边乡村游平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·微信小程序·小程序·maven
耀耀_很无聊7 小时前
第1章 初识SpringMVC
java·spring·mvc
麻衣带我去上学7 小时前
Spring源码学习(一):Spring初始化入口
java·学习·spring
东阳马生架构7 小时前
MySQL底层概述—1.InnoDB内存结构
java·数据库·mysql
手握风云-8 小时前
数据结构(Java版)第一期:时间复杂度和空间复杂度
java·数据结构