文章目录
SpringAOP
1 概念
Spring AOP(Aspect Oriented Programming,面向切面编程)是Spring框架中的一个模块,它提供了面向切面编程的支持。AOP允许开发者将横切关注点(如日志记录、权限检查、性能监控等)从业务逻辑中分离出来,通过这种方式,可以使代码更加模块化,易于维护和管理。
核心概念:
- 切面(Aspect):切面是跨越多个对象的行为或关注点的模块化。例如,事务管理就是企业级应用中的一个关注点,它需要跨越多个对象进行管理。
- 连接点(Joinpoint):在程序执行过程中某个特定的点,比如方法的调用或者异常的抛出。在Spring AOP中,连接点总是方法的执行。
- 切入点(Pointcut):定义了切面应该在哪些连接点上执行的规则。可以通过表达式来匹配哪些方法需要被切面所影响。
- 通知(Advice):在切面识别到某个连接点后要执行的动作。有多种类型的通知,包括前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。
- 织入(Weaving):将切面应用到目标对象上,从而创建代理对象的过程。这个过程可以在编译时、类加载时或运行时完成。Spring AOP采用的是运行时织入。
2 常见应用场景
Spring AOP(面向切面编程)的常见应用场景广泛,主要用于将那些遍布于应用各处的横切关注点(Cross-cutting Concerns)进行集中管理和代码复用。以下是一些典型的Spring AOP应用场景:
- 日志记录(Logging):在方法调用前后记录日志信息,用于跟踪方法执行情况、性能监控或调试。
- 权限检查(Security/Authorization):在方法执行前验证用户是否有权限执行该操作,比如角色检查或资源访问控制。
- 事务管理(Transaction Management):自动管理数据库事务的开启、提交或回滚,保证数据的一致性。
- 异常处理(Exception Handling):集中处理特定类型的异常,比如记录异常信息或执行特定的恢复操作。
- 性能监控(Performance Monitoring):监控方法执行时间,帮助识别和优化性能瓶颈。
- 缓存(Caching):自动缓存方法的返回结果,减少不必要的数据库查询或其他耗时操作。
- 参数校验和转换(Parameter Validation and Conversion):在方法调用前对参数进行校验或转换,确保符合业务逻辑要求。
- API调用统计(API Call Tracking):记录API的调用次数、频率等,用于分析和优化。
- SLF4J、Logback、Log4j等日志框架集成:通过AOP可以在不修改业务代码的情况下,灵活地切换或增强日志框架的功能。
- 自定义注解的处理:使用AOP拦截带有特定自定义注解的方法,实现特定逻辑,如标记某个方法需要审计、限流等。
这些场景体现了AOP的核心价值,即通过将横切关注点与核心业务逻辑分离,提高代码的可维护性和可读性,同时降低了模块间的耦合度。
aop的几种通知类型分别有什么常见的应用场景
3 AOP的几种通知类型分别有什么常见的应用场景
AOP(面向切面编程)的通知类型是实现切面功能的关键组成部分,每种类型对应着不同的应用场景。以下是Spring AOP五种通知类型及其常见应用场景:
- 前置通知(Before advice)- 应用场景:在目标方法执行前执行某些操作,如记录方法调用日志、验证权限、参数校验等。例如,在方法开始前记录操作日志,或检查用户是否已经登录。
- 后置通知(After returning advice)- 应用场景:在目标方法正常执行完毕后执行,通常用于清理资源、记录方法返回值或日志。例如,记录方法执行成功的信息或操作结果,以及进行一些资源清理工作。
- 异常通知(After throwing advice)- 应用场景:在目标方法抛出异常后执行,用于异常处理,如记录异常日志、发送错误报告、执行异常补偿措施等。例如,捕获并记录业务异常,或者向管理员发送异常报警邮件。
- 最终通知(After (finally) advice)- 应用场景:无论目标方法是否正常结束(包括正常返回或抛出异常),都会执行的代码块,常用于释放资源、关闭文件或数据库连接等。例如,确保数据库连接关闭,或执行必要的清理操作。
- 环绕通知(Around advice)- 应用场景:最灵活的通知类型,可以在方法调用前后执行自定义操作,甚至可以选择是否执行目标方法,适用于需要完全控制方法调用流程的场景,如性能监控、事务管理、日志记录与时间度量等。例如,环绕一个方法调用,测量其执行时间的同时控制事务的开启与提交或回滚。
通过这些通知类型,AOP能够有效地将横切关注点(如日志、安全、事务等)从核心业务逻辑中分离出来,提高代码的模块化程度和可维护性。
4 AOP实现 性能监控
在Spring AOP中,实现性能监控的一种常见方法是通过环绕通知(Around Advice)来测量方法的执行时间。环绕通知可以在方法调用前后执行自定义逻辑,非常适合用来监控性能。下面是一个简单的例子,展示如何使用Spring AOP来监控方法的执行时间:
4.1 首先,定义一个切面类,用于实现性能监控逻辑:
这里,我们定义了一个带有
@Around
注解的环绕通知方法,它会拦截所有标有自定义注解
@PerformanceMonitor
的方法。
PerformanceMonitor
是我们自定义的一个注解,用于标记需要监控的方法,并可配置额外的监控阈值。
packagecom.example.demo.aspect;importcom.example.demo.annotation.PerformanceMonitor;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.springframework.stereotype.Component;/**
* PerformanceMonitorAspect : 性能监控切面
*
* @author zyw
* @create 2024-06-06 13:18
*/@Aspect@Component@Slf4jpublicclassPerformanceMonitorAspect{/**
* 切面方法 Around环绕通知方法
*
* @param joinPoint
* @param performanceMonitor
* @return
* @throws Throwable
*/@Around("@annotation(performanceMonitor)")publicObjectmonitorPerformance(ProceedingJoinPoint joinPoint,PerformanceMonitor performanceMonitor)throwsThrowable{long start =System.currentTimeMillis();try{// 执行方法Object result = joinPoint.proceed();return result;}finally{long elapsedTime =System.currentTimeMillis()- start;// 记录方法执行时间
log.info("方法: {} 执行了 {} ms", joinPoint.getSignature().getName(), elapsedTime);if(performanceMonitor.logIfGreaterThan()>0&& elapsedTime > performanceMonitor.logIfGreaterThan()){
log.warn("性能警告:方法 {} 执行时间超过{}ms.", joinPoint.getSignature().getName(), performanceMonitor.logIfGreaterThan());}}}}
4.2 定义自定义注解
packagecom.example.demo.annotation;importjava.lang.annotation.ElementType;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Retention;importjava.lang.annotation.Target;/**
* PerformanceMonitor : 性能监控注解
*
* @author zyw
* @create 2024-06-06 13:19
*/@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfacePerformanceMonitor{// 默认不设置警告阈值longlogIfGreaterThan()default0;}
4.3 注解修饰监控的方法
这样,每次调用
performSomeTask
方法时,都会自动记录其执行时间,并在超过设定阈值时输出警告信息,帮助识别和优化性能瓶颈。
importcom.example.demo.annotation.PerformanceMonitor;@Override@PerformanceMonitor(logIfGreaterThan =100)// 如果执行时间超过100ms,则记录警告日志publicvoidperformSomeTask(Integer num){if(num ==1){try{Thread.sleep(2000);}catch(InterruptedException e){thrownewRuntimeException(e);}}}

5 AOP实现 API调用统计
5.1 定义切面类,用于实现API调用统计逻辑
在这个切面中,我们使用了
ConcurrentHashMap
和
AtomicLong
来安全地记录每个API方法的调用次数,确保在高并发环境下也能正确统计。
packagecom.example.demo.aspect;importcom.example.demo.annotation.PerformanceMonitor;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.springframework.stereotype.Component;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.atomic.AtomicLong;/**
* PerformanceMonitorAspect : 性能监控切面
*
* @author zyw
* @create 2024-06-06 13:18
*/@Aspect@Component@Slf4jpublicclassPerformanceMonitorAspect{privateConcurrentHashMap<String,AtomicLong> callCountMap =newConcurrentHashMap<>();/**
* 切面方法 Around环绕通知方法
*
* @param joinPoint
* @param performanceMonitor
* @return
* @throws Throwable
*/@Around("@annotation(performanceMonitor)")publicObjectmonitorPerformance(ProceedingJoinPoint joinPoint,PerformanceMonitor performanceMonitor)throwsThrowable{long start =System.currentTimeMillis();try{String methodName = joinPoint.getSignature().getName();long count = callCountMap.computeIfAbsent(methodName, k ->newAtomicLong()).incrementAndGet();// 执行方法Object result = joinPoint.proceed();// 可以在这里添加更复杂的统计逻辑,比如记录到数据库、发送到监控系统等// 这里简单打印调用次数
log.info("方法: {} 调用次数: {}", methodName, count);return result;}finally{long elapsedTime =System.currentTimeMillis()- start;// 记录方法执行时间
log.info("方法: {} 执行了 {} ms", joinPoint.getSignature().getName(), elapsedTime);if(performanceMonitor.logIfGreaterThan()>0&& elapsedTime > performanceMonitor.logIfGreaterThan()){
log.warn("性能警告:方法 {} 执行时间超过{}ms.", joinPoint.getSignature().getName(), performanceMonitor.logIfGreaterThan());}}}}
现在,每当
/api/data
这个API被调用时,
ApiCallStatsAspect
就会自动增加调用计数,并打印调用次数。你可以根据需要进一步扩展此逻辑,比如定期将统计数据发送到监控系统、数据库等,以便进行更深入的分析和优化。

6 AOP实现 缓存
6.1 定义缓存注解
首先,你需要定义一个自定义注解,用于标记需要缓存的方法。
packagecom.example.demo.annotation;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/**
* Cacheable : AOP缓存注解
*
* @author zyw
* @create 2024-06-06 16:16
*/@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfaceCacheable{// 缓存的键,可以根据方法参数生成Stringkey();// 缓存过期时间,默认永不过期longttl()default0;}
6.2 实现缓存切面
接下来,创建一个切面来拦截带有
@Cacheable
注解的方法,并实现缓存逻辑。
packagecom.example.demo.aspect;importcom.example.demo.annotation.Cacheable;importcom.example.demo.uitls.RedisUtil;importjakarta.annotation.Resource;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.springframework.stereotype.Component;/**
* CacheAspect :
*
* @author zyw
* @create 2024-06-06 16:17
*/@Aspect@ComponentpublicclassCacheAspect{@ResourceprivateRedisUtil redisUtil;@Around("@annotation(cacheable)")publicObjectcacheable(ProceedingJoinPoint joinPoint,Cacheable cacheable)throwsThrowable{String cacheKey =generateCacheKey(joinPoint, cacheable.key());Object result = redisUtil.get(cacheKey);if(result ==null){// 缓存中没有,执行方法并缓存结果
result = joinPoint.proceed();if(cacheable.ttl()>0){
redisUtil.set(cacheKey, result, cacheable.ttl());}else{
redisUtil.set(cacheKey, result);}}return result;}privateStringgenerateCacheKey(ProceedingJoinPoint joinPoint,String keyExpression){// 根据方法名、参数等生成缓存键,这里简化处理,实际可能需要更复杂的逻辑StringBuilder keyBuilder =newStringBuilder(keyExpression);Object[] args = joinPoint.getArgs();for(Object arg : args){
keyBuilder.append(":").append(arg.toString());}return keyBuilder.toString();}}
6.3 应用缓存注解
最后,在你想要缓存其返回结果的方法上使用
@Cacheable
注解。
packagecom.example.demo.service.impl;importcom.example.demo.annotation.Cacheable;importcom.example.demo.annotation.PerformanceMonitor;importcom.example.demo.entity.SysUser;importcom.example.demo.service.SysUserService;importcom.example.demo.service.TestService;importjakarta.annotation.Resource;importorg.springframework.stereotype.Service;importjava.util.List;/**
* TestServiceImpl :
*
* @author zyw
* @create 2023-12-18 15:15
*/@ServicepublicclassTestServiceImplimplementsTestService{@ResourceprivateSysUserService sysUserService;@Override@Cacheable(key ="UserListCacheKey", ttl =60)// 缓存1分钟publicList<SysUser>getUserList(){return sysUserService.list();}}
可用于结果固定,频繁需要获取的数据集,首次查询时走数据库,后缓存有效期内再次获取都从redis中取


7 AOP实现自定义滑动窗口限流
要实现AOP结合滑动窗口算法来实现自定义规则的限流,我们可以在原有的基础上进一步扩展,以支持更灵活的配置和更复杂的规则。以下是一个基于Spring AOP和滑动窗口算法的简单示例,包括自定义注解来设置限流规则,以及如何在切面中应用这些规则。
7.1 定义缓存注解
首先,定义一个自定义注解来标记需要限流的方法,并允许传入限流的具体规则
packagecom.example.demo.annotation;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/**
* SlidingWindowRateLimiter : 滑动窗口限流注解
*
* @author zyw
* @create 2024-06-06 17:20
*/@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceWindowRateLimit{// 允许的最大请求数intlimit();// 窗口时间长度,单位毫秒longtimeWindowMilliseconds();}
7.2 滑动窗口限流器
接下来,实现滑动窗口限流器,这里简化处理,直接使用内存实现,实际应用中可能需要基于Redis等持久化存储以适应分布式场景:
核心思想:每次请求进来时,获取当前时间的时间戳,将每次请求的时间戳存储到LinkedList集合中,同时以当前时间为窗口期的结束点,删除往前一个窗口期内所有的请求时间戳,将LinkedList集合剩余数据的个数与自定义设置的窗口期请求峰值进行对比,若等于则直接限流。
packagecom.example.demo.uitls;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importjava.io.Serializable;importjava.util.LinkedList;/**
* SlidingWindowRateLimiter : 滑动窗口限流算法
*
* @author zyw
* @create 2024-06-07 15:16
*/@Data@AllArgsConstructor@NoArgsConstructorpublicclassSlidingWindowRateLimiterimplementsSerializable{/**
* 请求队列
*/privateLinkedList<Long> requests =newLinkedList<>();/**
* 最大请求数
*/privateint maxRequests;/**
* 窗口大小
*/privatelong windowSizeInMilliseconds;publicSlidingWindowRateLimiter(int maxRequests,long windowSizeInMilliseconds){this.maxRequests = maxRequests;this.windowSizeInMilliseconds = windowSizeInMilliseconds;}/**
* 判断是否允许请求
* @return
*/publicsynchronizedbooleanallowRequest(){// 获取当前时间long currentTime =System.currentTimeMillis();// 清除窗口之外的旧请求while(!requests.isEmpty()&& currentTime - requests.peekFirst()> windowSizeInMilliseconds){
requests.removeFirst();}// 如果当前窗口请求未达到上限,则允许请求并记录if(requests.size()< maxRequests){
requests.addLast(currentTime);returntrue;}else{// 达到限流阈值,拒绝请求returnfalse;}}}
7.3 AOP切面实现
最后,创建AOP切面来应用限流逻辑:
将需要限流的方法所初始化的滑动窗口限流器缓存到Redis中,过期时间设置为对应的窗口时间。
一个窗口时间内,若没有新的请求进来,即存储的请求时间戳都为窗口期外的,因此可以直接清除掉已减少缓存占用空间。
packagecom.example.demo.aspect;importcom.example.demo.annotation.WindowRateLimit;importcom.example.demo.config.redis.RedisKeyEnum;importcom.example.demo.uitls.RedisUtil;importcom.example.demo.uitls.SlidingWindowRateLimiter;importjakarta.annotation.Resource;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.springframework.stereotype.Component;/**
* RateLimiterAspect :
*
* @author zyw
* @create 2024-06-06 17:21
*/@Aspect@ComponentpublicclassSlidingWindowRateLimiterAspect{@ResourceprivateRedisUtil redisUtil;@Around("@annotation(rateLimit)")publicObjectapplyRateLimit(ProceedingJoinPoint joinPoint,WindowRateLimit rateLimit)throwsThrowable{// 获取调用的方法名String methodName = joinPoint.getSignature().getName();// 获取方法对应的缓存滑动窗口限流器KEYString key =RedisKeyEnum.WINDOW_CURRENT_LIMITING.getKey()+ methodName;// 从缓存中获取滑动窗口限流器SlidingWindowRateLimiter rateLimiter = redisUtil.getCacheObject(key);// 如果滑动窗口限流器不存在,则创建一个新限流器if(rateLimiter ==null){
rateLimiter =newSlidingWindowRateLimiter(rateLimit.limit(), rateLimit.timeWindowMilliseconds());}// 如果滑动窗口限流器存在,则判断是否允许请求if(!rateLimiter.allowRequest()){thrownewRuntimeException("Too many requests, please try again later.");}// 如果允许请求,则更新滑动窗口限流器,缓存过期时间设置为滑动窗口限流器时间窗口
redisUtil.setCacheObject(key, rateLimiter, rateLimit.timeWindowMilliseconds(),TimeUnit.MILLISECONDS);// 允许执行方法return joinPoint.proceed();}}
7.4 应用限流注解
在需要做限流的方法上加上注解,在注解参数中设定 允许的最大请求数 和 窗口时间长度(单位毫秒)
packagecom.example.demo.service.impl;importcom.example.demo.annotation.WindowRateLimit;importcom.example.demo.service.TestService;importjakarta.annotation.Resource;importorg.springframework.stereotype.Service;/**
* TestServiceImpl :
*
* @author zyw
* @create 2023-12-18 15:15
*/@ServicepublicclassTestServiceImplimplementsTestService{@Override@WindowRateLimit(limit =5, timeWindowMilliseconds =60L*1000)// 每最多允许5次请求publicStringgetContent(){return"Hello Word";}}
首次请求时,初始化滑动窗口限流器,记录第一次请求的时间戳

窗口期内,记录了五次请求的时间戳后,已达到我们在注解中设置的窗口期最大请求量

此时接口限流

版权归原作者 柚几哥哥 所有, 如有侵权,请联系我们删除。