Spring AOP(面向切面编程)核心概念:切面、连接点、切点、通知与织入

Spring AOP:给你的代码穿上隐形战衣

各位程序猿、媛们,大家好!今天咱们来聊聊Spring AOP,这玩意儿就像给你的代码穿上一件隐形战衣,悄无声息地增强功能,既不影响核心业务逻辑,又能轻松实现日志记录、性能监控、安全控制等各种骚操作。

说起AOP,可能有些小伙伴会觉得高深莫测,其实一点也不难。想象一下,你是一名武林高手,精通各种招式(核心业务),但每次出招都要考虑会不会伤到自己(代码耦合),或者被打断(异常处理)。AOP就像一位神秘的武林前辈,在你出招前、出招后、甚至出招时,给你加持各种Buff,保护你的安全,提升你的战斗力,而你只需要专注于自己的招式本身。

那么,这位神秘的武林前辈到底是怎么做到的呢?这就涉及到AOP的几个核心概念了:切面(Aspect)、连接点(Joinpoint)、切点(Pointcut)、通知(Advice)和织入(Weaving)。 别怕,接下来咱们一个一个地拆解,保证让你明白得透透的。

1. 切面(Aspect):隐形战衣的蓝图

切面,你可以把它理解为一件隐形战衣的蓝图。它定义了什么时间、什么地点、做什么事情。换句话说,它包含了切点(Pointcut)通知(Advice)两部分,描述了在哪些连接点上应用哪些通知。

举个栗子:

假设我们要实现一个日志记录功能,记录所有Service层方法的执行时间。那么,这个日志记录功能就是一个切面。它定义了:

  • 切点: 所有Service层的方法
  • 通知: 在方法执行前记录开始时间,在方法执行后记录结束时间,并计算执行时长。

代码示例(使用AspectJ注解方式):

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogAspect {

    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    // 定义切点:所有Service层的方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayerExecution() {}

    // 前置通知:方法执行前执行
    @Before("serviceLayerExecution()")
    public void before(JoinPoint joinPoint) {
        logger.info("方法开始执行: {}#{}", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName());
    }

    // 后置通知:方法执行后执行(无论是否发生异常)
    @After("serviceLayerExecution()")
    public void after(JoinPoint joinPoint) {
        logger.info("方法执行完毕: {}#{}", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName());
    }

    // 返回后通知:方法正常返回后执行
    @AfterReturning(pointcut = "serviceLayerExecution()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        logger.info("方法返回值: {}#{} -> {}", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName(), result);
    }

    // 异常通知:方法抛出异常后执行
    @AfterThrowing(pointcut = "serviceLayerExecution()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
        logger.error("方法执行异常: {}#{} -> {}", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName(), e.getMessage());
    }

    // 环绕通知:方法执行前后都执行
    @Around("serviceLayerExecution()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        logger.info("方法环绕开始: {}#{}", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName());
        Object result = null;
        try {
            result = joinPoint.proceed(); // 执行目标方法
            logger.info("方法环绕返回: {}#{} -> {}", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName(), result);
            return result;
        } catch (Throwable e) {
            logger.error("方法环绕异常: {}#{} -> {}", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName(), e.getMessage());
            throw e;
        } finally {
            long endTime = System.currentTimeMillis();
            logger.info("方法环绕结束: {}#{},耗时:{}ms", joinPoint.getTarget().getClass().getSimpleName(), joinPoint.getSignature().getName(), (endTime - startTime));
        }
    }
}

代码解释:

  • @Aspect: 声明这是一个切面类。
  • @Component: 将这个切面类交给Spring管理。
  • @Pointcut("execution(* com.example.service.*.*(..))"): 定义切点,表示所有com.example.service包下的所有类的所有方法。 execution是Spring AOP提供的切点指示符之一,用于匹配方法的执行。
  • @Before("serviceLayerExecution()"): 定义前置通知,在切点对应的方法执行前执行。
  • @After("serviceLayerExecution()"): 定义后置通知,在切点对应的方法执行后执行(无论是否发生异常)。
  • @AfterReturning(pointcut = "serviceLayerExecution()", returning = "result"): 定义返回后通知,在切点对应的方法正常返回后执行。 returning = "result"表示将方法的返回值绑定到result变量上。
  • @AfterThrowing(pointcut = "serviceLayerExecution()", throwing = "e"): 定义异常通知,在切点对应的方法抛出异常后执行。 throwing = "e"表示将抛出的异常绑定到e变量上。
  • @Around("serviceLayerExecution()"): 定义环绕通知,可以控制目标方法的执行,可以在方法执行前后做一些事情。 ProceedingJoinPoint 允许你控制目标方法的执行,必须手动调用joinPoint.proceed()来执行目标方法。

2. 连接点(Joinpoint):代码中的十字路口

连接点,顾名思义,就是程序执行过程中可以插入切面的点。 它代表程序执行的一个特定位置,例如方法的调用、异常的抛出、字段的访问等。 可以把连接点想象成代码中的一个个十字路口,AOP可以在这些路口设置红绿灯,控制程序的走向。

常见连接点:

连接点类型 描述
方法执行 方法被调用时,这是最常用的连接点类型。
方法调用 方法内部调用其他方法时。
异常处理 try-catch块中的catch语句,当捕获到异常时。
字段访问 访问对象的字段时,包括读取和写入。
静态初始化 类加载时执行的静态初始化块。
对象创建 使用new关键字创建对象时。

需要注意的是,并非所有的连接点都需要被增强,只有被切点选中的连接点才会被织入切面逻辑。

3. 切点(Pointcut):选择目标的雷达

切点,就像一个雷达,用于扫描程序中所有的连接点,并精确地定位到我们需要增强的连接点。 它是一个表达式,定义了哪些连接点需要被切面拦截。 简单来说,切点就是用来确定哪些连接点应该被应用通知的。

切点表达式:

Spring AOP使用AspectJ切点表达式语言来定义切点。 切点表达式由 切点指示符(Pointcut Designator, PCD)表达式 组成。

常见的切点指示符:

切点指示符 描述 示例
execution 用于匹配方法的执行。 这是最常用的切点指示符。 execution(* com.example.service.*.*(..)): 匹配com.example.service包下的所有类的所有方法。
within 用于匹配指定类型内的连接点。 within(com.example.service.*): 匹配com.example.service包下的所有类的所有方法。
this 用于匹配当前执行对象为指定类型的连接点。 this(com.example.service.UserService): 匹配当前执行对象是UserService实例的连接点。
target 用于匹配目标对象为指定类型的连接点。 target(com.example.service.UserService): 匹配目标对象是UserService实例的连接点。
args 用于匹配方法参数为指定类型的连接点。 args(String): 匹配方法参数只有一个,且类型为String的连接点。
bean 用于匹配指定名称的Bean中的连接点。 bean(userService): 匹配名为userService的Bean中的所有方法。
@annotation 用于匹配标注了指定注解的连接点。 @annotation(com.example.annotation.Log): 匹配标注了@Log注解的所有方法。

组合切点表达式:

可以使用逻辑运算符 && (与)、|| (或) 和 ! (非) 来组合多个切点表达式,构建更复杂的切点。

举个栗子:

// 匹配所有Service层的方法,并且方法名以"get"开头
@Pointcut("execution(* com.example.service.*.*(..)) && execution(* com.example.service.*.get*(..))")
public void serviceLayerGetMethods() {}

4. 通知(Advice):隐形战衣的功能模块

通知,是切面中定义的具体操作,它描述了在连接点上做什么。 你可以把通知想象成隐形战衣上的各种功能模块,比如隐身、加速、防御等等。 通知定义了增强代码的具体逻辑,例如日志记录、事务管理、安全控制等。

Spring AOP支持以下几种类型的通知:

通知类型 描述 执行时机
前置通知(Before Advice) 在连接点之前执行。 前置通知通常用于验证参数、记录日志等。 在目标方法执行之前。
后置通知(After Advice) 在连接点之后执行,无论连接点是否发生异常都会执行。 后置通知通常用于释放资源、记录日志等。 在目标方法执行之后(无论是否发生异常)。
返回后通知(AfterReturning Advice) 在连接点正常返回后执行。 返回后通知可以访问方法的返回值。 在目标方法正常返回之后。
异常通知(AfterThrowing Advice) 在连接点抛出异常后执行。 异常通知可以访问抛出的异常对象。 在目标方法抛出异常之后。
环绕通知(Around Advice) 包围连接点的通知。 环绕通知可以控制目标方法的执行,可以在方法执行前后做一些事情。 环绕通知必须手动调用ProceedingJoinPoint.proceed()来执行目标方法。 如果不调用proceed(),目标方法将不会被执行。 环绕通知是最强大的通知类型,可以实现各种复杂的AOP场景,例如性能监控、缓存、事务管理等。 在目标方法执行前后。

代码示例:

上面切面(LogAspect)的代码示例已经包含了各种类型的通知,这里不再赘述。

5. 织入(Weaving):将战衣穿到代码身上

织入,是将切面应用到目标对象并创建代理对象的过程。 你可以把织入想象成将隐形战衣真正穿到代码身上,让代码具备了AOP增强的功能。

织入可以在以下时机发生:

织入时机 描述
编译期织入 在编译期将切面织入到目标类中。 这需要使用特殊的编译器,例如AspectJ编译器。 编译期织入的性能最高,因为切面已经编译到目标类中,不需要在运行时进行额外的处理。
类加载期织入 在类加载时将切面织入到目标类中。 这需要使用特殊的类加载器,例如AspectJ的Load Time Weaving(LTW)。 类加载期织入的性能比编译期织入稍差,但比运行时织入要好。
运行时织入 在运行时将切面织入到目标对象中。 Spring AOP采用的是运行时织入的方式。 运行时织入的灵活性最高,可以在不修改目标类的情况下动态地添加或删除切面。 但是,运行时织入的性能最差,因为它需要在运行时进行额外的处理。 Spring AOP通过代理模式来实现运行时织入。 Spring AOP有两种代理方式:JDK动态代理和CGLIB代理。 如果目标类实现了接口,Spring AOP会使用JDK动态代理。 如果目标类没有实现接口,Spring AOP会使用CGLIB代理。 CGLIB代理是通过继承目标类来实现的。

Spring AOP的织入方式:

Spring AOP采用的是运行时织入的方式,通过代理模式来实现。 当Spring容器启动时,它会扫描所有的Bean,并根据切点表达式找到需要增强的Bean,然后为这些Bean创建代理对象。 当调用目标方法时,实际上是调用代理对象的方法,代理对象会在方法执行前后执行通知。

两种代理方式:

  • JDK动态代理: 如果目标类实现了接口,Spring AOP会使用JDK动态代理。 JDK动态代理是通过实现目标类的接口来创建代理对象。
  • CGLIB代理: 如果目标类没有实现接口,Spring AOP会使用CGLIB代理。 CGLIB代理是通过继承目标类来创建代理对象。 由于CGLIB是通过继承来实现的,因此CGLIB代理的目标类不能是final的,并且目标方法也不能是finalprivate的。

总结一下:

概念 解释 比喻
切面 包含切点和通知,定义了什么时间、什么地点、做什么事情。 隐形战衣的蓝图
连接点 程序执行过程中可以插入切面的点,例如方法的调用、异常的抛出等。 代码中的十字路口
切点 用于扫描程序中所有的连接点,并精确地定位到我们需要增强的连接点。 选择目标的雷达
通知 切面中定义的具体操作,描述了在连接点上做什么,例如日志记录、事务管理等。 隐形战衣的功能模块,例如隐身、加速、防御等。
织入 将切面应用到目标对象并创建代理对象的过程,让代码具备了AOP增强的功能。 将隐形战衣真正穿到代码身上

AOP的优势

  • 解耦: AOP可以将横切关注点(例如日志记录、事务管理、安全控制等)与核心业务逻辑分离,降低代码耦合度,提高代码的可维护性和可重用性。
  • 代码复用: 可以将横切关注点提取到切面中,然后在多个模块中重用这些切面,避免代码重复。
  • 灵活性: 可以在不修改目标类的情况下动态地添加或删除切面,提高代码的灵活性和可扩展性。

总结

Spring AOP是一个强大的工具,可以帮助我们更好地组织和管理代码,提高代码的质量和效率。 理解了切面、连接点、切点、通知和织入这几个核心概念,你就掌握了Spring AOP的精髓。 以后,你就可以像武林高手一样,灵活运用AOP,给你的代码穿上隐形战衣,让你的程序更加健壮和高效!

希望这篇文章能够帮助你更好地理解Spring AOP,并在实际开发中灵活运用。 记住,编程的乐趣在于不断学习和探索,祝你编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注