Spring AOP是什么

  1. 最近负责开发一款内部人员使用的日志管理项目。其中涉及到了人员权限的校验问题。于是想到了用spring AOP的思路去实现,避免每次需要手动去添加代码校验。
  2. Spring AOP是什么,Aspect Oriented Programming, 面向切面编程,是Spring的核心之一。面向切面很明显就是空间意义上的拦截操作。
  3. 比如我需要在每个业务逻辑的前后做些事情,在每次接受请求的时候,写个日志
log.info("=======开始接受请求=======")
  1. 如果我希望在所有的接口请求请求的时候,都写这个日志。那么很明显,我总不能每个接口里面都加上这个日志输出代码。无疑是非常繁琐和重复的。而AOP可以很好的帮助我们去简化这个冗余的代码。
  2. 面向切面。如果说正常的业务逻辑是水平的,那么AOP就是垂直的。可以参考X-Y轴的概念。给每个业务逻辑纵向的扩展一些功能,将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,而这些功能很明显是可以公用的。
  3. 空间意义上的AOP:

    在执行正常的业务逻辑时,我们可以利用AOP进行纵向的扩展。而不影响它自己的业务逻辑功能。Spring有很多地方都采用了AOP的思想。
  4. 我的这个项目是判断用户的执行权限。具体业务逻辑:
    1)有操作人员进行用户权限配置的时候,删除了某人。往后台发出删除用户的请求。
    2)AOP将该请求拦截,判断该用户是否有权限做该操作。如果有,继续执行接受请求之后的方法,如果没有,则返回前端json,表示该用户无权限操作。

项目开发历程

说起来还是非常简单的。现在我开始说一下我的开发历程。

  1. Springboot项目中往前端返回特定json字符串的相关配置,以及自定义异常的拦截肯定是都要有的。
  2. 实现对权限的AOP控制,首先要有一个特殊标识符,不可能对所有的方法都进行权限控制,只对特定的方法进行权限控制。所以我先自定义了一个注解 Permission
一、自定义注解Permission
import java.lang.annotation.*;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 5:24 PM
 * @Desc: 自定义权限注解,用于AOP
 */

@Target( ElementType.METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Documented
public @interface Permission {
    
}
注解的注解:元注解

1. @TARGET

* 用于标注这个注解放在什么地方,类上,方法上,构造器上
 * ElementType.METHOD 用于描述方法
 * ElementType.FIELD 用于描述成员变量,对象,属性(包括enum实例)
 * ElementType.LOCAL_VARIABLE 用于描述局部变量
 * ElementType.CONSTRUCTOR 用于描述构造器
 * ElementType.PACKAGE 用于描述包
 * ElementType.PARAMETER 用于描述参数
 * ElementType.TYPE 用于描述类,接口,包括(包括注解类型)或enum声明

2.@Retention

*  用于说明这个注解的生命周期
 * RetentionPolicy.RUNTIME 始终不会丢弃,运行期也保留该注解。因此可以使用反射机制来读取该注解信息。
 * 我们自定义的注解通常用这种方式
 * RetentionPolicy.CLASS 在类加载的时候丢弃,在字节码文件的处理中有用。注解默认使用这种方式
 * RetentionPolicy.SOURCE 在编译阶段丢弃,这些注解在编译结束后就不再有任何意义,所以他们不会写入字节码中
 * @Override,@SuppressWarnings都属于这类注解。
 * 我们自定义使用中一般使用第一种
 * java过程为 编译-加载-运行

3. @Documented

* 将注解信息添加到文本中

本项目中的Permission注解用于方法,所以我在controller的特定需要权限控制的方法上添加该注解即可。

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;


/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 6:17 PM
 * @Desc: 用户Controller类
 */
@Slf4j
@RestController
@RequestMapping(value = "/user")
@Api(tags = "UserController")
public class UserController {
    
    @Permission
    @ApiOperation( value = "增加用户", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @RequestMapping(value = "/addUser",method = RequestMethod.POST)
    public Response<Void> addUser(@RequestBody AddUserRequestVO vo){
        /**
         * 具体代码实现逻辑省略
         */
        return new Response<>();
    }
}

我使用了swagger2,用于快速构建RESTFUL API,方便调试。后续我将会攥写有关swagger的配置。

4. RequestVO

该项目的权限控制只需要获取它的权限标识符和操作人员ID,然后调用写好的校验service去执行校验方法即可。
于是我定义了一个RequestVO

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 6:18 PM
 * @Desc: 权限控制的请求vo基类
 */
@Setter
@Getter
@NoArgsConstructor
public class RequestVO {

    /**
     * 权限标识符
     */
    private String authrity;

    /**
     * 操作人员ID
     */
    private Long adminId;
}

让所有的需要权限控制的接口,请求vo全部继承这个控制权限VO基类。
在获取拦截的方法中的参数之后,直接去调用service方法校验即可。

二、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.springframework.stereotype.Component;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 6:46 PM
 * @Desc:
 */
@Slf4j
@Aspect
@Component
@ResponseBody
public class PermissionAspect {
	@Autowired
    private CheckService checkService;

    @Around( value = "@annotation(permission)")
    public Response PermissionCheck(ProceedingJoinPoint joinPoint, Permission permission) throws Throwable{
        log.info("======开始权限校验======");

        //1.获取请求参数
        Object[] objects = joinPoint.getArgs();

        for(Object obj : objects){
            if (obj instanceof RequestVO){
                Long adminId = ((RequestVO) obj).getAdminId();
                String authority = ((RequestVO) obj).getAuthrity();

                //若校验失败,抛出自定义异常
                if (checkService.check( adminId,authority )){
                    log.info( "=======权限校验失败======" );

                    throw new BusinessException( "抱歉,您无该操作权限" );
                }

                log.info( "=======权限校验成功======" );
                //若校验成功,继续方法的执行,并获取返回结果,返回给前端
                try {
                    Object object = joinPoint.proceed();
                    if (object instanceof Response){
                        return (Response)object;
                    }

                } catch (Throwable throwable) {
                    /**
                     *  在方法执行过程中,捕获异常
                     *  如果捕获的是自定义异常,则取出内容并抛出
                     *  如果捕获的不是自定义异常,直接抛出
                     *  
                     */
                    if(throwable instanceof BusinessException){
                        throw new BusinessException( throwable.getMessage() );
                    }
                    throw new Exception( throwable );
                }
            }
        }
        return new Response(  );
    }
}

这一串代码基本上就是我项目中整个AOP的配置了。

  1. 基本注解
@Aspect : 将当前类标识为一个切面
@Component :肯定是必不可少的。让Spring容器扫描到。
@ResponseBody 在我的项目中,在后续的权限校验后,会返回特定的json对象给前端,所以此处加了该注解。视具体项目而论
  1. 方法注解
1)@Before 前置通知,在方法执行之前
2)@After 后置通知,在方法执行之后
3)@Around 环绕通知,在方法执行之前执行之后都可以。也是我的代码中使用的
里面的格式
	@Around( value = "@annotation(xxx)")即可 xxx为你的自定义注解名
使用该注解,方法参数中第一个参数必须是 ProceedingJoinPoint
4)@Pointcut 定义切点

3.通过 ProceedingJoinPoint获取方法参数

Object[] getArgs() 获取方法参数
Signature getSignature() :获取方法签名对象; (后跟.getName 即可获取方法名)
Object getTarget:获取目标对象
  1. 获取参数后,本来是用的
Arrays.asList(objects).stream().forEach( object -> {} );

可是由于其中不能直接方法返回,所以只能用for循环迭代数组。
如果有更好的实现方法,欢迎留言提出。
5. 直接校验权限,如果权限校验失败,直接返回自定义异常。如果校验成功,继续执行接口中的方法。

Object object = oinPoint.proceed();

该方法是需要加上try…catch的。可是加上去之后,默认catch的异常是
Throwable 。在接口方法具体实现中抛出的自定义异常,可能就无法被我的异常捕获器捕获。
所以先判断捕获的异常是否是自定义异常,如果是,就继续抛出我的自定义异常。如果不是,则抛出默认的Exception。

if(throwable instanceof BusinessException){
      throw new BusinessException( throwable.getMessage() );
  }
     throw new Exception( throwable );

Object即是接口方法继续执行后的返回值。
项目中我封装了一个Response,专门用于与前端交互。

import org.springframework.http.HttpStatus;

@Setter
@Getter
public class Response<T> {

    private String code;

    private String message;

    private String updateTime;

    private T body;


    public Response code(String code) {
        this.code = code;
        return this;
    }

    public Response body(T body) {
        this.body = body;
        return this;
    }

    public Response message(String message) {
        this.message = message;
        return this;
    }

    public Response time(String updateTime) {
        this.updateTime = updateTime;
        return this;
    }

    /**
     * 该构造方法默认code 为200
     */
    public Response() {
        this(HttpStatus.OK.name(), null);
    }

    /**
     * 该构造方法默认code 为200
     * @param body 需要返回的对象
     */
    public Response(T body) {
        this(HttpStatus.OK.name(), body);
    }

    public Response(String code, T body) {
        this(code, null, body);
    }

    public Response(String code, String message, T body) {
        this.code = code;
        this.body = body;
        this.message = message;
    }
}

我的所有接口返回值都是封装为Response,我只需要判断一下object是否是我的Response类,即可直接返回给前端。

Object object = joinPoint.proceed();
if (object instanceof Response){
   return (Response)object;
}

以上就是我项目中的AOP实现了。
通过自定义注解的方式,去动态的控制部分接口方法执行AOP权限控制。
总的来说,收获还是很大的。

后续:
1. 在研究了aop的注解之后,发现@Around并不适合我的这个权限校验。用@Before更加简单一些,也不需要再去处理方法处理后的情况。
于是修改了一下:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/4/18 6:46 PM
 * @Desc:
 */
@Slf4j
@Aspect
@Component
public class PermissionAspect {

    @Autowired
    private CheckService checkService;

    @Before( value = "@annotation(permission)")
    public void PermissionCheck(JoinPoint joinPoint, Permission permission){
        log.info("======开始权限校验======");

        //1.获取请求参数
        Object[] objects = joinPoint.getArgs();
        for(Object obj : objects){
            if (obj instanceof RequestVO){
                Long adminId = ((RequestVO) obj).getAdminId();
                String authority = ((RequestVO) obj).getAuthrity();

                //若校验失败,抛出自定义异常
                if (checkService.check( adminId,authority )){
                    log.info( "=======权限校验失败======" );
                    throw new BusinessException( "抱歉,您无该操作权限" );
                }

                log.info( "=======权限校验成功======" );
            }
        }
    }

}

直接用@Before只需要关心方法执行前的权限校验即可。后续的请求处理就不需要管了。

  1. @Pointcut的使用
    如果不使用自定义注解的方式去控制哪些方法或类经过你的aop控制,也可以直接定义Pointcut(切点)。
    代码如下:
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.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.Arrays;

/**
 * @Project: 
 * @Author: Mr_yao
 * @Date: 2019/1/28 6:13 PM
 * @Desc: AOP实现打印接口调用日志
 */

@Aspect
@Slf4j
@Component
public class LogAspect {

    /**
     * 定义切点,这是一个标记方法
     * com.xxx.xxx.service下的所有子包及方法
     */
    @Pointcut("execution( * com.xxx.xxx.service..*.*(..))")
    public void anyMethod() {
    }

    @Before( "anyMethod()" )
    public void Before(JoinPoint joinPoint){
        log.info( "========接受到请求========" );
    }

    @AfterReturning("anyMethod()")
    public void afterMethod(){
        log.info( "=======请求处理完毕========" );
    }

    @AfterThrowing("anyMethod()")
    public void afterThrowMethod(){
        log.info( "=======请求处理异常========" );
    }

   }

定义某一个地方为切点,里面的语法可以自己网上搜索,可以直接标识到某个包及包下的所有子类。只在你的切点范围内,会执行AOP对应操作。也是很方便的

这个时候 @Before @After中的注解范围就是你的切点方法了
@After 方法执行完后通知(不论是执行成功还是异常)
@AfterReturning 方法正常执行后通知
@AfterThrowing 方法抛出异常后通知

不管是用切点还是自定义注解的方式,都可以控制AOP执行的范围。视项目而定即可。

ProceedingJoinPoint extends JoinPoint

具体差异大家可以看源码。