Spring AOP是什么
- 最近负责开发一款内部人员使用的日志管理项目。其中涉及到了人员权限的校验问题。于是想到了用spring AOP的思路去实现,避免每次需要手动去添加代码校验。
- Spring AOP是什么,Aspect Oriented Programming, 面向切面编程,是Spring的核心之一。面向切面很明显就是空间意义上的拦截操作。
- 比如我需要在每个业务逻辑的前后做些事情,在每次接受请求的时候,写个日志
log.info("=======开始接受请求=======")
- 如果我希望在所有的接口请求请求的时候,都写这个日志。那么很明显,我总不能每个接口里面都加上这个日志输出代码。无疑是非常繁琐和重复的。而AOP可以很好的帮助我们去简化这个冗余的代码。
- 面向切面。如果说正常的业务逻辑是水平的,那么AOP就是垂直的。可以参考X-Y轴的概念。给每个业务逻辑纵向的扩展一些功能,将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,而这些功能很明显是可以公用的。
- 空间意义上的AOP:
在执行正常的业务逻辑时,我们可以利用AOP进行纵向的扩展。而不影响它自己的业务逻辑功能。Spring有很多地方都采用了AOP的思想。 - 我的这个项目是判断用户的执行权限。具体业务逻辑:
1)有操作人员进行用户权限配置的时候,删除了某人。往后台发出删除用户的请求。
2)AOP将该请求拦截,判断该用户是否有权限做该操作。如果有,继续执行接受请求之后的方法,如果没有,则返回前端json,表示该用户无权限操作。
项目开发历程
说起来还是非常简单的。现在我开始说一下我的开发历程。
- Springboot项目中往前端返回特定json字符串的相关配置,以及自定义异常的拦截肯定是都要有的。
- 实现对权限的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的配置了。
- 基本注解
@Aspect : 将当前类标识为一个切面
@Component :肯定是必不可少的。让Spring容器扫描到。
@ResponseBody 在我的项目中,在后续的权限校验后,会返回特定的json对象给前端,所以此处加了该注解。视具体项目而论
- 方法注解
1)@Before 前置通知,在方法执行之前
2)@After 后置通知,在方法执行之后
3)@Around 环绕通知,在方法执行之前执行之后都可以。也是我的代码中使用的
里面的格式
@Around( value = "@annotation(xxx)")即可 xxx为你的自定义注解名
使用该注解,方法参数中第一个参数必须是 ProceedingJoinPoint
4)@Pointcut 定义切点
3.通过 ProceedingJoinPoint获取方法参数
Object[] getArgs() 获取方法参数
Signature getSignature() :获取方法签名对象; (后跟.getName 即可获取方法名)
Object getTarget:获取目标对象
- 获取参数后,本来是用的
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只需要关心方法执行前的权限校验即可。后续的请求处理就不需要管了。
- @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
具体差异大家可以看源码。
第一次写博客,各位大牛多多包涵哈。欢迎留言评论?