SpringBoot 中AOP用法

aop作用


在开发中我们经常使用oop这种纵向结构来开发,但是却会出现一些横切的功能。譬如,日志记录的功能,我们需要在每个方法执行的详细信息通过日志记录,但是我们为每个方法去写日志,明显不合理。再如异常处理功能,我们需要在每个方法执行抛出的异常都专门处理都不合理。这样就需要AOP面向切面开发来处理横切问题

aop术语


  • 通知(advice):
    通知主要是定义切面是什么以及何时使用。
    Before:在接合点之前执行通知。
    AfterReturning:在接合点执行完成之后执行通知。
    AfterThrowing:如果从接合点抛出了任何异常,都执行通知。
    After:接合点执行完成之后,无论是否抛出了异常,都执行通知。
    Around:在接合点周围执行通知,意思就是可能在接合点之前执行,也可能在接合点之后执行。
  • 连接点(join point):
    意思就是代码中的点,在这个点上开始玩切面。效果肯定是向应用程序中插入额外的逻辑。
  • 切点(point cut):
    用来选择需要执行一个或者多个连接点的表达式。
  • 切面(aspect):
    切面就是切点和通知的结合。

通知注解


注解 用途
@Pointcut 定义切入点
@Before 目标方法执行之前执行
@After 目标方法执行之后必定执行,无论是否报错
@AfterReturning 目标方法有返回值且正常返回后执行
@AfterThrowing 目标方法抛出异常后执行
@Around 可以获取到目标方法的入参和返回值

maven导包


<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

定义切入点


利用execution表达式来给包、类、方法定义切入点。

// 解释:匹配cn.spy.service.impl.MyServiceImpl类下的所有方法
execution(* cn.spy.service.impl.MyServiceImpl.(..))
// 解释:匹配cn.spy.service.impl.MyServiceImpl类下的public void xx();方法。
execution(public void cn.spy.service.impl.MyServiceImpl.(..))
// 解释:匹配cn.spy.service.impl.MyServiceImpl类下第一个参数为String类型,无返回值的所有公共方法。
execution(public void cn.spy.service.impl.MyServiceImpl.*(String,..))

用法:

@Pointcut("execution(public String com.sunpeiyu.demoitem.controller.RedisController.testRequest())")

切入点对象JoinPoint


//获取目标方法的参数信息
Object[] obj = joinPoint.getArgs();
//AOP代理类的信息
joinPoint.getThis();
//代理的目标对象
joinPoint.getTarget();
//用的最多 通知的签名
Signature signature = joinPoint.getSignature();
//代理的是哪一个方法
System.out.println("代理的是哪一个方法"+signature.getName());
//AOP代理类的名字
System.out.println("AOP代理类的名字"+signature.getDeclaringTypeName());
//AOP代理类的类(class)信息
signature.getDeclaringType();
//获取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//请求
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
//会话
HttpSession session = (HttpSession) requestAttributes.resolveReference(RequestAttributes.REFERENCE_SESSION);
//响应
HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();
//获取请求参数
Enumeration<String> enumeration = request.getParameterNames();
Map<String,String> parameterMap = new HashMap<>();
while (enumeration.hasMoreElements()){
    String parameter = enumeration.nextElement();
    parameterMap.put(parameter,request.getParameter(parameter));
}
String str = JSON.toJSONString(parameterMap);
if(obj.length > 0) {
    System.out.println("请求的参数信息为:"+str);
}

案例1,给指定方法返回的字符串编码


@Slf4j
@Aspect
@Component
public class ExcrypResultAspect {
 
    @Pointcut("execution(public String com.sunpeiyu.demoitem.controller.RedisController.testRequest())")
    public void cutHttpRequest() {
 
    }
 
    @Around(value = "cutHttpRequest()")
    public Object encrypt(ProceedingJoinPoint joinPoint) throws Throwable {
        String targetResult = (String) joinPoint.proceed();
        log.info("=========================== targetResult = " + targetResult);
        byte[] encodeArr = Base64.getEncoder().encode(targetResult.getBytes(StandardCharsets.UTF_8));
        String base64Str = new String(encodeArr, StandardCharsets.UTF_8);
        log.info("=========================== base64Str = " + base64Str);
        return base64Str;
    }
}

结果:


案例2,自定义注解,标识方法,将方法返回值编码


// 自定义注解
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptResult {
 
    String rule() default "MD5";
}
 
// 切面实现
@Slf4j
@Aspect
@Component
public class ExcrypResultAspect {
 
    @Pointcut("@annotation(com.sunpeiyu.demoitem.aspect.EncryptResult)")
    public void cutHttpRequest() {
 
    }
 
    @Around(value = "cutHttpRequest()")
    public Object encrypt(ProceedingJoinPoint joinPoint) throws Throwable {
        String targetResult = (String) joinPoint.proceed();
        log.info("=========================== targetResult = " + targetResult);
        EncryptResult encryptResult = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(EncryptResult.class);
        String rule = encryptResult.rule();
 
        if (rule.equals("base64")) {
            byte[] encodeArr = Base64.getEncoder().encode(targetResult.getBytes(StandardCharsets.UTF_8));
            String base64Str = new String(encodeArr, StandardCharsets.UTF_8);
            log.info("=========================== base64Str = " + base64Str);
            return base64Str;
        }
 
        String md5Digest = MD5Utils.md5Digest(targetResult);
        log.info("=========================== md5Digest = " + md5Digest);
        return md5Digest;
    }
}
 
// 测试
@EncryptResult
@GetMapping("/testRequest")
public String testRequest() {
    return "success";
}

结果:

案例3,HttpServletRequest解密


// 请求体解码注解
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptResult {
 
}
 
// 切面方法
@Around(value = "@annotation(com.sunpeiyu.demoitem.aspect.DecryptResult)")
public Object decrypt() {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    byte[] httpBody = HttpUtils.getHttpBody(request);
    byte[] decodeArr = Base64.getDecoder().decode(httpBody);
    return new String(decodeArr, StandardCharsets.UTF_8);
}

AOP切面执行顺序


实验

  • controller
@Slf4j
@RequestMapping("/demo")
@RestController
public class DemoController {
 
    @GetMapping("/doAopTest")
    public String doAopTest() {
        log.info("==============> doAopTest");
        return "doAopTest";
    }
}
  • 切面
// ===== 切面1 =====
@Slf4j
@Order(1)
@Component
@Aspect
public class OneAspect {
 
    @Pointcut("execution(* cn.sunpy.demo.controller.DemoController.doAopTest())")
    public void cut() {
 
    }
 
    @Before("cut()")
    public void beforeExe() {
        log.info("==============> OneAspect.beforeExe");
    }
 
    @After("cut()")
    public void afterExe() {
        log.info("==============> OneAspect.afterExe");
    }
 
    @Around("cut()")
    public Object aroundExe(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("==============> OneAspect.aroundExe first");
        Object result = joinPoint.proceed();
        log.info("==============> OneAspect.aroundExe final");
        return result;
    }
 
    @AfterReturning("cut()")
    public void afterReturnExe() {
        log.info("==============> OneAspect.afterReturnExe first");
    }
 
    @AfterThrowing("cut()")
    public void afterThrowing() {
        log.info("==============> OneAspect.afterThrowing first");
    }
}

// ===== 切面2 =====
@Slf4j
@Order(2)
@Component
@Aspect
public class TwoAspect {
 
    @Pointcut("execution(* cn.sunpy.demo.controller.DemoController.doAopTest())")
    public void cut() {
 
    }
 
    @Before("cut()")
    public void beforeExe() {
        log.info("==============> TwoAspect.beforeExe");
    }
 
    @After("cut()")
    public void afterExe() {
        log.info("==============> TwoAspect.afterExe");
    }
 
    @Around("cut()")
    public Object aroundExe(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("==============> TwoAspect.aroundExe first");
        Object result = joinPoint.proceed();
        log.info("==============> TwoAspect.aroundExe final");
        return result;
    }
 
    @AfterReturning("cut()")
    public void afterReturnExe() {
        log.info("==============> TwoAspect.afterReturnExe first");
    }
 
    @AfterThrowing("cut()")
    public void afterThrowing() {
        log.info("==============> TwoAspect.afterThrowing first");
    }
}

AOP中异常处理

  • Around处理异常

捕获目标方法的抛出的异常

@Aspect
@Component
public class ExceptionHandlingAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object handleExceptions(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            // 执行目标方法
            return joinPoint.proceed();
        } catch (Exception e) {
            // 捕获异常并处理
            System.err.println("Exception occurred: " + e.getMessage());
            // 可以选择重新抛出异常、返回默认值或执行其他逻辑
            throw e; // 重新抛出异常
        }
    }
}

AOP失效场景


1、同一个类中this调用被AOP拦截的方法

  • 解决方法:
    开启CGLIB代理(springboot默认开启)。
    使用代理类去调用。
((MyService) AopContext.currentProxy()).anotherMethod();

2、目标方法为final类型

不要在被代理的类中定义 public final 方法,因为这些方法无法被代理。

3、代理类为final类型

CGLIB 代理会继承目标类,因此目标类不能是 final 类型,否则无法生成代理类。确保目标类不是 final 类型

AOP源码


https://www.jianshu.com/p/0b3f6d5fe5bd

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容