在本专栏之前的文章中已经带大家熟悉了Spirng中核心概念IOC的原理以及手写了核心代码,接下来将继续介绍Spring中另一核心概念AOP。
AOP即切面编程是Spring框架中的一个关键概念,它允许开发者在应用程序中优雅地处理横切关注点,如日志记录、性能监控和事务管理。在切面编程中,切点表达式是一项关键技术,它定义了在何处应用切面的逻辑。本章将深入探讨Spring切点表达式的实现原理,为读者提供对这一重要概念的深刻理解。
1.AOP案例
1.1 案例背景
假设我们有一个在线商城的Web应用,用户可以浏览商品、下单购买商品等。我们希望记录每个HTTP请求的开始时间、结束时间以及执行时间,以便监控应用的性能并快速定位潜在的问题。
1.2 AOP解决方案
我们可以使用Spring AOP和切点表达式来实现这个日志记录功能。以下是实现步骤:
1. 创建一个切面类:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RequestLoggingAspect {
private long startTime;
@Before("execution(* com.example.controller.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
startTime = System.currentTimeMillis();
System.out.println("Request received for: " + joinPoint.getSignature().toShortString());
}
@After("execution(* com.example.controller.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
System.out.println("Request completed for: " + joinPoint.getSignature().toShortString());
System.out.println("Execution Time: " + executionTime + "ms");
}
}
上述代码定义了一个切面类 RequestLoggingAspect
,其中包含两个通知方法 logBefore
和 logAfter
。logBefore
方法在方法执行前记录开始时间和请求信息,而 logAfter
方法在方法执行后记录结束时间和执行时间。
2. 配置切点表达式:
在切面类中,我们使用 @Before
和 @After
注解分别标注了 logBefore
和 logAfter
方法,并指定了切点表达式 "execution(* com.example.controller.*.*(..))"
。这个切点表达式表示我们希望拦截所有 com.example.controller
包下的方法执行。
3. 启用AspectJ支持:
确保在Spring配置文件中启用了AspectJ的支持。可以通过以下配置实现:
<aop:aspectj-autoproxy />
在Spring Boot应用的配置类中,保证@Aspect标记的切面类正确被容器管理即可。
4. 结果:
当用户发起HTTP请求时,切面会自动拦截匹配的方法,记录请求的开始时间和结束时间,并输出到日志中。这样,我们就能够实时监控每个请求的性能,并在需要时进行故障排除。
2. 知识补充
2.1 切点和连接点的概念
在Spring框架中,切点(Pointcut)和连接点(JoinPoint)是实现切面编程的两个核心概念。切点定义了在应用程序中哪些地方切入(或触发)切面的逻辑,而连接点则代表在应用程序执行过程中的具体执行点。连接点可以是方法的调用、方法的执行、异常的抛出等。理解这两个概念是理解切点表达式的基础。
- 连接点(JoinPoint)是程序执行的特定点,它可以是方法的执行、方法的调用、对象的创建等。在Spring AOP中,连接点通常表示方法的执行。连接点是AOP切面可以插入的地方,例如,我们可以在方法调用之前或之后插入额外的逻辑。
- 切点(Pointcut)是一个表达式,它定义了连接点的集合。换句话说,切点确定了在哪些连接点上切入切面逻辑。Spring框架支持多种切点表达式的定义,其中最常用的是AspectJ切点表达式。
2.1.1 AspectJ切点表达式
AspectJ是一种强大的面向切面编程(AOP)语言,Spring框架引入了AspectJ切点表达式以方便开发者定义切点。AspectJ切点表达式使用一种类似于正则表达式的语法来匹配连接点。
AspectJ切点表达式的语法包括以下几个关键部分:
- execution关键字:用于指定要匹配的方法执行连接点。
//匹配com.example.service包中的所有类的所有方法
execution(* com.example.service.*.*(..))
- 访问修饰符和返回类型:可以使用通配符来匹配任意修饰符或返回类型。
//匹配任何公共方法的执行。
execution(public * com.example.service.*.*(..))
- 包和类的限定符:用于指定包和类的名称,通配符`*`可用于匹配任意字符。
//匹配com.example.service包中UserService类的所有方法执行。
execution(* com.example.service.UserService.*(..))
- 方法名:可以指定具体的方法名或使用通配符匹配多个方法。
//匹配UserService类中以"get"开头的所有方法执行。
execution(* com.example.service.UserService.get*(..))
- 参数列表:可以使用“ (..) ”来匹配任意参数列表。
//匹配UserService类的所有方法执行,无论参数列表如何
execution(* com.example.service.UserService.*(..))
AspectJ切点表达式的灵活性使开发者能够定义精确的切点,以满足不同的应用需求。通过深入学习和掌握AspectJ切点表达式,开发者可以更好地利用Spring AOP来管理应用程序中的横切关注点。接下来,我们将深入研究切点表达式的实现原理,以更好地理解Spring框架是如何解析和匹配这些表达式的。
3. 实现原理
3.1代码分支
https://github.com/yihuiaa/little-spring/tree/pointcut-expressionhttps://github.com/yihuiaa/little-spring/tree/pointcut-expression
3.2 核心代码
ClassFilter 和 MethodMatcher 接口
- ClassFilter:该接口用于筛选出应该应用切面的目标类。在Pointcut表达式中,如果没有指定特定的目标类,ClassFilter将返回true,表示匹配任何类。否则,它将根据指定的规则筛选出匹配的类。
- MethodMatcher:这个接口用于匹配目标类中的方法。MethodMatcher决定了哪些方法会成为连接点,从而被切面拦截。MethodMatcher接口包括两个方法:`matches(Method method, Class<?> targetClass)` 用于匹配方法,和 `isRuntime()` 用于表示匹配是否需要在运行时进行动态计算。
AspectJExpressionPointcut 的简单实现
- 表达式解析:首先,AspectJExpressionPointcut会将切点表达式进行解析,将其转化为内部的数据结构,以便进行进一步处理。这个解析过程涉及到词法分析和语法分析,以确保切点表达式的语法正确性。
- 连接点匹配:一旦切点表达式被解析,AspectJExpressionPointcut 将会使用 ClassFilter 和 MethodMatcher 接口来匹配连接点。它会遍历应用程序中的类和方法,根据表达式的定义,确定哪些连接点符合切点表达式的要求。
- 运行时动态匹配:在某些情况下,切点表达式可能需要在运行时动态计算。例如,当表达式中包含参数绑定时,需要在实际方法执行时才能确定是否匹配。AspectJExpressionPointcut会在运行时进行动态匹配,以确保准确的连接点匹配。
下面将借助aspectjweaver的功能简单实现Spring AOP切点表达式功能,实现对execution函数的支持。
3.2.1 首先添加maven坐标
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.0</version>
</dependency>
特征 | Spring AOP | AspectJ |
---|---|---|
编程模型 | 基于代理的编程模型,使用 Spring 代理生成 AOP 代理。 | 纯粹基于注解或 XML 的编程模型,使用 AspectJ 编译器或运行时织入器。 |
编织方式 | 运行时织入,通过代理包装目标对象来添加切面行为。 | 支持编译时织入和运行时织入,更灵活且功能更强大。 |
性能 | 由于使用代理,性能开销较小,但有些限制。 | 性能较好,编译时织入可以最小化运行时开销。 |
支持的切入点表达式(Pointcut) | 仅支持一部分切入点表达式,如方法执行(execution)。 | 支持广泛的切入点表达式,包括访问、调用、初始化等多种方式。 |
复杂度 | 适用于简单的切面需求,易于配置和使用。 | 适用于复杂的切面需求,提供更多高级功能和灵活性。 |
集成度 | 紧密集成到 Spring 框架中,易于使用和配置。 | 相对独立,需要额外配置 AspectJ 编译器或运行时织入器。 |
配置方式 | 使用 Spring 的注解或 XML 配置来定义切面。 | 使用 AspectJ 注解或 XML 配置来定义切面。 |
3.2.2 ClassFilter接口
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
3.2.3 MethodMatcher接口
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
}
3.2.4 Pointcut 切点接口
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
3.2.5 AspectJExpressionPointcut 切点表达式类
/**
* ● @author: YiHui
* ● @date: Created in 17:33 2023/9/24
* ● @Description: 这是一个自定义的 AspectJ 表达式切点,用于在 Spring AOP 中匹配切点表达式。
*/
public class AspectJExpressionPointcut implements Pointcut, ClassFilter, MethodMatcher {
// 支持的切点原语集合
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();
static {
// 添加支持的切点原语。在此示例中,我们仅支持 EXECUTION 原语,您可以根据需要添加更多。
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
}
// 切点表达式对象,用于解析和匹配切点
private final PointcutExpression pointcutExpression;
/**
* 构造函数,用给定的表达式创建 AspectJ 表达式切点。
*
* @param expression 切点表达式,用于定义匹配的切点
*/
public AspectJExpressionPointcut(String expression) {
// 创建一个 PointcutParser 实例,用于解析切点表达式
PointcutParser pointcutParser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(
SUPPORTED_PRIMITIVES, this.getClass().getClassLoader());
// 解析给定的切点表达式并将其分配给成员变量 pointcutExpression
pointcutExpression = pointcutParser.parsePointcutExpression(expression);
}
/**
* 检查给定的类是否符合切点表达式的条件。
*
* @param clazz 要检查的类
* @return 如果类匹配切点表达式,则返回 true,否则返回 false
*/
@Override
public boolean matches(Class<?> clazz) {
return pointcutExpression.couldMatchJoinPointsInType(clazz);
}
/**
* 检查给定的方法是否符合切点表达式的条件。
*
* @param method 要检查的方法
* @param targetClass 方法所属的目标类
* @return 如果方法匹配切点表达式,则返回 true,否则返回 false
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
// 使用切点表达式检查方法执行是否匹配
return pointcutExpression.matchesMethodExecution(method).alwaysMatches();
}
/**
* 获取用于类筛选的 ClassFilter 实例。
*
* @return ClassFilter 实例,用于过滤匹配的类
*/
@Override
public ClassFilter getClassFilter() {
return this;
}
/**
* 获取用于方法匹配的 MethodMatcher 实例。
*
* @return MethodMatcher 实例,用于匹配符合切点表达式的方法
*/
@Override
public MethodMatcher getMethodMatcher() {
return this;
}
}
4. 测试
4.1测试代码
public class HelloService {
public String hello() {
System.out.println("hello word! yihuiComeOn");
return "hello word! yihuiComeOn";
}
}
public class PointcutExpressionTest {
@Test
public void testPointcutExpression() throws Exception {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut("execution(* service.HelloService.*(..))");
Class<HelloService> clazz = HelloService.class;
Method method = clazz.getDeclaredMethod("hello");
System.out.println("切点表达式匹配结果-类匹配:"+pointcut.matches(clazz));
System.out.println("切点表达式匹配结果-方法匹配:"+pointcut.matches(method, clazz));
}
}
4.2 测试结果
切点表达式匹配结果-类匹配:true
切点表达式匹配结果-方法匹配:true