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
的,并且目标方法也不能是final
或private
的。
总结一下:
概念 | 解释 | 比喻 |
---|---|---|
切面 | 包含切点和通知,定义了什么时间、什么地点、做什么事情。 | 隐形战衣的蓝图 |
连接点 | 程序执行过程中可以插入切面的点,例如方法的调用、异常的抛出等。 | 代码中的十字路口 |
切点 | 用于扫描程序中所有的连接点,并精确地定位到我们需要增强的连接点。 | 选择目标的雷达 |
通知 | 切面中定义的具体操作,描述了在连接点上做什么,例如日志记录、事务管理等。 | 隐形战衣的功能模块,例如隐身、加速、防御等。 |
织入 | 将切面应用到目标对象并创建代理对象的过程,让代码具备了AOP增强的功能。 | 将隐形战衣真正穿到代码身上 |
AOP的优势
- 解耦: AOP可以将横切关注点(例如日志记录、事务管理、安全控制等)与核心业务逻辑分离,降低代码耦合度,提高代码的可维护性和可重用性。
- 代码复用: 可以将横切关注点提取到切面中,然后在多个模块中重用这些切面,避免代码重复。
- 灵活性: 可以在不修改目标类的情况下动态地添加或删除切面,提高代码的灵活性和可扩展性。
总结
Spring AOP是一个强大的工具,可以帮助我们更好地组织和管理代码,提高代码的质量和效率。 理解了切面、连接点、切点、通知和织入这几个核心概念,你就掌握了Spring AOP的精髓。 以后,你就可以像武林高手一样,灵活运用AOP,给你的代码穿上隐形战衣,让你的程序更加健壮和高效!
希望这篇文章能够帮助你更好地理解Spring AOP,并在实际开发中灵活运用。 记住,编程的乐趣在于不断学习和探索,祝你编程愉快!