Spring AOP代码实现:实例演示与注解全解

2023-05-16

1 理解AOP

1.1 什么是AOP

AOP(Aspect Oriented Programming),面向切面思想,是Spring的三大核心思想之一(两外两个:IOC-控制反转、DI-依赖注入)。

那么AOP为何那么重要呢?在我们的程序中,经常存在一些系统性的需求,比如权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。例如下面这个示意图:
在这里插入图片描述有多少业务操作,就要写多少重复的校验和日志记录代码,这显然是无法接受的。当然,用面向对象的思想,我们可以把这些重复的代码抽离出来,写成公共方法,就是下面这样:
在这里插入图片描述这样,代码冗余和可维护性的问题得到了解决,但每个业务方法中依然要依次手动调用这些公共方法,也是略显繁琐。有没有更好的方式呢?有的,那就是AOP,AOP将权限校验、日志记录等非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中:
在这里插入图片描述

1.2 AOP体系与概念

简单地去理解,其实AOP要做三类事:

在哪里切入,也就是权限校验等非业务操作在哪些业务代码中执行。
在什么时候切入,是业务代码执行前还是执行后。
切入后做什么事,比如做权限校验、日志记录等。
因此,AOP的体系可以梳理为下图:
在这里插入图片描述一些概念详解:

  • Pointcut:切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution方式和annotation方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
  • Advice:处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
  • Aspect:切面,即Pointcut和Advice。
  • Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
  • Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。

2 AOP实例

实践出真知,接下来我们就撸代码来实现一下AOP。

  • 使用 AOP,首先需要引入 AOP 的依赖。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.1 第一个实例

接下来,我们先看一个极简的例子:所有的get请求被调用前在控制台输出一句"get请求的advice触发了"。

具体实现如下:

  • 1.创建一个AOP切面类,只要在类上加个 @Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里实现advice:
package com.jingudi.framework.log.log.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @author 1060785272@qq.com
 * @version 1.0
 * @description: TODO
 * @date 2022-4-10 下午 9:31
 */
@Aspect
@Component
public class LogAdvice {
    // 定义一个切点:所有被GetMapping注解修饰的方法会织入advice
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    private void logAdvicePointcut(){}

    @Before("logAdvicePointcut()")
    public void logAdvice(){
        // 这里只是一个示例,你可以写任何处理逻辑
        System.out.println("get请求的advice触发了");
    }
}

  • 2.随便创建一个接口类,内部创建一个get请求
    (必须要有@GetMapping):
@ApiOperation("查询用户列表")
@GetMapping
public PageResult<UserEntity> getUserList(QueryUserVo vo) {
    return userService.getUserList(vo);
}

在这里插入图片描述

2.2 第二个实例

下面我们将问题复杂化一些,该例的场景是:

自定义一个注解PermissionsAnnotation
创建一个切面类,切点设置为拦截所有标注PermissionsAnnotation的方法,截取到接口的参数,进行简单的权限校验
将PermissionsAnnotation标注在测试接口类的测试接口test上
具体的实现步骤:

  • 1.使用@Target、@Retention、@Documented自定义一个注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionAnnotation{
}
  • 2.创建第一个AOP切面类,,只要在类上加个@Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里实现第一步权限校验逻辑:
package com.jingudi.advice;

import com.alibaba.fastjson.JSONObject;
import com.jingudi.modules.system.dto.DictDetailDto;
import lombok.extern.log4j.Log4j;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @author 1060785272@qq.com
 * @version 1.0
 * @description: TODO
 * @date 2022-4-10 下午 9:56
 */
@Aspect
@Component
@Order(1)
@Slf4j
public class PermissionFirstAdvice {
    // 定义一个切面,括号内写入第1步中自定义注解的路径
    @Pointcut("@annotation(com.jingudi.annotation.PermissionsAnnotation)")
    private void permissionCheck() {
    }

    @Before("permissionCheck()")
    public void beforeAdvice(JoinPoint joinPoint){
        // 这里只是一个示例,你可以写任何处理逻辑
        System.out.println("---------Before触发了----------");
        // 获取签名
        Signature signature = joinPoint.getSignature();
        // 获取切入的包名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取即将执行的方法名
        String funcName = signature.getName();
        log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);

        // 也可以用来记录一些信息,比如获取请求的 URL 和 IP
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取请求 URL
        String url = request.getRequestURL().toString();
        // 获取请求 IP
        String ip = request.getRemoteAddr();
        log.info("用户请求的url为:{},ip地址为:{}", url, ip);
    }

    @Around("permissionCheck()")
    public Object permissionCheckFirst(ProceedingJoinPoint joinPoint) throws Throwable {
        // 这里只是一个示例,你可以写任何处理逻辑
        System.out.println("---------Around触发了----------");
        //获取请求参数,详见接口类
        Object[] objects = joinPoint.getArgs();
        System.out.println(objects);
//        Integer id = ((JSONObject) objects[0]).getInteger("id");
        DictDetailDto object1 = (DictDetailDto)objects[0];

        // 修改入参
        JSONObject object = new JSONObject();

        return joinPoint.proceed(objects);
    }



    @AfterReturning(pointcut  = "permissionCheck()", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result){
        // 这里只是一个示例,你可以写任何处理逻辑
        System.out.println("---------AfterReturning触发了----------");
        Signature signature = joinPoint.getSignature();
        String classMethod = signature.getName();
        log.info("方法{}执行完毕,返回参数为:{}", classMethod, result);
        // 实际项目中可以根据业务做具体的返回值增强
        log.info("对返回参数进行业务上的增强:{}", result + "增强版");
    }

    @AfterThrowing(pointcut = "permissionCheck()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        // 处理异常的逻辑
        log.info("执行方法{}出错,异常为:{}", method, ex);
    }

    @After("permissionCheck()")
    public void afterAdvice(JoinPoint joinPoint){
        // 这里只是一个示例,你可以写任何处理逻辑
        System.out.println("---------After触发了----------");
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        log.info("方法{}已经执行完", method);
    }
}

@Order(0)
AOP加载顺序(切面加载顺序)

总结:

  1. 前置通知
    在目标方法执行之前执行执行的通知。

  2. 环绕通知
    在目标方法执行之前和之后都可以执行额外代码的通知。

  3. 后置通知
    在目标方法执行之后执行的通知。

  4. 异常通知
    在目标方法抛出异常时执行的通知。

  5. 最终通知
    是在目标方法执行之后执行的通知。

以上5种都可以额外接收一个JoinPoint参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数。

五种通知的执行顺序:

  1. 在目标方法没有抛出异常的情况下
    前置通知
    环绕通知的调用目标方法之前的代码
    目标方法
    环绕通知的调用目标方法之后的代码
    后置通知
    最终通知

  2. 在目标方法抛出异常的情况下
    前置通知
    环绕通知的调用目标方法之前的代码
    目标方法
    抛出异常
    异常通知
    最终通知

  3. 如果存在多个切面
    多切面执行时,采用了责任链设计模式。
    切面的配置顺序决定了切面的执行顺序,多个切面执行的过程,类似于方法调用的过程,在环绕通知的proceed()执行时,
    去执行下一个切面或如果没有下一个切面执行目标方法,从而达成了如下的执行过程:
    在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring AOP代码实现:实例演示与注解全解 的相关文章

随机推荐