[源码系列:手写spring] AOP第一节:切点表达式

        在本专栏之前的文章中已经带大家熟悉了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,其中包含两个通知方法 logBeforelogAfterlogBefore 方法在方法执行前记录开始时间和请求信息,而 logAfter 方法在方法执行后记录结束时间和执行时间。 

2. 配置切点表达式:

        在切面类中,我们使用 @Before@After 注解分别标注了 logBeforelogAfter 方法,并指定了切点表达式 "execution(* com.example.controller.*.*(..))"。这个切点表达式表示我们希望拦截所有 com.example.controller 包下的方法执行。
 

3. 启用AspectJ支持:

确保在Spring配置文件中启用了AspectJ的支持。可以通过以下配置实现:

 在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-expressionicon-default.png?t=N7T8https://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坐标

        
            org.aspectj
            aspectjweaver
            1.8.0
        
特征 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 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 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





 

你可能感兴趣的:(Spring源码剖析,java,spring,后端)