1、理解AOP
AOP:Aspect Oriented Program:面向切面编程。面向切面编程解决的,实现的是将组件功能的扩展通过动态植入的方式添加进来(不是修改源码来实现添加,也不是通过继承父类,实现功能扩展)。
在我们的项目中,总会遇到各种各样的需求,比如鉴权,记录日志或者记录一些用户的访问情况等信息,若是用代码去解决,那就回产生极大的代码量而且还都是重复的代码,非常冗余且不利于维护。
用鉴权来说,基本上每个接口都需要进行鉴权,若是每个接口都写一个鉴权操作,那这工程量既大又无法让人接受。当然,我们也可以直接写一个工具类,来进行鉴权操作,这样,至少代码冗余和可维护性的问题得到了解决,但是每个业务方法中依然要依次手动调用这个工具类,还是很不方便的。那有没有更好的方式呢?当然是有的,那就是AOP,AOP可以将鉴权、记录日志或等代码完全提取出来,通过一个节点将代码植入进去,完成所需的功能。
2、AOP体系结构
aop主要做了以下三件事:
- 在哪里切入(即在哪个方法鉴权操作)
- 什么时候切入(即在方法执行前还是执行中进行鉴权)
- 切入后做什么(即做鉴权、记录日志等操作)
aop的体系结构如下:
- Pointcut:切点。决定将鉴权从哪里切入业务代码(即织入切面)
- 分为execution方式和annotation方式
- execution:可以用路径表达式指定有哪些类要织入切面
- annotation:可以指定被哪些注解修饰的代码织入切面
- 分为execution方式和annotation方式
- Advice:处理。包括处理时机和处理内容
- 处理时机就是在什么时候执行处理内容
- 处理内容就是要做什么事,比如鉴权
- Aspect:切面,即Pointcut和Advice
- JointPoint:连接点,是程序执行的一个点。例如一个方法的执行或者一个异常的处理
- 在 Spring AOP 中,一个连接点总是代表一个方法执行
- Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程
3、AOP实例
在开始实例之前,创建一个spring boot项目,然后引入aop依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建一个接口返回JSON体,用于返回JSON数据:
@Data
public class WebApiResult<T> {
private Boolean success;
private String message;
private int code;
private T data;
/**
* 操作成功
* @param message
* @param data
* @return
*/
public static WebApiResult ok(String message,Object data){
WebApiResult result = new WebApiResult();
result.setSuccess(true);
result.setMessage(message);
result.setCode(200);
result.setData(data);
return result;
}
/**
* 操作成功
* @param message
* @return
*/
public static WebApiResult ok(String message){
WebApiResult result = new WebApiResult();
result.setSuccess(true);
result.setMessage(message);
result.setCode(200);
result.setData(null);
return result;
}
/**
* 操作失败
* @param message
* @return
*/
public static WebApiResult error(String message){
WebApiResult result = new WebApiResult();
result.setSuccess(false);
result.setMessage(message);
result.setCode(0);
result.setData(null);
return result;
}
}
1、实例一
该例子为所有请求为get的接口都会打印出“get请求成功”。
1.创建一个切面类
只需要创建一个类,然后在类上加上**@Aspect
**注解即可。
@Aspect
注解用来描述一个切面类,定义切面类的时候需要打上这个注解。
@Component
注解将该类交给 Spring 来管理。在这个类里实现advice。
@Aspect
@Component
public class MyAop01 {
/**
* 定义一个切点:所有被GetMapping注解修饰的方法会织入advice
*/
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
private void myAop01Pointcut() {}
/**
* Before表示myAop01将在目标方法执行前执行
*/
@Before("myAop01Pointcut()")
public void myAop01(){
// 处理逻辑
System.out.println("get请求成功");
}
}
2.创建一个接口,并创建一个get请求
@RestController
@RequestMapping(value = "/demo")
public class DemoController {
@GetMapping(value = "/getTest")
public WebApiResult aopTest() {
return WebApiResult.ok("成功");
}
@PostMapping(value = "/postTest")
public WebApiResult aopTest2(@RequestParam("id") String id) {
return WebApiResult.ok("成功");
}
}
3.实例测试
启动项目,然后访问http://localhost:8080/demo/getTest,会发现控制台打印了上述的话;
访问http://localhost:8080/demo/postTest,控制台没有打印。
2、实例二
实例二做复杂一点,具体为:
- 创建一个注解:@MyAnnotation
- 创建一个切面类,切点为拦截被@MyAnnotation修饰的方法,获取到接口的参数,并进行鉴权
- 将@MyAnnotation标记在接口方法上
1.编写自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}
2.创建一个User类
@Data
public class User {
private Integer uid;
private String name;
}
3.创建一个切面类
@Aspect
@Component
@Order(1)
public class MyAop02 {
/**
* 定义一个切面,括号内写入自定义注解的路径
*/
@Pointcut("@annotation(com.gggd.aopdemo.anno.MyAnnotation)")
private void myAop02Pointcut() {
}
@Around("myAop02Pointcut()")
public Object myAop02(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===================第一个切面===================:");
//获取请求参数
Object[] objects = joinPoint.getArgs();
Integer uid = ((User) objects[0]).getUid();
String name = ((User) objects[0]).getName();
System.out.println("uid1->>>>>>>>>>>>>>>>>>>>>>" + uid);
System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);
// uid小于0则抛出非法uid的异常
if (uid < 0) {
return WebApiResult.error("非法uid");
}
//返回参数
return joinPoint.proceed();
}
}
4.创建接口方法
@PostMapping("test02")
@MyAnnotation
public WebApiResult demo2(@RequestBody User user){
return WebApiResult.ok("成功");
}
5.测试
先构造正常参数请求接口,会发现正常;
构造负数uid请求接口,会返回非法uid提示。
有人会问,如果我一个接口想设置多个切面类进行校验怎么办?这些切面的执行顺序如何管理?
很简单,一个自定义的AOP
注解可以对应多个切面类,这些切面类执行顺序由@Order
注解管理,该注解后的数字越小,所在切面类越先执行。
下面在实例中进行演示:
创建第二个AOP切面类,在这个类里实现第二步权限校验:
6.创建第二个切面类
@Aspect
@Component
@Order(0)
public class MyAop03 {
@Pointcut("@annotation(com.gggd.aopdemo.anno.MyAnnotation)")
private void myAop03Pointcut() {
}
@Around("myAop03Pointcut()")
public Object myAop03(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===================第二个切面===================:");
//获取请求参数
Object[] objects = joinPoint.getArgs();
Integer uid = ((User) objects[0]).getUid();
String name = ((User) objects[0]).getName();
System.out.println("uid1->>>>>>>>>>>>>>>>>>>>>>" + uid);
System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);
// name不是管理员则抛出异常
if (!name.equals("admin")) {
return WebApiResult.error("非法用户");
}
return joinPoint.proceed();
}
}
7.测试
构建两个参数都异常的请况,会发现接口返回了非法用户,而不是非法uid了,这就证明了是@Order(0)的切面先执行了。
4、AOP相关注解
1、@Pointcut
@Pointcut
注解,用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。
@Pointcut 注解指定一个切点,定义需要拦截的东西,这里介绍两个常用的表达式:一个是使用 execution()
,另一个是使用 annotation()
。
execution()表达式:
以 execution(* club.gggd.controller..*.*(..)))
表达式为例:
- 第一个 * 号的位置:表示返回值类型,* 表示所有类型
- 包名:表示需要拦截的包名,后面的两个点表示当前包和当前包的所有子包,在本例中指 club.gggd.controller 包、子包下所有类的方法
- 第二个 * 号的位置:表示类名,* 表示所有类
*(..)
:这个星号表示方法名,* 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
annotation() 表达式:
annotation()
方式是针对某个注解来定义切点,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {}
然后使用该切面的话,就会切入注解是 @PostMapping
的所有方法。这种方式很适合处理 @GetMapping、@PostMapping、@DeleteMapping
不同注解有各种特定处理逻辑的场景。还有就是如上面案例所示,针对自定义注解来定义切面。
2、@Around
@Around
注解用于修饰Around
增强处理,Around
增强处理非常强大,表现在:
- @Around可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法。这个特性的实现在于:调用ProceedingJoinPoint参数的procedd()方法才会执行目标方法。
- @Around可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。
Around
增强处理有以下特点:
-
当定义一个
Around
增强处理方法时,该方法的第一个形参必须是ProceedingJoinPoint
类型(至少一个形参)。在增强处理方法体内,调用ProceedingJoinPoint
的proceed
方法才会执行目标方法:这就是@Around
增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用ProceedingJoinPoint
的proceed
方法,则目标方法不会执行。 -
调用
ProceedingJoinPoint
的proceed
方法时,还可以传入一个Object[ ]
对象,该数组中的值将被传入目标方法作为实参——这就是Around
增强处理方法可以改变目标方法参数值的关键。这就是如果传入的Object[ ]
数组长度与目标方法所需要的参数个数不相等,或者Object[ ]
数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。 -
@Around
功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before
、AfterReturning
就能解决的问题,就没有必要使用Around
了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用Around
。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用Around增强处理了。
下面,在前面例子上做一些改造,来观察@Around
的特点。
自定义注解类不变。首先,定义接口类:
1.接口类新增方法
@PostMapping("test03")
@MyAnnotation
public WebApiResult demo3(@RequestBody User user){
return WebApiResult.ok("成功", user);
}
2.创建切面类
@Component
@Aspect
public class MyAop04 {
@Pointcut("@annotation(com.gggd.aopdemo.anno.MyAnnotation)")
private void myAop04Pointcut() {
}
@Around("myAop04Pointcut()")
public Object myAop04(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===================开始增强处理===================");
//获取请求参数
Object[] objects = joinPoint.getArgs();
Integer uid = ((User) objects[0]).getUid();
String name = ((User) objects[0]).getName();
System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + uid);
System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);
// 修改入参
User user = new User();
user.setUid(1000);
user.setName("张三");
objects[0] = user;
// 将修改后的参数传入
return joinPoint.proceed(objects);
}
}
3.测试
请求接口则发现参数变成了aop中设计的参数。
3、@Before
@Before
注解指定的方法在切面切入目标方法之前执行,可以做一些记录日志处理,也可以做一些信息的统计,比如获取用户的请求 URL以及用户的 IP 地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。例如下面代码:
@Aspect
@Component
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.gggd.aopdemo.anno.MyAnnotation)")
private void logAspectPointcut() {
}
/**
* 在上面定义的切面方法之前执行该方法
* @param joinPoint jointPoint
*/
@Before("logAspectPointcut()")
public void logAspect(JoinPoint joinPoint) {
log.info("====doBefore方法进入了====");
// 获取签名
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);
}
}
启动服务,随意请求一个被注解@MyAnnotation修饰的接口即可返回相应的日志。
JointPoint
对象很有用,可以用它来获取一个签名,利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs()
获取)等。
4、@After
@After
注解和 @Before
注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。
@Aspect
@Component
@Slf4j
public class LogAspectAfter {
/**
* 定义一个切面,拦截 club.gggd.controller 包下的所有方法
*/
@Pointcut("execution(* com.gggd.aopdemo.controller..*.*(..))")
public void pointCut() {}
/**
* 在上面定义的切面方法之后执行该方法
* @param joinPoint jointPoint
*/
@After("pointCut()")
public void doAfter(JoinPoint joinPoint) {
log.info("==== doAfter 方法进入了====");
Signature signature = joinPoint.getSignature();
String method = signature.getName();
log.info("方法{}已经执行完", method);
}
}
启动服务,随意请求一个controller包中的接口即可返回相应的日志。
5、@AfterReturning
@AfterReturning
注解和 @After
有些类似,区别在于 @AfterReturning
注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:
@Slf4j
@Aspect
@Component
public class AfterReturningDemo {
@Pointcut("@annotation(com.gggd.aopdemo.anno.MyAnnotation)")
public void pointCut() {}
/**
* 在上面定义的切面方法返回后执行该方法,可以捕获返回对象或者对返回对象进行增强
* @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 + "增强版");
//获取方法返回体
WebApiResult webApiResult = (WebApiResult) result;
//获取并修改数据
User u = (User) webApiResult.getData();
u.setName("李四");
}
}
启动服务,请求http://localhost:8080/demo/test03则发现返回的是李四。
需要注意的是,在 @AfterReturning
注解中,属性 returning
的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning
方法中可以对返回值进行增强,可以根据业务需要做相应的封装。
6、@AfterThrowing
当被切方法执行过程中抛出异常时,会进入 @AfterThrowing
注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing
属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。
@Aspect
@Component
@Slf4j
public class AfterThrowingDemo {
/**
* 定义一个切面,拦截 club.gggd.controller 包下的所有方法
*/
@Pointcut("@annotation(com.gggd.aopdemo.anno.MyAnnotation)")
public void pointCut() {}
/**
* 在上面定义的切面方法执行抛异常时,执行该方法
* @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.toString());
}
}
创建一个接口,为了测试,手动写一个运行时异常:
@PostMapping("test04")
@MyAnnotation
public WebApiResult demo4(@RequestBody User user){
String s = null;
s.length();
return WebApiResult.ok("成功", user);
}
启动服务,请求http://localhost:8080/demo/test04,即可打印异常。
7、切点指示器
Spring AOP借助AspectJ的切点表达式语言来定义Spring切面,下面是切点表达式中使用的指示器:
表达式 | 描述 |
---|---|
execution | 匹配方法执行的连接点,也就是哪些方法要应用切面 |
within | 用来限定连接点必须在确定的类型或包中 |
this | 用来限定连接点属于给定类型的一个实例(代理对象) |
target | 用来限定连接点属于给定类型的一个实例(被代理对象) |
args | 用来限定连接点,也就是方法执行时它的参数属于给定类型的一个实例 |
bean | 用来指定某个bean执行切面方法 |
@annotation | 用来限定连接点的方法拥有给定注解 |
@target | 用来限定连接点属于一个执行对象(被代理对象)所属的拥有给定注解的类 |
@args | 用来限定连接点,方法执行时传递的参数的运行时类型拥有给定注解 |
@within | 用来限定连接点属于拥有给定注解的类型中 |
execution
以 execution(* club.gggd.controller..*.*(..)))
表达式为例:
- 第一个 * 号的位置:表示返回值类型,* 表示所有类型
- 包名:表示需要拦截的包名,后面的两个点表示当前包和当前包的所有子包,在本例中指 club.gggd.controller 包、子包下所有类的方法
- 第二个 * 号的位置:表示类名,* 表示所有类
*(..)
:这个星号表示方法名,* 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
例:
@Pointcut("execution(* com.gggd.aopdemo.controller..*.*(..))")
public void pointCut() {}
within
within
指示器用来限定连接点必须在确定的类型或包中,即within
指示器只拦截确定的类型,也就是跟它的接口无关,定义什么类型就拦截什么类型。
例:
@Before("within(com.gggd.aopdemo.controller.FruitController)")
@Pointcut("within(com.gggd.aopdemo.controller.FruitController)")
需要注意的是:within()所指定的连接点最小范围只能是类,而execution()所指定的连接点可以大到包,小到方法入参。 所以从某种意义上讲,execution()函数功能涵盖了within()函数的功能
this
this
指示器用来限定连接点属于给定类型的一个实例(代理对象),不管方法是来源于哪个接口或类,只要代理对象的实例属于this
中所定义的类型,那么这个方法就会被拦截。
例:
@Before("this(com.gggd.aopdemo.controller.FruitController)")
target
target
指示器用来限定连接点属于给定类型的一个实例(被代理对象),这个指示器的语义跟this
指示器是很相似的,但是它们有一个重要的区别,this
中的实例指的是代理对象,而**target
中的实例指的是被代理对象**。
例:
@Before("target(com.gggd.aopdemo.controller.FruitController)")
args
定义包含args
指示器的切面,下面的切面表达式中额外定义了within
指示器,这个主要是为了缩小args
的使用范围。如果不加,Spring AOP会尝试去代理所有符合条件的对象,但是有些对象的访问会有限制,导致启动异常。这个也提醒我们使用AOP时必须要明确指定使用范围,否则会造成不可预料的错误。
@Before("within(com.gggd.aopdemo.controller.FruitController.*) && args(java.lang.Integer, java.lang.String)")
args
指示器用来限定连接点,也就是方法执行时它的参数属于给定类型的一个实例,例如上方的例子,只有在FruitController
类中,方法第一个参数为Integer
类型,第二个参数为String
类型的方法才会被拦截。
注意:args
指示器不但对参数类型有要求,而且还会对参数个数、定义顺序有要求。
bean
bean
用来对某个类进行拦截,也就是通过execution
指定某个方法时,可能指定的这个方法是接口里的,这个接口又被很多个类实现,此时就可以使用bean
对指定的类进行拦截。
例:
@Before("execution(com.gggd.aopdemo.server.Fruit.buy(..)) && bean(Apple)")
上方的例子中,Fruit
是一个接口,里面有一个方法为buy
,Apple
、Banana
分别实现了这个接口,例子中bean
指定了Apple
这个类,则在拦截时,只会拦截Apple
这个类的buy
方法。
@annotation
annotation()
方式是针对某个注解来定义切点,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:
@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {}
然后使用该切面的话,就会切入注解是 @PostMapping
的所有方法。这种方式很适合处理 @GetMapping、@PostMapping、@DeleteMapping
不同注解有各种特定处理逻辑的场景。还有就是如上面案例所示,针对自定义注解来定义切面。
@target
**@target
用来限定连接点属于一个执行对象(被代理对象)所属的拥有给定注解的类。**虽然@target
和target
名称相似,但是它们用法是完全不同的,前者针对注解,后置针对类型。
例:
@Before("within(com.gggd.aopdemo.server.Fruit.*) && @target(org.springframework.validation.annotation.Validated)")
@target
限定在一个执行对象的所属类,与它的父类接口无关,即@target
指定的注解只能放在需要被增强的方法所属的类中,若该类实现了某接口,将注解放在该接口上是不生效的。
@args
@args
用来限定连接点,方法执行时传递的参数的运行时类型拥有给定注解。这个给定的注解要么定义在方法参数的类型本身上,那么定义在的它的实现类上。比如RedAppleWeight
它虽然继承了AppleWeight
(拥有指定注解),但是它本身没有指定注解,相关方法不会被拦截。
例:
@Before("within(com.gggd.aopdemo.server.Fruit.*) && @args(com.fasterxml.jackson.databind.annotation.JsonDeserialize)")
@within
@within
用来限定连接点属于拥有给定注解的类型中。@target
针对执行对象所属的类,而@within
针对执行对象所属的类型。另外所执行的方法必须属于拥有给定注解的类型中,比如redApple.print();
被重写在RedApple
类中,redApple.print(11);
被定义在SweetFruit
类中,当这两个类拥有指定注解后方法执行时才会被拦截。
例:
@Before("within(com.gggd.aopdemo.server.Fruit.*) && @within(org.springframework.validation.annotation.Validated)")
5、自定义注解实现脱敏
引入依赖
<!--引入lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--引入validation,用于字段验证-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!--引入aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--引入lang3-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
编写User实体类
@Data
public class SysUser {
@NotNull(message = "uid不能为空")
private Integer uid;
private String name;
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "联系电话格式有误")
private String tel;
private String idCard;
}
编写脱敏类型枚举
public enum DesensitizeType {
PHONE("手机号"),
IDCARD("身份证"),
NAME("姓名");
private String value;
DesensitizeType(String value) {
this.value = value;
}
}
编写脱敏注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Desensitization {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Desensitize {
/**
* @return 需要脱敏的类型
*/
DesensitizeType type();
}
编写切面类
@Aspect
@Component
public class DesensitizationAspect {
/**
* 声明一个切入点
*/
@Pointcut("@annotation(com.gggd.aopdemo.anno.Desensitization)")
private void desensitizePointcut() {}
/**
* 切入操作
* @param result 方法的返回体
*/
@AfterReturning(pointcut = "desensitizePointcut()", returning = "result")
public void check(Object result){
try {
//获取方法返回体
WebApiResult webApiResult = (WebApiResult) result;
//获取需要脱敏的数据
Object o = webApiResult.getData();
//判断数据是否为集合
if(o instanceof Collection){
//为集合,则将集合数据取出进行脱敏
Collection collection = (Collection) o;
for(Object c : collection){
//开始脱敏
desensitize(c);
}
}else{
//不为集合,开始脱敏
desensitize(o);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 进行脱敏类型判断
* @param o
* @throws IllegalAccessException
*/
private void desensitize(Object o) throws IllegalAccessException{
//获取对象的全部字段
Field[] fields = o.getClass().getDeclaredFields();
//迭代字段
for(Field f : fields ){
//判断字段是否存在@Desensitize注解
if(f.isAnnotationPresent(Desensitize.class)){
//注解存在,设置字段为可修改
f.setAccessible(true);
//获取注解的类型值
Desensitize desensitize = f.getAnnotation(Desensitize.class);
String value = String.valueOf(desensitize.type());
//对值进行判断,启动相应的脱敏规则
switch (value){
case "PHONE" :
//对手机号进行脱敏
desensitizePhone(f, o);
break;
case "IDCARD" :
//对身份证进行脱敏
desensitizeIDCard(f, o);
break;
case "NAME" :
//对姓名进行脱敏
desensitizeName(f, o);
break;
default:
break;
}
}
}
}
/**
* 脱敏手机号
* @param f
* @param o
* @throws IllegalAccessException
*/
private void desensitizePhone(Field f, Object o) throws IllegalAccessException{
//获取字段的值
String phone = (String) f.get(o);
//判断值是否为空
if(StringUtils.isNotEmpty(phone)){
if(phone.length() == 11){
//通过正则表达式将手机号中间4位替换为*号
String newTel = phone.replaceAll("(\\w{3})\\w*(\\w{4})", "$1****$2");
//将值设置为修改后的值
f.set(o, newTel);
}
}
}
/**
* 脱敏身份证
* @param f
* @param o
* @throws IllegalAccessException
*/
private void desensitizeIDCard(Field f, Object o) throws IllegalAccessException{
//获取字段的值
String idCard = (String) f.get(o);
//判断值是否为空
if (StringUtils.isNotEmpty(idCard)) {
//对身份证号进行过脱敏操作
if (idCard.length() == 15){
idCard = idCard.replaceAll("(\\w{6})\\w*(\\w{3})", "$1******$2");
}
if (idCard.length() == 18){
idCard = idCard.replaceAll("(\\w{6})\\w*(\\w{3})", "$1*********$2");
}
//将值设置为修改后的值
f.set(o, idCard);
}
}
/**
* 脱敏姓名
* @param f
* @param o
* @throws IllegalAccessException
*/
private void desensitizeName(Field f, Object o) throws IllegalAccessException{
//获取字段的值
String name = (String) f.get(o);
//判断值是否为空且长度是否大于2位
if (StringUtils.isNotEmpty(name) && name.length() >= 2) {
//对姓名进行过脱敏操作
String newName = "";
if(name.length() == 2){
newName = StringUtils.left(name, 1) + "*";
}else{
String start = "";
for (int i = 0; i < name.length() - 2; i++) {
start += "*";
}
newName = name.replaceAll(name.substring(1, name.length() - 1), start);
}
//将值设置为修改后的值
f.set(o, newName);
}
}
}
编写接口类
@RestController
@RequestMapping("/desensitizationDemo")
public class SysUserController {
@GetMapping("desensitization")
@Desensitization
public WebApiResult<SysUser> check(@RequestBody SysUser u){
//测试集合
// List<SysUser> list = new ArrayList<>();
// list.add(u);
// SysUser u2 = new SysUser();
// BeanUtils.copyProperties(u, u2);
// u2.setUid(2);
// list.add(u2);
// SysUser u3 = new SysUser();
// BeanUtils.copyProperties(u, u3);
// u3.setUid(3);
// list.add(u3);
// return WebApiResult.ok("成功", list);
//测试对象
return WebApiResult.ok("成功", u);
}
}
开始测试
先在需要脱敏的实体类上加入注解:
@Data
public class SysUser {
@NotNull(message = "uid不能为空")
private Integer uid;
//脱敏注解,类型为:NAME
@Desensitize(type = DesensitizeType.NAME)
private String name;
//脱敏注解,类型为:PHONE
@Desensitize(type = DesensitizeType.PHONE)
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "联系电话格式有误")
private String tel;
//脱敏注解,类型为:IDCARD
@Desensitize(type = DesensitizeType.IDCARD)
private String idCard;
}
然后启动服务,
输入数据:
{
"uid":"1",
"name":"狗狗狗蛋儿",
"tel":"18333333333",
"idCard":"452123222222222222"
}
访问:http://localhost:8080/desensitizationDemo/desensitization
返回以下数据,脱敏成功。
{
"success": true,
"message": "成功",
"code": 200,
"data": {
"uid": 1,
"name": "狗***儿",
"tel": "183****3333",
"idCard": "452123*********222"
}
}
接下来将接口类中测试对象的代码注释掉,将测试集合的代码取消注释,重启服务,再次请求接口,返回以下数据,脱敏成功。
{
"success": true,
"message": "成功",
"code": 200,
"data": [
{
"uid": 1,
"name": "狗***儿",
"tel": "183****3333",
"idCard": "452123*********222"
},
{
"uid": 2,
"name": "狗***儿",
"tel": "183****3333",
"idCard": "452123*********222"
},
{
"uid": 3,
"name": "狗***儿",
"tel": "183****3333",
"idCard": "452123*********222"
}
]
}
6、升级注解脱敏
上面的脱敏注解虽然达到了脱敏的功能,但是接口的返回体是固定的,适配性不是很好,于是,我对注解进行了改造,只需要传入返回体的Class和所需要的字段,即可自动解析返回脱敏后的数据。
1、修改@Desensitization注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Desensitization {
/**
* @return 接口返回体的Class
*/
Class DataClass();
/**
* @return 选填,若为空则表示该返回体可直接进行脱敏,不为空则填入返回体中存放含有脱敏字段的类
*/
String field() default "";
}
2、在DesensitizationAspect中进行改造
1.新增方法isCollection(Object result)
/**
* 判断是否为集合并进行脱敏
* @param result
* @throws IllegalAccessException
*/
private void isCollection(Object result) throws IllegalAccessException{
//判断是否为集合
if(result instanceof Collection){
Collection collection = (Collection) result;
for (Object o : collection){
desensitize(o);
}
}else {
desensitize(result);
}
}
2.对check(Object result)
方法进行改造
public void check(JoinPoint joinPoint, Object result){
try {
//获取被切入的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取被切入方法上的注解
Desensitization annotation = signature.getMethod().getAnnotation(Desensitization.class);
//获取注解的value:field
String field = annotation.field();
//判断field是否为空,为空则返回的是一个对象或者集合
if("".equals(field)){
//判断是否为集合并脱敏
isCollection(result);
}else {
//获取注解的value:dataClass
Class dataClass = annotation.DataClass();
//获取字段列表
Field[] fields = dataClass.getDeclaredFields();
//创建一个标志位,判断是否存在需要脱敏的字段
boolean is = true;
for (Field f : fields) {
//获取到需要脱敏的字段
if(f.getName().equals(field)){
is = false;
f.setAccessible(true);
Object o = f.get(result);
//判断是否为集合并脱敏
isCollection(o);
}
}
//未找到需要脱敏的字段,抛出异常
if(is){
throw new RuntimeException("The field could not be found:" + field);
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
3.测试
回到controller层的接口中,由于对@Desensitization注解进行了改造,新增了两个值,所以注解上需要加上值。
@GetMapping("desensitization")
//需要加入两个值
@Desensitization(DataClass = WebApiResult.class, field = "data")
public WebApiResult<SysUser> check(@RequestBody SysUser u){
//测试集合
List<SysUser> list = new ArrayList<>();
list.add(u);
SysUser u2 = new SysUser();
BeanUtils.copyProperties(u, u2);
u2.setUid(2);
list.add(u2);
SysUser u3 = new SysUser();
BeanUtils.copyProperties(u, u3);
u3.setUid(3);
list.add(u3);
return WebApiResult.ok("成功", list);
//测试对象
// return WebApiResult.ok("成功", u);
}
启动服务,访问http://localhost:8080/desensitizationDemo/desensitization,发现依然可以进行脱敏。
接下来测试返回单个对象:
@GetMapping("check1")
//因为返回体是集合或者对象,所以不需要加field字段
@Desensitization(DataClass = User.class)
public User check1(@RequestBody User u){
return u;
}
启动服务,访问http://localhost:8080/desensitizationDemo/check1,依然可以脱敏。
再测试集合:
@GetMapping("check2")
//因为返回体是集合或者对象,所以不需要加field字段
@Desensitization(DataClass = User.class)
public List<User> check2(@RequestBody User u){
List<User> list = new ArrayList<>();
list.add(u);
User u2 = new User();
BeanUtils.copyProperties(u, u2);
u2.setUid(2);
list.add(u2);
User u3 = new User();
BeanUtils.copyProperties(u, u3);
u3.setUid(3);
list.add(u3);
return list;
}
启动服务,访问http://localhost:8080/desensitizationDemo/check2,依然可以脱敏。