SpringBoot:切面AOP详解及应用举例(配合自定义注解实现权限校验、日志记录)

1.理解AOP

1.1 什么是AOP

转载自大佬的文章https://blog.csdn.net/mu_wind/article/details/102758005?spm=1001.2014.3001.5506

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 的依赖。

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

2.1 配合自定义注解实现权限校验

  1. 自定义一个注解HasPermissions
  2. 创建一个切面类,切点设置为拦截所有标注HasPermissions注解的方法
  3. 将需要进行权限校验的接口加上@HasPermissions注解

1.自定义注解HasPermissions

/**
 * 权限注解
 */
@Target(ElementType.METHOD) // @Target注解用于定义注解的使用位置
@Retention(RetentionPolicy.RUNTIME) // @Retention注解用于指明修饰的注解的生存周期,即会保留到哪个阶段
public @interface HasPermissions {

  /*
   * 权限标识:用来关联角色
   * 如系统管理员角色 拥有 删除商品接口的权限
   * 表设计大致如下
   *
   * 角色ID     权限标识
   *  1        delete:commodity
   *  2        update:commodity
   *
   *  在权限切面逻辑里 通过 token 获取当前登录人信息
   *  查询当前登录人所有角色及拥有的权限
   *  然后和方法上此注解的值进行比较即可
   */
  String value();

}

2.创建第一个AOP切面类,,只要在类上加个 @Aspect 注解即可。@Aspect 注解用来描述一个切面类,定义切面类的时候需要打上这个注解。@Component 注解将该类交给 Spring 来管理。在这个类里实现第一步权限校验逻辑:

@Aspect // 切面 定义了通知和切点的关系
@Component
@Slf4j
// @Order(0) 切面类执行顺序由@Order注解管理,该注解后的数字越小,所在切面类越先执行。
public class PreAuthorizeAspect {

    // 定义一个切点:所有被HasPermissions注解修饰的方法会织入advice
    @Around("@annotation(com.kernel.spring.annotation.HasPermissions)")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        
		// JointPoint 对象很有用,可以用它来获取一个签名,
		// 利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs() 获取)等。

        // 获取方法上的HasPermissions注解信息
        HasPermissions annotation = method.getAnnotation(HasPermissions.class);
        
        /*
         * 注解值 如 delete:commodity
         * 这里可以从token里获取当前登录人的信息 然后查询角色及拥有的权限标识
         * 然后判断 delete:commodity 是否在所拥有的权限标识内
         * 存在则验证通过
         * 否则验证失败
         */
        String authority = annotation.value();
        
        // 验证失败
        // return RestResult.fail(ResultCode.PERMISSION_NO_ACCESS);
        
        // 验证通过执行方法
        return point.proceed();
    }
}

3.在需要加权限的接口上加上此注解

@Api(tags = "hello word")
@RestController
@RequestMapping("/hello")
public class HelloController {
    
    @PostMapping("/delete")
    @ApiOperation(value = "商品删除", notes = "商品删除")
    @HasPermissions("delete:commodity") // 拥有此权限标识的角色才可以访问
    public RestResult<String> delete() {

        return RestResult.success("success", ResultCode.SUCCESS);
    }
}

2.2 日志记录

使用AOP来记录系统请求日志

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

@Aspect // 切面 定义了通知和切点的关系
@Component
@Slf4j
// @Order(0) 切面类执行顺序由@Order注解管理,该注解后的数字越小,所在切面类越先执行。
public class SysLogAspect {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    // 定义一个切点 RestController注解修饰的类 下的所有方法
    @Pointcut("@within(org.springframework.web.bind.annotation.RestController)")
    private void logAdvicePointcut() {}

	// 环绕通知
    @Around("logAdvicePointcut()")
    public Object logAdvice(ProceedingJoinPoint joinPoint) throws Throwable {

        // 开始时间
        long startTime = System.currentTimeMillis();

        // 执行方法
        Object result = joinPoint.proceed();

        // 方法执行耗时
        long time =  System.currentTimeMillis() - startTime;

        // 记录日志
        recordLog(joinPoint,time,result);
        return result;
    }

    /**
     * 记录日志
     * @param joinPoint ProceedingJoinPoint
     * @param time 方法运行总时长
     * @param result 方法返回结果
     */
    private void recordLog(ProceedingJoinPoint joinPoint, long time, Object result) throws JsonProcessingException {
        // 获取签名
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        // 可以获得方法名 方法上的注解等信息
        Method method = methodSignature.getMethod();
        // 请求的方法名
        String methodName = method.getName();

        // 接口请求参数
        Object[] args = joinPoint.getArgs();

        // 返回结果
        String resultJsonStr = objectMapper.writeValueAsString(result);

        // 获取请求的 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:{},请求方法:{},参数:{},返回结果:{},耗时:{} ms",url,methodName,args,resultJsonStr,time);

    }
}

2.新建一个Controller

@Api(tags = "hello word")
@RestController
@RequestMapping("/hello")
public class HelloController {

    @PostMapping("/test")
    @ApiOperation(value = "测试接口", notes = "hello word")
    public RestResult<String> getHello() {

        return RestResult.success("hello", ResultCode.SUCCESS);
    }


    @PostMapping("/delete")
    @ApiOperation(value = "商品删除", notes = "商品删除")
    @HasPermissions("delete:commodity") // 拥有此权限标识的角色才可以访问
    public RestResult<String> delete() {

        return RestResult.success("success", ResultCode.SUCCESS);
    }
}

测试
在这里插入图片描述

3.AOP相关注解

上面的案例中,用到了诸多注解,下面针对这些注解进行详解。

3.1 @Pointcut

@Pointcut 注解,用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。

@Aspect
@Component
public class SysLogAspect {

    /**
     * 定义一个切面,拦截 com.kernel.spring.controller 包和子包下的所有方法
     */
    @Pointcut("execution(* com.kernel.spring.controller..*.*(..))")
    public void pointCut() {}
}

@Pointcut 注解指定一个切点,定义需要拦截的东西,这里介绍两个常用的表达式:一个是使用 execution(),另一个是使用 annotation()。

execution表达式:
以execution(* com.kernel.spring.controller….(…))表达式为例:

  • 第一个 * 号的位置:表示返回值类型,* 表示所有类型。
  • 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,在本例中指 com.kernel.spring.controller包、子包下所有类的方法。
  • 第二个 * 号的位置:表示类名,* 表示所有类。
  • (…):这个星号表示方法名, 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。

annotation() 表达式:
annotation() 方式是针对某个注解来定义切点,比如我们对具有 @PostMapping 注解(也可以是自定义注解)的方法做切面,可以如下定义切面:

@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {}

@within(注解类型全限定名)

@within(注解类型全限定名)匹配所有持有指定注解的类里面的方法, 即要把注解加在类上

3.2 @Around

@Around注解用于修饰Around增强处理,Around增强处理非常强大,表现在:

  1. @Around可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法。这个特性的实现在于,调用ProceedingJoinPoint参数的procedd()方法才会执行目标方法。
  2. @Around可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。

Around增强处理有以下特点:

  1. 当定义一个Around增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型(至少一个形参)。在增强处理方法体内,调用ProceedingJoinPoint的proceed方法才会执行目标方法:这就是@Around增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用ProceedingJoinPoint的proceed方法,则目标方法不会执行。
  2. 调用ProceedingJoinPoint的proceed方法时,还可以传入一个Object[ ]对象,该数组中的值将被传入目标方法作为实参——这就是Around增强处理方法可以改变目标方法参数值的关键。这就是如果传入的Object[ ]数组长度与目标方法所需要的参数个数不相等,或者Object[ ]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。

@Around功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturning就能解决的问题,就没有必要使用Around了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around增强处理了。
代码见AOP应用

3.3 @Before

@Before 注解指定的方法在切面切入目标方法之前执行,可以做一些 Log 处理,也可以做一些信息的统计,比如获取用户的请求 URL 以及用户的 IP 地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。

3.4 @After

@After 注解和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。

3.5 @AfterReturning

@AfterReturning 注解和 @After 有些类似,区别在于 @AfterReturning 注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:

@Aspect
@Component
@Slf4j
public class SysLogAspect {
    /**
     * 在上面定义的切面方法返回后执行该方法,可以捕获返回对象或者对返回对象进行增强
     * @param joinPoint joinPoint
     * @param result result
     */
    @AfterReturning(pointcut = "pointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {

        Signature signature = joinPoint.getSignature();
        String classMethod = signature.getName();
        log.info("方法{}执行完毕,返回参数为:{}", classMethod, result);
        // 实际项目中可以根据业务做具体的返回值增强
        log.info("对返回参数进行业务上的增强:{}", result + "增强版");
    }
}

需要注意的是,在 @AfterReturning 注解 中,属性 returning 的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning 方法中可以对返回值进行增强,可以根据业务需要做相应的封装

3.6 @AfterThrowing

当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing 属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。

@Aspect
@Component
@Slf4j
public class SysLogAspect {
    /**
     * 在上面定义的切面方法执行抛异常时,执行该方法
     * @param joinPoint jointPoint
     * @param ex ex
     */
    @AfterThrowing(pointcut = "pointCut()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        // 处理异常的逻辑
        log.info("执行方法{}出错,异常为:{}", method, ex);
    }
}

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值