👳我亲爱的各位大佬们好😘😘😘
♨️本篇文章记录的为 AOP的另类用法 (权限校验&&自定义注解) 相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉🙉。
♨️如果文章有什么需要改进的地方还请大佬不吝赐教❤️🧡💛
👨🔧 个人主页 : 阿千弟
🔥 上期内容👉👉👉 : “速通“ 老生常谈的HashMap [实现原理&&源码解读]
前言 :
告别了OOP编程, 迎来了一个新的AOP编程时代👍👍👍, 最近有同学问我AOP除了写日志还能干什么, 其实AOP能干的事情挺多的, 可能只是他们写的代码中暂时用不到. 其实如果当我们写一些简单的程序的时候, SpringSecurity完全用不到的时候, 就可以使用AOP与自定义注解来为角色的访问权限进行鉴定, 绝对比Security更轻量👉👉👉.
文章目录
- 前言 :
- 自定义注解
- AOP回顾
- 基本概念
- 声明一个切入点
- 切入点指示器(PCD)
- 基本通知
- 通知的参数
- AOP整合自定义注解校验接口权限
- 引入依赖
- 自定义注解
- 定义切面
- AopController
- 测试结果
自定义注解
我们在接触框架的时候经常会用到注解, 但是我们自己自定义注解的情况比较少, 一般都是配合切面编程使用
自定义一个注解非常简单, 它就和创建一个接口一样, 如果我们创建的是一个接口的话, 那么就用interface关键字表示, 如果我们是要自定义一个注解, 那么就在interface关键字前面加上一个@符号
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HasRole {
String name() default "";
String value() default "";
}
- 自定义注解里面还存在一些属性, 这些属性有点类似与我们的抽象方法, 是不能加方法体的, 加了是会报错的
- 自定义的属性是可以赋默认值的
// name的默认值是 ""
String name() default "";
-
它的返回类型必须是基础类型或者是String类型, 不可以是封装类型
-
@Target
标志自定义的注解的作用范围
类型 | 作用范围 |
---|---|
TYPE | 允许被修饰的注解作用在类, 接口和枚举上 |
FIELD | 允许作用在属性字段上 |
METHOD | 允许作用在方法上 |
PARAMETER | 允许作用在方法参数上 |
CONSTRUCTOR | 允许作用在构造器上 |
LOCAL_VARIABLE | 允许作用在本地局部变量上 |
ANNOTATION_TYPE | 允许作用在注解上 |
PACKAGE | 允许作用在包上 |
5. @Retention
标志自定义的作用是定义被它所注解的注解保留多久(生命周期)
一共有三种策略,定义在 RetentionPolicy 枚举中. 从注释上看:
类型 | 解释 |
---|---|
source | 注解只保留在源文件,当 Java 文件编译成 cláss 文件的时候,注解被遗弃;被编译器忽略 |
class | 注解被保留到 class 文件,但 jvm 加载 class 文件时候被遗弃,这是默认的生命周期 |
runtime | 注解不仅被保存到 class 文件中,jym 加载 class 文件之后,仍然存在 |
AOP回顾
基本概念
Aspect(切面)
: 一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。Join point(连接点 )
: 在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。Advice(通知)
: 在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括“around”、“before”和“after”等通知。 通知的类型将在后面部分进行讨论。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链。Pointcut(切入点 )
: 匹配连接点(Joinpoint)的断言。通知和一个【切入点表】达式关联,并在满足这个切入点的连接点上运行。 【切入点表达式如何和连接点匹配】是AOP的核心:Spring缺省使用AspectJ切入点语法。Introduction(引入)
: Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。Target object(目标对象)
:被一个或者多个切面(aspect)所通知(advise)的对象。也有人把它叫做 被通知(advised) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。AOP代理 AOP proxy
: 在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
8 .Weaving(织入)
: 把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象,这个过程叫织入。 这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。 Spring和其他纯Java AOP框架一样,在运行时完成织入。
声明一个切入点
【切入点确定感兴趣的连接点】,从而使我们能够控制通知何时运行。
切入点声明由两部分组成:包含【名称和方法签名】,以及确定我们感兴趣的方法执行的【切入点表达式】。
怎么确定一个方法:public void com.ddd.service.impl.*(…)
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
切入点指示器(PCD)
这里只先回顾一下这两个常用的注解
@execution
: (常用)用于匹配方法执行的连接点,这是在使用Spring AOP时使用的主要切入点指示符。(匹配方法)
模式 | 描述 |
---|---|
public * *(…) | 任何公共方法的执行 |
cn.javass…IPointcutService.*() | cn.javass包及所有子包下IPointcutService接口中的任何无参方法 |
cn.javass….*(…) | cn.javass包及所有子包下任何类的任何方法 |
cn.javass…IPointcutService.(*) | cn.javass包及所有子包下IPointcutService接口的任何只有一个参数方法 |
(!cn.javass…IPointcutService+).(…) | 非“cn.javass包及所有子包下IPointcutService接口及子类型”的任何方法 |
cn.javass…IPointcutService+.() | cn.javass包及所有子包下IPointcutService接口及子类型的的任何无参方法 |
cn.javass…IPointcut.test*(java.util.Date) | cn.javass包及所有子包下IPointcut前缀类型的的以test开头的只有一个参数类型为java.util.Date的方法,注意该匹配是根据方法签名的参数类型进行匹配的,而不是根据执行时传入的参数类型决定的.如定义方法:public void test(Object obj);即使执行时传入java.util.Date,也不会匹配的; |
@annotation
: (常用)于匹配当前执行方法持有指定注解的方法。(方法上的注解)
基本通知
注解 | 说明 |
---|---|
@Before | 前置通知,在被切的方法执行前执行 |
@After | 后置通知,在被切的方法执行后执行,比return更后 |
@AfterRunning | 返回通知,在被切的方法return后执行 |
@AfterThrowing | 异常通知,在被切的方法抛异常时执行 |
@Around | 环绕通知,这是功能最强大的Advice,可以自定义执行顺序 |
通知的参数
任何通知方法都可以声明一个类型为【org.aspectj.lang.JoinPoint】
的参数作为它的【第一个参数】(注意,around
通知需要声明类型为( ProceedingJoinPoint )
的第一个参数,它是【JoinPoint】
的一个子类。 【JoinPoint】
接口提供了许多有用的方法:
getArgs()
: 返回方法参数。getThis()
: 返回代理对象。getTarget()
: 返回目标对象。getSignature
(): 返回被通知的方法的签名。toString()
: 打印被建议的方法的有用描述。
AOP整合自定义注解校验接口权限
在我们的业务开发中, 常常会遇到这样的问题 : 每当我们进行一个对数据库的操作, 通常需要鉴权用户是否具有一个正确的访问权限, 然而每次都对不同的业务流程中添加相同的鉴权的执行流程, 势必会造成代码的冗余, 而且也不利于后期代码的拓展
这时候我们可以对鉴权的业务进行切点织入, 织入切面, 化繁为简
引入依赖
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
自定义注解
这里先模拟一个缓存的类, 因为在真实业务交易权限的过程中我们一般都是把查出的用户数据放在缓存中, 以至于不用每次使用用户信息时都需要从数据库中查询
public class CacheManager {
//保存用户和具有的角色之间对应关系
public static final Map<String, Set<String>> USER_ROLE_MAP = new HashMap<>();
static{
//用户zhangsan具有user和admin两个角色
Set<String> roleSet3 = Sets.newHashSet("admin","user");
USER_ROLE_MAP.put("zhangsan",roleSet3);
//用户lisi具有user一个角色
Set<String> roleSet4 = Sets.newHashSet("user");
USER_ROLE_MAP.put("lisi",roleSet4);
}
}
定义切面
【编写方法】声明一个切入点,该切入点在匹配连接点时“提供”‘Account’对象值,然后从通知中引用指定的切入点
@Component
@Aspect
public class AopConfig {
//定义一个切点(通过注解)
@Pointcut("@annotation(com.example.aopdemo.annotation.HasRole))")
public void pointcut(){}
//前置通知
@Before("pointcut()")
public void before(JoinPoint joinPoint){
System.out.println("before-------------");
//获取到HttpServletRequest,ThreadLocal
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
HttpServletRequest request = servletRequestAttributes.getRequest();
String username = request.getParameter("username");
//获取当前用户的角色集合
Set<String> userRoles = CacheManager.USER_ROLE_MAP.get(username);
//获取当前请求的方法上的注解hasRole中设置的角色
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
//反射获取当前被调用的方法
Method method = signature.getMethod();
//判断当前方法是否有hasRole注解
//如果有,判断是否用户具有注解属性中要求的角色
//如果没有hasRole注解,那么说明方法不需要判断用户的角色,可以匿名访问
HasRole hasRole = method.getDeclaredAnnotation(HasRole.class);
if(hasRole != null && (userRoles == null || !userRoles.contains(hasRole.value()))){
throw new RuntimeException("用户没有访问权限");
}
}
}
这里只是配置了一个固定的注解再在pointcut上,是不需要判断的。但是如果我们不是指定某一个具体的注解的话,那么需要增加判断。因为我们的前置通知里面除了判断接口权限,还可以做很多其他的事情
AopController
@RestController
public class AopController {
//访问这个方法需要用户具有admin的角色
@HasRole("admin")
@GetMapping("query1")
public String query1(){
return "query1 需要 admin 角色";
}
//访问这个方法需要用户具有user的角色
@HasRole("user")
@GetMapping("query2")
public String query2(){
return "query2 需要 user 角色";
}
//任何用户都可以访问
@GetMapping("query3")
public String query3(){
return "query3 可以匿名访问";
}
}
测试结果
测试1
因为我们本次的传参是username=lbw, 然而lbw并没有admin权限, 所以此时我们的AOP生效进行拦截
测试2
测试3
这里可以匿名访问是因为, 我们并没有在query3方法上添加@HasRole注解, 这时请求的参数不会走AOP, AOP拦截不生效, 所以可以匿名访问
如果这篇【文章】有帮助到你💖,希望可以给我点个赞👍,创作不易,如果有对Java后端或者对
spring
感兴趣的朋友,请多多关注💖💖💖
👨🔧 个人主页 : 阿千弟