AOP面向切面(方法)编程

作者 : admin 本文共7898个字,预计阅读时间需要20分钟 发布时间: 2024-06-16 共1人阅读

AOP面向切面(方法)编程

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

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;
    }
}

效果如下

AOP面向切面(方法)编程插图

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

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

再例如下面这些通知

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相反

二、通知顺序

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

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

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

AOP面向切面(方法)编程插图(1)

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

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

AOP面向切面(方法)编程插图(2)

三、切入点表达式

  • 基本概念

AOP面向切面(方法)编程插图(3)

使用示例:

@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 注解的方法
}

  • 注意事项

AOP面向切面(方法)编程插图(4)

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

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

  • 使用自定义注释
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面向切面(方法)编程插图(5)

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

  • 在切面类中指定自定义注释的全类名
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;
    }
}

  • 添加注释
public class MyService {
    @MyLog
    public void method1() {
        // 方法实现
    }

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

四、连接点

AOP面向切面(方法)编程插图(6)

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

  • ProceedingJoinPoint
@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
@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 可以控制目标方法的执行。

使用示例:

  • 自定义注释
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 {
}

  • 记录员工操作日记
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;
}
}

效果图
AOP面向切面(方法)编程插图(7)

本站无任何商业行为
个人在线分享 » AOP面向切面(方法)编程
E-->