SpringAOP 实战

目录

  1. 引言
  2. 什么是AOP?
  3. AOP的核心概念
  4. 实际应用场景
  5. 总结与思考
  6. 拓展阅读

引言

Spring框架有两大核心特性:IOC(控制反转) 和 AOP(面向切面编程)。相比于IOC在日常工作中的广泛应用,AOP的使用频率相对较低。本文旨在通过实际工作中的场景,为大家揭示AOP面向切面编程的强大功能和实际应用价值。

什么是AOP?

AOP(Aspect Oriented Programming)即面向切面编程,是通过预编译方式和运行期动态代理实现程序功能统一维护的一种技术。AOP是OOP(面向对象编程)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

下面用一张图理解一下AOP:

在这里插入图片描述

通过AOP技术,我们可以将业务中的通用部分(如日志记录、性能统计、安全控制、事务处理、异常处理等)抽取出来,并对业务逻辑进行无侵入式的增强,而我们只需要专注于核心业务逻辑的开发。

AOP的核心概念

  • Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。

  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。

  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。

  • Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。

  • Target(目标对象):织入 Advice 的目标对象.。

  • Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程

实际应用场景

场景1:接口限流

背景

在高并发系统中,我们常常需要对某些接口进行限流,以防止系统被大量请求冲垮。这里我们实现一个简单的限流机制,要求同一个用户在指定时间内不可重复请求某个接口。

代码实现
  1. 首先在类上面添加@Aspect标识这是一个切面类 并添加@Component注解让其被Spring发现

    @Slf4j
    @Aspect
    @Component
    public class RateLimitingAspect {
    
  2. 创建切面方法,并定义增强类型及切入点

    @Around("@annotation(com.beansmile.common.config.RateLimiting)")
       public Object rateLimiting(ProceedingJoinPoint pjp) throws Throwable {
    

    其中@Around代表增强类型为环绕增强、除此之外还有:before(执行前) after(执行后),注解的内容是设置的切入点,这里设置的为添加@RateLimiting注解的地方,此处也可以通过表达式设置为 xxx包下的类、xxx类下方法等等

  3. 创建自定义注解: RateLimiting

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface RateLimiting {
    
        /**
         * 间隔时间,单位秒
         */
        int limitInSeconds() default 2;
    
        /**
         * 返回信息
         */
        String returnMsg() default "您的请求太快了,请稍后提交!";
    }
    
  4. 具体逻辑代码

        @Around("@annotation(com.beansmile.common.config.RateLimiting)")
        public Object rateLimiting(ProceedingJoinPoint pjp) throws Throwable {
    
            String method = pjp.getSignature().toShortString();
    
            // lockKey = userId:xxxController.xxxMethod()
            String lockKey = AuthUtil.getUserId() + ":" + method;
    
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            Method signatureMethod = methodSignature.getMethod();
    
    		// 拿到注解内容
            RateLimiting rateLimitingAnnotation = signatureMethod.getAnnotation(RateLimiting.class);
            String returnMsg = rateLimitingAnnotation.returnMsg();
            int limitInSeconds = rateLimitingAnnotation.limitInSeconds();
    
            Boolean lock = redisTemplate.opsForValue().setIfAbsent(method, 1, limitInSeconds, TimeUnit.SECONDS);
    
            if (BooleanUtils.isTrue(lock)) {
                // 放行
                return pjp.proceed();
            } else {
                log.info("请求限流,lockKey:{},间隔时间:{}", lockKey, limitInSeconds);
                throw new ServiceException(returnMsg);
            }
        }
    

上述代码中,我们以 userId + 类名称 + 方法名称 作为唯一标识,通过redis锁的方式进行限流。

测试代码:  这里我们设置了请求间隔为1秒
    @RequestMapping(value = "/test", method = RequestMethod.GET)
    @RateLimiting(limitInSeconds = 1)
    public Result<?> test() {
        // 具体业务逻辑
        // xxxxxxxxxxxxx
        return Result.OK("操作成功!");
    }

正常请求:

在这里插入图片描述

快速请求:

在这里插入图片描述

可以看到,我们通过使用AOP的方式实现了 接口限流的逻辑,并且没有修改任何业务逻辑。

场景2:全局异常捕获

背景

在程序运行时不可避免的会出现一些异常, 如NullPointerException空指针、ArrayIndexOutOfBoundsException数组下标越界等, 这些异常直接抛到前端页面会导致用户体验不好,过多的try/catch捕捉又会导致代码冗余。

代码实现
  1. 定义切入点
	@Pointcut("execution(public * com.beansmile.modules..*.*Controller.*(..)))")
	public void exceptionPointcut() {
	}

这里定义的切入点为 com.beansmile.modules包下 所有控制器下的方法。

  1. 创建异常枚举类
@Getter
@AllArgsConstructor
public enum ExceptionEnum {

    /**
     * NullPointerException
     */
    NULL_POINTER(10001, NullPointerException.class, "空指针异常"),

    /**
     * ArrayIndexOutOfBoundsException
     */
    ARRAY_INDEX_OUT_OF_BOUND(10002, ArrayIndexOutOfBoundsException.class, "数组下标越界"),

    /**
     * IOException
     */
    IO(10003, IOException.class, "输入输出异常")
    ;

    final int code;
    final Class<?> clazz;
    final String name;

    public static ExceptionEnum getEnumByClazz(Class<?> clazz) {
        for (ExceptionEnum exceptionEnum : values()) {
            if (clazz.isAssignableFrom(exceptionEnum.clazz)) {
                return exceptionEnum;
            }
        }
        return null;
    }
}
  1. 具体逻辑代码
	@Around("exceptionPointcut()")
    public Object exceptionHandling(ProceedingJoinPoint pjp) throws Throwable {

        try {
            return pjp.proceed();
        } catch (Exception e) {

            // 通过枚举类获取异常信息
            ExceptionEnum exceptionEnum = ExceptionEnum.getEnumByClazz(e.getClass());

            if (null == exceptionEnum) throw e;

            // 打印异常信息
            printExceptionInfo(pjp, e);

            // 抛出自定义异常,并携带错误码用于快速排查。
            throw new ServiceException("网络开小差了,请稍后重试或联系客服![" + exceptionEnum.getCode() + "]");
        }
    }

    private void printExceptionInfo(ProceedingJoinPoint pjp, Exception e) {
        String methodName = pjp.getSignature().getName();
        String className = pjp.getTarget().getClass().getSimpleName();

        log.error("printExceptionInfo_类名:{},方法名:{},异常信息:{}", className, methodName, e.toString());
        e.printStackTrace();
    }

上述代码中,通过对异常时进行切入,对异常做了统一抛出和打印日志。

测试代码

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public Result<?> test() {
        // 模拟空指针异常
        String str = null;
        str.length();
        return Result.OK("操作成功!");
    }

请求接口进行测试:符合预期,统一错误返回,并携带错误码

在这里插入图片描述

查看后端日志: 保留了原始错误信息,并快速记录了错误的发生源头。

printExceptionInfo_类名:ApprovalFlowController,方法名:test,异常信息:java.lang.NullPointerException
java.lang.NullPointerException

总结与思考

AOP作为Spring框架的核心特性之一,为我们提供了一种非侵入式的方法来处理横切关注点。通过本文的两个实际应用场景,我们可以看到AOP在处理诸如限流、日志记录、异常处理等方面的强大能力。

使用AOP的主要优势包括:

  1. 代码分离:核心业务逻辑与横切关注点分离,提高代码清晰度。
  2. 复用性:通用功能可以在多个位置复用,无需重复编写。
  3. 可维护性:集中管理横切关注点,便于统一修改和维护。
  4. 灵活性:可以动态地添加或移除功能,而无需修改核心代码。

然而,在使用AOP时也需要注意一些潜在的问题:

  1. 性能影响:过度使用AOP可能会带来轻微的性能开销。
  2. 调试难度:因为是动态织入的代码,可能增加调试的复杂性。
  3. 适用范围:并非所有场景都适合使用AOP,需要权衡利弊。

拓展阅读

Spring AOP官方文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值