SpringAOP 常见应用场景

文章目录

  • SpringAOP
    • [1 概念](#1 概念)
    • [2 常见应用场景](#2 常见应用场景)
    • [3 AOP的几种通知类型分别有什么常见的应用场景](#3 AOP的几种通知类型分别有什么常见的应用场景)
    • [4 AOP实现 性能监控](#4 AOP实现 性能监控)
      • [4.1 首先,定义一个切面类,用于实现性能监控逻辑:](#4.1 首先,定义一个切面类,用于实现性能监控逻辑:)
      • [4.2 定义自定义注解](#4.2 定义自定义注解)
      • [4.3 注解修饰监控的方法](#4.3 注解修饰监控的方法)
    • [5 AOP实现 API调用统计](#5 AOP实现 API调用统计)
      • [5.1 定义切面类,用于实现API调用统计逻辑](#5.1 定义切面类,用于实现API调用统计逻辑)
    • [6 AOP实现 缓存](#6 AOP实现 缓存)
      • [6.1 定义缓存注解](#6.1 定义缓存注解)
      • [6.2 实现缓存切面](#6.2 实现缓存切面)
      • [6.3 应用缓存注解](#6.3 应用缓存注解)
    • [7 AOP实现自定义滑动窗口限流](#7 AOP实现自定义滑动窗口限流)
      • [7.1 定义缓存注解](#7.1 定义缓存注解)
      • [7.2 滑动窗口限流器](#7.2 滑动窗口限流器)
      • [7.3 AOP切面实现](#7.3 AOP切面实现)
      • [7.4 应用限流注解](#7.4 应用限流注解)

SpringAOP

1 概念

Spring AOP(Aspect Oriented Programming,面向切面编程)是Spring框架中的一个模块,它提供了面向切面编程的支持。AOP允许开发者将横切关注点(如日志记录、权限检查、性能监控等)从业务逻辑中分离出来,通过这种方式,可以使代码更加模块化,易于维护和管理。

核心概念:

  1. 切面(Aspect):切面是跨越多个对象的行为或关注点的模块化。例如,事务管理就是企业级应用中的一个关注点,它需要跨越多个对象进行管理。
  2. 连接点(Joinpoint):在程序执行过程中某个特定的点,比如方法的调用或者异常的抛出。在Spring AOP中,连接点总是方法的执行。
  3. 切入点(Pointcut):定义了切面应该在哪些连接点上执行的规则。可以通过表达式来匹配哪些方法需要被切面所影响。
  4. 通知(Advice):在切面识别到某个连接点后要执行的动作。有多种类型的通知,包括前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。
  5. 织入(Weaving):将切面应用到目标对象上,从而创建代理对象的过程。这个过程可以在编译时、类加载时或运行时完成。Spring AOP采用的是运行时织入。

2 常见应用场景

Spring AOP(面向切面编程)的常见应用场景广泛,主要用于将那些遍布于应用各处的横切关注点(Cross-cutting Concerns)进行集中管理和代码复用。以下是一些典型的Spring AOP应用场景:

  1. 日志记录(Logging):在方法调用前后记录日志信息,用于跟踪方法执行情况、性能监控或调试。
  2. 权限检查(Security/Authorization):在方法执行前验证用户是否有权限执行该操作,比如角色检查或资源访问控制。
  3. 事务管理(Transaction Management):自动管理数据库事务的开启、提交或回滚,保证数据的一致性。
  4. 异常处理(Exception Handling):集中处理特定类型的异常,比如记录异常信息或执行特定的恢复操作。
  5. 性能监控(Performance Monitoring):监控方法执行时间,帮助识别和优化性能瓶颈。
  6. 缓存(Caching):自动缓存方法的返回结果,减少不必要的数据库查询或其他耗时操作。
  7. 参数校验和转换(Parameter Validation and Conversion):在方法调用前对参数进行校验或转换,确保符合业务逻辑要求。
  8. API调用统计(API Call Tracking):记录API的调用次数、频率等,用于分析和优化。
  9. SLF4J、Logback、Log4j等日志框架集成:通过AOP可以在不修改业务代码的情况下,灵活地切换或增强日志框架的功能。
  10. 自定义注解的处理:使用AOP拦截带有特定自定义注解的方法,实现特定逻辑,如标记某个方法需要审计、限流等。

这些场景体现了AOP的核心价值,即通过将横切关注点与核心业务逻辑分离,提高代码的可维护性和可读性,同时降低了模块间的耦合度。

aop的几种通知类型分别有什么常见的应用场景

3 AOP的几种通知类型分别有什么常见的应用场景

AOP(面向切面编程)的通知类型是实现切面功能的关键组成部分,每种类型对应着不同的应用场景。以下是Spring AOP五种通知类型及其常见应用场景:

  1. 前置通知(Before advice)
    • 应用场景:在目标方法执行前执行某些操作,如记录方法调用日志、验证权限、参数校验等。例如,在方法开始前记录操作日志,或检查用户是否已经登录。
  2. 后置通知(After returning advice)
    • 应用场景:在目标方法正常执行完毕后执行,通常用于清理资源、记录方法返回值或日志。例如,记录方法执行成功的信息或操作结果,以及进行一些资源清理工作。
  3. 异常通知(After throwing advice)
    • 应用场景:在目标方法抛出异常后执行,用于异常处理,如记录异常日志、发送错误报告、执行异常补偿措施等。例如,捕获并记录业务异常,或者向管理员发送异常报警邮件。
  4. 最终通知(After (finally) advice)
    • 应用场景:无论目标方法是否正常结束(包括正常返回或抛出异常),都会执行的代码块,常用于释放资源、关闭文件或数据库连接等。例如,确保数据库连接关闭,或执行必要的清理操作。
  5. 环绕通知(Around advice)
    • 应用场景:最灵活的通知类型,可以在方法调用前后执行自定义操作,甚至可以选择是否执行目标方法,适用于需要完全控制方法调用流程的场景,如性能监控、事务管理、日志记录与时间度量等。例如,环绕一个方法调用,测量其执行时间的同时控制事务的开启与提交或回滚。

通过这些通知类型,AOP能够有效地将横切关注点(如日志、安全、事务等)从核心业务逻辑中分离出来,提高代码的模块化程度和可维护性。

4 AOP实现 性能监控

在Spring AOP中,实现性能监控的一种常见方法是通过环绕通知(Around Advice)来测量方法的执行时间。环绕通知可以在方法调用前后执行自定义逻辑,非常适合用来监控性能。下面是一个简单的例子,展示如何使用Spring AOP来监控方法的执行时间:

4.1 首先,定义一个切面类,用于实现性能监控逻辑:

这里,我们定义了一个带有@Around注解的环绕通知方法,它会拦截所有标有自定义注解@PerformanceMonitor的方法。PerformanceMonitor是我们自定义的一个注解,用于标记需要监控的方法,并可配置额外的监控阈值。

java 复制代码
package com.example.demo.aspect;

import com.example.demo.annotation.PerformanceMonitor;
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;

/**
 * PerformanceMonitorAspect : 性能监控切面
 *
 * @author zyw
 * @create 2024-06-06  13:18
 */

@Aspect
@Component
@Slf4j
public class PerformanceMonitorAspect {

    /**
     * 切面方法 Around环绕通知方法
     *
     * @param joinPoint
     * @param performanceMonitor
     * @return
     * @throws Throwable
     */
    @Around("@annotation(performanceMonitor)")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint, PerformanceMonitor performanceMonitor) throws Throwable {
        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 定义自定义注解

java 复制代码
package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * PerformanceMonitor : 性能监控注解
 *
 * @author zyw
 * @create 2024-06-06  13:19
 */

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PerformanceMonitor {
    // 默认不设置警告阈值
    long logIfGreaterThan() default 0;
}

4.3 注解修饰监控的方法

这样,每次调用performSomeTask方法时,都会自动记录其执行时间,并在超过设定阈值时输出警告信息,帮助识别和优化性能瓶颈。

java 复制代码
import com.example.demo.annotation.PerformanceMonitor;    

    @Override
    @PerformanceMonitor(logIfGreaterThan = 100) // 如果执行时间超过100ms,则记录警告日志
    public void performSomeTask(Integer num) {
        if (num == 1){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

5 AOP实现 API调用统计

5.1 定义切面类,用于实现API调用统计逻辑

在这个切面中,我们使用了ConcurrentHashMapAtomicLong来安全地记录每个API方法的调用次数,确保在高并发环境下也能正确统计。

java 复制代码
package com.example.demo.aspect;

import com.example.demo.annotation.PerformanceMonitor;
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;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * PerformanceMonitorAspect : 性能监控切面
 *
 * @author zyw
 * @create 2024-06-06  13:18
 */

@Aspect
@Component
@Slf4j
public class PerformanceMonitorAspect {

    private ConcurrentHashMap<String, AtomicLong> callCountMap = new ConcurrentHashMap<>();
    /**
     * 切面方法 Around环绕通知方法
     *
     * @param joinPoint
     * @param performanceMonitor
     * @return
     * @throws Throwable
     */
    @Around("@annotation(performanceMonitor)")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint, PerformanceMonitor performanceMonitor) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            String methodName = joinPoint.getSignature().getName();
            long count = callCountMap.computeIfAbsent(methodName, k -> new AtomicLong()).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 定义缓存注解

首先,你需要定义一个自定义注解,用于标记需要缓存的方法。

java 复制代码
package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Cacheable : AOP缓存注解
 *
 * @author zyw
 * @create 2024-06-06  16:16
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cacheable {
    // 缓存的键,可以根据方法参数生成
    String key();
    // 缓存过期时间,默认永不过期
    long ttl() default 0;
}

6.2 实现缓存切面

接下来,创建一个切面来拦截带有@Cacheable注解的方法,并实现缓存逻辑。

java 复制代码
package com.example.demo.aspect;

import com.example.demo.annotation.Cacheable;
import com.example.demo.uitls.RedisUtil;
import jakarta.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;


/**
 * CacheAspect :
 *
 * @author zyw
 * @create 2024-06-06  16:17
 */

@Aspect
@Component
public class CacheAspect {

    @Resource
    private RedisUtil redisUtil;

    @Around("@annotation(cacheable)")
    public Object cacheable(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
        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;
    }

    private String generateCacheKey(ProceedingJoinPoint joinPoint, String keyExpression) {
        // 根据方法名、参数等生成缓存键,这里简化处理,实际可能需要更复杂的逻辑
        StringBuilder keyBuilder = new StringBuilder(keyExpression);
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            keyBuilder.append(":").append(arg.toString());
        }
        return keyBuilder.toString();
    }

}

6.3 应用缓存注解

最后,在你想要缓存其返回结果的方法上使用@Cacheable注解。

java 复制代码
package com.example.demo.service.impl;

import com.example.demo.annotation.Cacheable;
import com.example.demo.annotation.PerformanceMonitor;
import com.example.demo.entity.SysUser;
import com.example.demo.service.SysUserService;
import com.example.demo.service.TestService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.List;

/**
 * TestServiceImpl :
 *
 * @author zyw
 * @create 2023-12-18  15:15
 */
@Service
public class TestServiceImpl implements TestService {

    @Resource
    private SysUserService sysUserService;

    @Override
    @Cacheable(key = "UserListCacheKey", ttl = 60) // 缓存1分钟
    public List<SysUser> getUserList() {
        return sysUserService.list();
    }
}

可用于结果固定,频繁需要获取的数据集,首次查询时走数据库,后缓存有效期内再次获取都从redis中取


7 AOP实现自定义滑动窗口限流

要实现AOP结合滑动窗口算法来实现自定义规则的限流,我们可以在原有的基础上进一步扩展,以支持更灵活的配置和更复杂的规则。以下是一个基于Spring AOP和滑动窗口算法的简单示例,包括自定义注解来设置限流规则,以及如何在切面中应用这些规则。

7.1 定义缓存注解

首先,定义一个自定义注解来标记需要限流的方法,并允许传入限流的具体规则

java 复制代码
package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * SlidingWindowRateLimiter : 滑动窗口限流注解
 *
 * @author zyw
 * @create 2024-06-06  17:20
 */

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WindowRateLimit {
    // 允许的最大请求数
    int limit();
    // 窗口时间长度,单位毫秒
    long timeWindowMilliseconds();
}

7.2 滑动窗口限流器

接下来,实现滑动窗口限流器,这里简化处理,直接使用内存实现,实际应用中可能需要基于Redis等持久化存储以适应分布式场景:

java 复制代码
package com.example.demo.uitls;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.LinkedList;

/**
 * SlidingWindowRateLimiter : 滑动窗口限流算法
 *
 * @author zyw
 * @create 2024-06-07  15:16
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SlidingWindowRateLimiter implements Serializable {
    /**
     * 请求队列
     */
    private LinkedList<Long> requests = new LinkedList<>();
    /**
     * 最大请求数
     */
    private int maxRequests;
    /**
     * 窗口大小
     */
    private long windowSizeInMilliseconds;

    public SlidingWindowRateLimiter(int maxRequests, long windowSizeInMilliseconds) {
        this.maxRequests = maxRequests;
        this.windowSizeInMilliseconds = windowSizeInMilliseconds;
    }

    /**
     * 判断是否允许请求
     * @return
     */
    public synchronized boolean allowRequest() {
        // 获取当前时间
        long currentTime = System.currentTimeMillis();

        // 清除窗口之外的旧请求
        while (!requests.isEmpty() && currentTime - requests.peekFirst() > windowSizeInMilliseconds) {
            requests.removeFirst();
        }

        // 如果当前窗口请求未达到上限,则允许请求并记录
        if (requests.size() < maxRequests) {
            requests.addLast(currentTime);
            return true;
        } else {
            // 达到限流阈值,拒绝请求
            return false;
        }
    }
}

7.3 AOP切面实现

最后,创建AOP切面来应用限流逻辑:

java 复制代码
package com.example.demo.aspect;

import com.example.demo.annotation.WindowRateLimit;
import com.example.demo.config.redis.RedisKeyEnum;
import com.example.demo.uitls.RedisUtil;
import com.example.demo.uitls.SlidingWindowRateLimiter;
import jakarta.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * RateLimiterAspect :
 *
 * @author zyw
 * @create 2024-06-06  17:21
 */

@Aspect
@Component
public class SlidingWindowRateLimiterAspect {

    @Resource
    private RedisUtil redisUtil;

    @Around("@annotation(rateLimit)")
    public Object applyRateLimit(ProceedingJoinPoint joinPoint, WindowRateLimit rateLimit) throws Throwable {
        // 获取调用的方法名
        String methodName = joinPoint.getSignature().getName();
        // 获取方法对应的缓存滑动窗口限流器KEY
        String key = RedisKeyEnum.WINDOW_CURRENT_LIMITING.getKey() + methodName;
        // 从缓存中获取滑动窗口限流器
        SlidingWindowRateLimiter rateLimiter = redisUtil.getCacheObject(key);
        // 如果滑动窗口限流器不存在,则创建一个新限流器
        if (rateLimiter == null) {
            rateLimiter = new SlidingWindowRateLimiter(rateLimit.limit(), rateLimit.timeWindowMilliseconds());
        }
        // 如果滑动窗口限流器存在,则判断是否允许请求
        if (!rateLimiter.allowRequest()) {
            throw new RuntimeException("Too many requests, please try again later.");
        }
        // 如果允许请求,则更新滑动窗口限流器
        redisUtil.setCacheObject(key, rateLimiter);
        // 允许执行方法
        return joinPoint.proceed(); 
    }

}

7.4 应用限流注解

在需要做限流的方法上加上注解,在注解参数中设定 允许的最大请求数窗口时间长度(单位毫秒)

java 复制代码
package com.example.demo.service.impl;

import com.example.demo.annotation.WindowRateLimit;
import com.example.demo.service.TestService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

/**
 * TestServiceImpl :
 *
 * @author zyw
 * @create 2023-12-18  15:15
 */
@Service
public class TestServiceImpl implements TestService {

    @Override
    @WindowRateLimit(limit = 5, timeWindowMilliseconds = 60L*1000) // 每最多允许5次请求
    public String getContent() {
        return "Hello Word";
    }
}

首次请求时,初始化滑动窗口限流器,记录第一次请求的时间戳

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

此时接口限流

相关推荐
盐真卿1 天前
python2
java·前端·javascript
一嘴一个橘子1 天前
mybatis - 动态语句、批量注册mapper、分页插件
java
组合缺一1 天前
Json Dom 怎么玩转?
java·json·dom·snack4
危险、1 天前
一套提升 Spring Boot 项目的高并发、高可用能力的 Cursor 专用提示词
java·spring boot·提示词
kaico20181 天前
JDK11新特性
java
钊兵1 天前
java实现GeoJSON地理信息对经纬度点的匹配
java·开发语言
jiayong231 天前
Tomcat性能优化面试题
java·性能优化·tomcat
爬山算法1 天前
Hibernate(51)Hibernate的查询缓存如何使用?
spring·缓存·hibernate
秋刀鱼程序编程1 天前
Java基础入门(五)----面向对象(上)
java·开发语言
sunnyday04261 天前
基于Netty构建WebSocket服务器实战指南
服务器·spring boot·websocket·网络协议