65 切面AOP
切面基础概念
- AOP:Aspect Oriented Programming,面向切面编程。是通过预编译方式(aspectj)或者运行期动态代理(Spring)实现程序功能的统一维护的技术。
面试问题:
Spring的两大核心是什么:IoC和AOP
分别有什么作用
IoC:控制反转,目的用于解耦,底层使用的技术是反射+工厂模式
AOP:面向切面编程,目的是在不修改目标对象源码的情况下,进行功能增强,底层使用的是动态代理技术
- AOP作用:不修改源码的情况下,进行功能增强,通过动态代理实现的;
- AOP的底层是通过动态代理实现的。在运行期间,通过代理技术动态生成代理对象,代理对象方法执行时进行功能的增强介入,再去调用目标方法,从而完成功能增强。
- 常用的动态代理技术有:
- JDK的动态代理:基于接口实现的
- cglib的动态代理:基于子类实现的
- Spring的AOP采用了哪种代理方式?
- 如果目标对象有接口,就采用JDK的动态代理技术
- 如果目标对象没有接口,就采用cglib技术
-
AOP相关概念
-
目标对象(Target):要代理的/要增强的目标对象。
-
代理对象(Proxy):目标对象被AOP织入增强后,就得到一个代理对象
-
连接点(JoinPoint):能够被拦截到的点,在Spring里指的是方法。能增强的方法
-
切入点(PointCut):要对哪些连接点进行拦截的定义。要增强的方法
-
通知/增强(Advice):拦截到连接点之后要做的事情。如何增强,额外添加上去的功能和能力
-
切面(Aspect):是切入点和通知的结合。 告诉Spring的AOP:要对哪个方法,做什么样的增强
-
织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程
-
-
AOP通知类型
- @Before 前置通知 通知方法会在目标方法之前执行
- @After 后置通知 通知方法会在目标方法之后执行
- @Around 环绕通知 可以在目标方法之前和之后执行,可以更改方法执行之后的结果,自主性最大,需要掌握
- @AfterReturning 通知方法将在目标方法正常结束之后执行
- @AfterThrowing 通知方法会在目标方法爆出异常之后执行
切面应用
为了更好对AOP的使用有更深的了解,这里我们以下面这个场景进行实现:
现在在项目中有一些接口需要进行权限校验,即某些用户是无权访问这个接口的,如果无权限应进行拦截。同时这个接口很重要,应该避免某些人员进行恶意的刷接口,所以我们需要对这个接口进行防刷。对某些恶意刷的IP进行封禁,并进行限流,请你基于AOP来完成这个功能。
完整代码请查看:点击查看
注解
那么我们首先就需要写一个注解,来标识这种需要进行处理的接口。
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermission {}
这里介绍一下元注解的概念
@Retention
中文翻译为保留的意思,标明自定义注解的生命周期。
从编写Java代码到运行主要周期为源文件→ Class文件 → 运行时数据,@Retention则标注了自定义注解的信息要保留到哪个阶段,分别对应的value取值为SOURCE →CLASS→RUNTIME。
- SOURCE 源代码java文件,生成的class文件中就没有该信息了
- CLASS class文件中会保留注解,但是jvm加载运行时就没有了
- RUNTIME 运行时,如果想使用反射获取注解信息,则需要使用RUNTIME,反射是在运行阶段进行反射的
- 示例:当RentionPolicy取值为SOURCE时,Class文件中不会保留注解信息,而取值为CLASS时,Class反编译文件中则保留了注解的信息
- Source:一个最简单的用法,就是自定义一个注解例如@ThreadSafe,用来标识一个类时线程安全的,就和注释的作用一样,不过更引人注目罢了。
- Class:这个有啥用呢?个人觉得主要是起到标记作用,还没有做实验,例如标记一个@Proxy,JVM加载时就会生成对应的代理类。
- Runtime:反射实在运行阶段执行的,那么只有Runtime的生命周期才会保留到运行阶段,才能被反射读取,也是我们最常用的。
@Target
中文翻译为目标,描述自定义注解的使用范围,允许自定义注解标注在哪些Java元素上(类、方法、属性、局部属性、参数…)
切面
/*** @FileName SecurityAspect* @Description 权限校验 限流 防刷 AOP切面* @Author yaoHui* @date 2024-10-09**/
@Aspect
@Slf4j
@Component
public class SecurityAspect {// 用于记录每个IP的请求时间戳,用于限流private Map<String, List<Long>> requestTimestamps = new ConcurrentHashMap<>();// 用于封禁IP的列表private Set<String> bannedIps = ConcurrentHashMap.newKeySet();// 限流阈值private static final int MAX_REQUESTS = 10; // 最多允许的请求次数private static final long TIME_WINDOW_MS = 10 * 1000; // 时间窗口,10秒// 切入点 指定被注解CheckPermission标记的就是连接点@Pointcut("@annotation(com.fang.screw.communal.annotation.CheckPermission)")public void checkPermission(){}/**** @Description 对连接点进行业务扩展 进行鉴权 限流 防刷等操作* @param proceedingJoinPoint* @return {@link Object }* @Author yaoHui* @Date 2024/10/9*/@Around("checkPermission()")public Object doPermissionCheck(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();String ipAddress = request.getRemoteAddr();UserPO userPO = CurrentUserHolder.getUser();// 检查IP是否被封禁if (bannedIps.contains(ipAddress)) {return R.failed("该IP已被封禁,请稍后再试!");}// 非超级管理员不可以访问该接口if (userPO.getUserType() != 0){return R.failed("无访问权限!");}// 限流机制if (isRequestTooFrequent(ipAddress)) {bannedIps.add(ipAddress); // 封禁该IPlog.warn("IP {} 被封禁,因短时间内过多请求", ipAddress);return R.failed("请求过于频繁,请稍后再试!");}return proceedingJoinPoint.proceed();}/**** @Description 判断请求是否过于频繁* @param ipAddress* @return {@link boolean }* @Author yaoHui* @Date 2024/10/9*/private boolean isRequestTooFrequent(String ipAddress) {long currentTime = System.currentTimeMillis();requestTimestamps.putIfAbsent(ipAddress, new ArrayList<>());List<Long> timestamps = requestTimestamps.get(ipAddress);timestamps.add(currentTime);// 清理超过时间窗口的请求timestamps.removeIf(timestamp -> currentTime - timestamp > TIME_WINDOW_MS);return timestamps.size() > MAX_REQUESTS;}
}
权限注解的使用
/**** @Description 查询分类文章List* @return {@link PoetryResult<Map<Integer,List<ArticleVO>>> }* @Author yaoHui* @Date 2024/9/22*/@CheckPermission@GetMapping("/getListSortArticle")public R<Map<Integer, List<ArticleVO>>> getListSortArticle() {return articleService.getListSortArticle();}