Skip to content

Latest commit

 

History

History
767 lines (543 loc) · 36.5 KB

File metadata and controls

767 lines (543 loc) · 36.5 KB

本章介绍 FIT 框架对 AOP 的实现。

2.1 AOP 概念

AOP(Aspect Oriented Programming),即面向切面编程。作为 OOP(Object Oriented Programming)的延续,AOP 以切面(Aspect)为基本单元,切面会切入各个类中,将其共有的实现特定功能的代码抽取到切面中,等到需要时再切入对象中去。这样的模式既不改变程序原有的行为,又实现了解耦的目的。从技术上来说,AOP 是通过代理机制实现的,由生成的代理完成了所抽取代码的功能。

下面先介绍一些核心的 AOP 概念和术语,这些术语可能并不是特别直观,请您保持耐心继续阅读,在本章结束后,相信您会对 FIT AOP 有一个详细的了解。

  • 切面(Aspect):一个关注点的模块化,这个关注点可能横切多个类。在企业级 Java 应用程序中,事务管理、日志记录等功能就是一些适合被横切关注的例子。
  • 连接点(JoinPoint):执行程序过程中的某个特定位置。
  • 通知(Advice):一个切面在特定的连接点执行的操作,不同类型的通知包括“around”,"before" 等。FIT 框架将通知建模为拦截器,并围绕着连接点维护一系列拦截器。
  • 切入点(PointCut):与连接点匹配的谓词。通知与切入点表达式相关联,并在与切入点匹配的连接点上运行。与切入点表达式匹配的连接点概念是 AOP 的核心,切入点使通知能够独立于面向对象的层次结构进行定位。
  • 目标(target):被通知的对象。
  • AOP 代理(AOP proxy):由 AOP 框架创建的代理对象,用于实现切面的行为。

2.2 FIT AOP 的能力

FIT AOP 模块是建立在 FIT IoC 模块之上的。FIT AOP 的核心链路全部由自研接口串联,可自由切换切面实现及动态代理的扩展,以实现注解式事务、重试、鉴权、缓存等上层能力。 下面介绍 FIT AOP 的关键能力:

实现了 AOP 的基础能力

  • Before:可在目标方法前插入逻辑。
  • After:可在目标方法后插入逻辑。
  • Around:可在目标方法前后插入逻辑。
  • AfterReturning:可在目标方法成功返回后插入逻辑。
  • AfterThrowing:可在目标方法抛出异常后插入逻辑。

可自定义扩展切面

目前, FIT AOP 已经实现了对自研的切面能力的支持。通过引入第三方包,还可以实现对 AspectJ 的切面能力的支持。

可自定义扩展动态代理

目前,FIT AOP 主要支持 JDK 动态代理和 ByteBuddy 代理这两种代理模式。

2.3 FIT AOP 的使用

说明:代码中出现的包、类、接口和注解等不一定在 FIT 源码中真实存在,仅作为示例使用

2.3.1 声明一个切面(Aspect)

一般来说,声明一个切面需要以下两个步骤: (1)表示该类是一个 Bean,可以通过在类上添加 @Component @Bean 等注解实现。 (2)表示该类是一个切面类,可以通过在类上添加 @Aspect 注解实现。 以下代码为例:

@Component
public class myAspect {
    // ...
}

如上所示,myAspect 类上带有 @Component 注解,表示该类是需要被容器进行管理的 Bean

@Component
@Aspect
public class myAspect {
    // ...
}

如上所示,myAspect 类上带有 @Aspect 注解,声明了 myAspect 是一个切面类。

切面可以具有方法和属性,这与其他任何类相同。同时它还应该包含切入点、通知的声明。

2.3.2 声明一个切入点(PointCut)

在 FIT AOP 中,切入点决定了哪些特定的连接点(JoinPoint)将被通知所拦截,这使我们可以自由控制通知在何时运行。换而言之,我们可以将切入点视为匹配 Bean 上的方法执行。

一个切入点的声明通常由两部分组成: (1)切入点表达式(PointCut expression):通过使用 @PointCut 注解表示,它准确地确定我们需要的方法执行。 (2)切入点签名(PointCut signature):由名称和任何参数组成,用作切入点签名的方法必须具有 void 返回类型。 以下代码为例,我们声明了一个名为myPointcut的切入点,该切入点与名为myTransfer的任何方法的执行匹配:

@Pointcut("execution(* myTransfer(..))") // the PointCut expression
public void myPointcut() {} // the PointCut signature

FIT AOP 支持的切入点指示符

FIT AOP 支持多种切入点指示符用于切入点表达式,,为便于用户使用和进行知识迁移,我们采用了类 AspectJ 的格式,并拓展出了 FIT 特有的指示符。

execution

用于匹配方法执行连接点,这是使用 FIT AOP 时使用最频繁的切入点指示符。 以下介绍execution表达式通常遵循的模式:

execution(modifiers-pattern?
            ret-type-pattern
            declaring-type-pattern?
            name-pattern(param-pattern)
            throws-pattern?)

以下是各个部分的详细说明:

  • modifiers-pattern(可选):匹配方法的访问修饰符,取值有 publicprivateproected。如果省略则不限制访问修饰符,支持 * 通配符。
  • ret-type-pattern:匹配方法的返回类型,取值有 Java 基础数据类型、Object、数组类型,可以使用 * 来匹配任何返回类型。
  • declaring-type-pattern(可选) :匹配切入点类(包路径 + 类型),支持 .*(一级包路径)、..*(多级包路径)通配符。
  • name-pattern:匹配方法名,支持 * 通配符。
  • param-pattern:匹配方法的参数列表,多个参数之间用逗号分隔,支持 *(任意一个参数)、..(任意个参数)通配符。
  • throws-pattern(可选):匹配方法可能抛出的异常,目前暂不支持。

以下代码为例,表示执行任何 public 方法:

execution(public * *(..))
within

用于匹配 within 表达式中包路径以及子包下任意类任意方法,支持通配符。 以下代码为例,匹配了在 modelengine.aop 包及其子包下的所有类所有方法:

within(modelengine.aop..*)
this

用于匹配方法所属代理类,不支持通配符。 有以下 2 种用法: (1)参数过滤:匹配的是参数类型和个数,个数在 this 括号中以逗号分隔,类型是在 this 括号中声明。 (2)参数绑定:匹配的是参数类型和个数,个数在 this 括号中以逗号分隔,类型是在通知方法中定义,并且必须在 argsName 中声明。 以下代码为例,匹配了代理对象是 modelengine.fit.demo.MyService` 类的目标对象中的所有连接点:

this(modelengine.fit.demo.MyService)
target

用于匹配方法所属被代理类,不支持通配符。 有以下两种用法: (1)参数过滤:匹配的是参数类型和个数,个数在 target 括号中以逗号分隔,类型是在 target 括号中声明。 (2)参数绑定:匹配的是参数类型和个数,个数在 target 括号中以逗号分隔,类型是在通知方法中定义,并且必须在 argsName 中声明。 以下代码为例,匹配了是 modelengine.fit.demo.MyService 类的目标对象的所有连接点:

target(modelengine.fit.demo.MyService)
this和target的不同点
  • this 作用于代理对象,target 作用于目标对象
  • this 表示目标对象被代理之后生成的代理对象和指定的类型匹配会被拦截,匹配的是代理对象。target 表示目标对象和指定的类型匹配会被拦截,匹配的是目标对象。
args

用于匹配当前执行的方法传入的参数为指定类型的执行方法。 有以下 2 种用法: (1)参数过滤:匹配的是参数类型和个数,个数在 args 括号中以逗号分隔,类型是在 args 括号中声明。 (2)参数绑定:匹配的是参数类型和个数,个数在 args 括号中以逗号分隔,类型是在通知方法中定义,并且必须在 argsName 中声明。 以下代码为例,匹配了任何采用单个参数的连接点,其中在运行时传递的参数是 Serializable

args(java.io.Serializable)
@annotation

用于匹配当前执行方法持有指定注解的方法,不支持通配符,支持嵌套注解查找。 有以下 2 种用法: (1)参数过滤:匹配的是参数类型和个数,个数在 @annotation 括号中以逗号分隔,类型是在 @annotation 括号中声明。 (2)参数绑定:匹配的是参数类型和个数,个数在 @annotation 括号中以逗号分隔,类型是在通知方法中定义,并且必须在 argsName 中声明。 以下代码为例,匹配了具有 Transactional 注解的执行方法中的任何连接点:

@annotation(modelengine.fitframework.transaction.Transactional)
@args

用于匹配目标对象方法的参数类型所属类上有指定的注解,不支持通配符,不支持单独使用,支持嵌套注解查找。 有以下 2 种用法: (1)参数过滤:匹配的是参数类型和个数,个数在 @args 括号中以逗号分隔,类型是在 @args 括号中声明。 (2)参数绑定:匹配的是参数类型和个数,个数在 @args 括号中以逗号分隔,类型是在通知方法中定义,并且必须在 argsName 中声明。 以下代码为例,匹配了任何采用单个参数的连接点,并且传递的参数所属的类具有 @Classified 注解:

@args(modelengine.security.Classified)
@target

用于匹配方法所属类上指定的注解,不支持通配符,不支持单独使用,支持嵌套注解查找。 有以下 2 种用法: (1)参数过滤:匹配的是参数类型和个数,个数在 @target 括号中以逗号分隔,类型是在 @target 括号中声明。 (2)参数绑定:匹配的是参数类型和个数,个数在 @target 括号中以逗号分隔,类型是在通知方法中定义,并且必须在 argsName 中声明。 以下代码为例,匹配了目标对象具有 Transactional 注解的所有连接点:

@target(modelengine.fitframework.transaction.Transactional)
@within

用于匹配方法声明类上指定的注解类,方法所属类是本类或者子类,不支持通配符,支持嵌套注解查找。 有以下 2 种用法: (1)参数过滤:匹配的是参数类型和个数,个数在 @within 括号中以逗号分隔,类型是在 @within 括号中声明。 (2)参数绑定:匹配的是参数类型和个数,个数在 @within 括号中以逗号分隔,类型是在通知方法中定义,并且必须在 argsName 中声明。 以下代码为例,匹配了具有 @Transactional 注解的类中的任何连接点:

@within(modelengine.fitframework.transaction.Transactional)
@within和@target的不同点
  • @target 用于判断被调用的目标对象中是否声明了指定注解,如果有,会被拦截。@within 用于判断被调用的方法所属的类中是否声明了指定注解,如果有,会被拦截。
  • @target 关注的是被调用的对象,@within 关注的是调用的方法所在的类(当有继承关系时会体现差异,@within 会对子类继承的方法生效,但是若子类的其他方法或者子类重写了父类的方法就不会再生效)
@params

用于匹配方法参数指定的注解,不支持通配符,不支持单独使用,支持嵌套注解查找。 以下代码为例,当方法参数中有包含 MyAnnotation 注解的参数时才会匹配:

@params(modelengine.MyAnnotation)

FIT AOP 支持的组合切入点表达式

在 FIT AOP 中,我们可以通过使用 &&||! 组合多个切入点表达式,我们还可以按名称引用切入点表达式。 以下代码为例:

public class Pointcuts {

    @Pointcut("execution(public * *(..))")
    public void publicMethod() {}

    @Pointcut("within(modelengine.trading..*)")
    public void inTrading() {}

    @Pointcut("publicMethod() && inTrading()")
    public void tradingOperation() {}
}

如上所示,如果是权限修饰符为 public 的方法,则 publicMethod() 匹配。如果方法执行在 trading 模块中,则 inTrading() 匹配。 如果方法执行为 trading 模块中的任何公共方法,则 tradingOperation() 匹配。

2.3.3 声明一个通知(Advice)

在 FIT AOP 中,通知与切入点表达式相关联,并可以在与切入点匹配的方法执行之前、之后等时间运行。切入点表达式可以是内联切入点,也可以是对已经命名好的切入点的引用。

Before 通知

Before 通知在匹配的方法执行前运行。在 FIT AOP 中,我们可以在切面中用 @Before 注解声明一个 Before 通知。 以下代码为例:

@Aspect
public class BeforeExample{

    @Before("execution(* modelengine.dao.*.*(..))")
    public void doBefore(){
        // ...
    }
}

我们还可以引用了已经命名好了的切入点:

@Aspect
public class BeforeExample{

    @Pointcut("execution(* modelengine.dao.*.*(..))")
    public void myPointcut(){
        // ...
    }

    @Before("myPointcut()")
    public void doBefore() {
        // ...
    }
}

After Returning 通知

After Returning 通知在匹配的方法正常返回时运行。在 FIT AOP 中,我们可以在切面中用 @AfterReturning 注解声明一个 After Returning 通知:

@Aspect
public class AfterReturningExample {

    @AfterReturning("execution(* modelengine.dao.*.*(..))")
    public void doAfterReturning() {
        // ...
    }
}

有时我们需要在通知中访问返回的实际值,可以使用绑定返回值的 @AfterReturning 形式来获取,以下代码为例:

@Aspect
public class AfterReturningExample {

   @AfterReturning(
        pointcut="execution(* modelengine.dao.*.*(..))",
        returning="retVal")
    public void doAfterReturning(Object retVal) {
        // ...
    }
}

returning 属性中使用的名称必须与 Advice 方法中的参数的名称相对应。当方法执行返回时,返回值将作为相应的参数值传递给 Advice 方法。returning 子句还将匹配限制为仅返回指定类型值的那些方法执行(在本例中为 Object,它匹配任何返回值)。

After Throwing 通知

当匹配的方法执行通过抛出异常而退出时,After Throwing 通知将运行。在 FIT AOP 中,我们可以在切面中用 @AfterThrowing 注解声明一个After Throwing 通知:

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("execution(* modelengine.dao.*.*(..))")
    public void doAfterThrowing() {
        // ...
    }
}

有时,我们希望仅在引发特定类型的异常时才运行通知,并且需要在通知中访问抛出的异常。我们可以通过 Throwing 属性来限制匹配,并将抛出的异常绑定到通知的参数,以下代码为例:

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="execution(* modelengine.dao.*.*(..))",
        throwing="ex")
    public void doAfterThrowing(DataAccessException ex) {
        // ...
    }
}

同样的,throwing 属性中使用的名称必须与 doAfterThrowing 方法中的参数名称相对应。当方法执行通过抛出异常而退出时,该异常将作为相应的参数值传递给 doAfterThrowing 方法。throwing 子句还将匹配限制为仅触发指定类型异常的那些方法执行(在本例中为DataAccessException)。

After(Finally)通知

当匹配的方法执行退出时,After(Finally)通知运行。在 FIT AOP 中,我们可以在切面中使用 @After 注解声明一个After(Finally)通知:

@Aspect
public class AfterFinallyExample {

    @After("execution(* modelengine.dao.*.*(..))")
    public void doAfterFinally() {
        // ...
    }
}

不同于仅适用于成功的正常返回的AfterRturning通知,After(Finally)通知将针对从连接点抛出的任何结果、正常返回或异常。这类似于try-catch 模式中的 finally。所以 After(Finally)通知应该准备好处理正常和异常等多种返回情况。

Around 通知

Around 通知“围绕”方法的执行运行。他有机会在方法运行之前和之后进行工作,并确定何时、如何以及是否实际运行。在需要在方法执行之前和之后共享状态(例如启动和停止计时器)的场景下,我们可以使用 Around 通知。 在 FIT AOP 中,我们可以在切面中使用 @Around 注解声明一个 Around 通知。Around 通知的方法应将 Object 声明为其返回类型,并且该方法的第一个参数必须是 ProceedingJoinPoint 类型。在方法的主体中,必须在 ProceedingJoinPoint 上调用 proceed() 实例才能运行基础方法。在不带参数的情况下调用 proceed() 会将调用者的原始数据提供给基础方法。proceed() 方法有一个重载变体,他接受一个参数数组 Object[] ,数组中的值在被调用时将用作基础方法的参数:

@Aspect
public class AroundExample {

    @Around("execution(* modelengine.service.*.*(..))")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }
}

访问当前的 JoinPoint

除了 Around 通知需要声明一个 ProceedingJoinPoint 类型(是 JoinPoint 的子类)的参数作为第一个参数,任何通知都可以声明一个JoinPoint 类型的参数作为其第一个参数。 在 FIT AOP 中,JoinPoint 接口给我们提供了一系列方法进行访问,以下列举一些:

  • getArgs():获取目标方法的参数数组。
  • getThis():获取当前正在执行的代理对象。
  • getTarget():获取增强处理的目标对象。
  • getSignature():获取目标方法的签名。
  • toString():连接点的字符串表现形式。

2.4 AOP 的代理机制

理解 AOP 代理

下面我们考虑这样一个场景:在企业级应用中,随着业务逻辑的增加,service 层中的代码变得臃肿、复杂、难以维护。为了解决这种问题,我们需要将其他的非核心的、附加的操作抽取出来,让 service 层只关心核心的业务。为此,我们使用了代理模式,编写代码创建一个代理类,将非核心功能写入代理类,并由创建的代理对象来执行这些功能。 使用这种代理模式的主要优点有:

  • 通过代理,我们可以在不修改原对象的情况下,增加一些额外的操作。
  • 通过代理,我们可以为多个对象提供一个通用的代理,从而实现跨切面的复用。
  • 通过代理,我们可以在原对象和客户端之间增加隔离层,从而隐藏原对象的一些实现细节。

FIT AOP 的代理

由于手动编写代码创建代理的过程过于复杂繁琐,为了降低工作量,FIT 框架对创建代理的代码进行了封装。用户只需创建一个切面类,将所有的非业务代码在切面类中完成,再将切面类和委托类分别用 @Component@AspectFit 等注解标注即可。FIT 框架底层会自动根据注解进行注入,调用切面类和委托类生成代理对象。这就使用户在使用 FIT AOP 时无需关注代理在底层的创建实现而直接使用代理。

总之, FIT AOP 是基于(动态)代理的。以下是 FIT 框架支持的代理模式:

  • FIT AOP 可以使用 JDK 动态代理,JDK 动态代理内置于 JDK 中,使得任何接口(或者接口集)都可以被代理。
  • FIT AOP 还可以使用 ByteBuddy 代理模式实现动态代理。

2.5 AOP API

2.5.1 FIT 中的 PointCut API

下面我们分析 FIT AOP 是如何解析切点表达式,匹配需要被拦截的方法的。 包:modelengine.fitframework.aop.interceptor.aspect.parser

参数传递

FIT 框架提供了 PointcutParameter 接口,主要用于获取和设置方法参数的相关信息,该接口被 DefaultPointcutParameter 类实现,用于在方法调用时传递切入点参数:

public class DefaultPointcutParameter implements PointcutParameter {

    private final String name;
    private final Class<?> type;
    private Object binding;

    public DefaultPointcutParameter(String name, Class<?> type) {
        this.name = name;
        this.type = type;
    }

    @Override
    public String getName() { return this.name; }

    @Override
    public Class<?> getType() { return this.type; }

    @Override
    public Object getBinding() { return this.binding; }

    @Override
    public void setBinding(Object boundValue) { this.binding = boundValue; }
}

如上所示,nametype 是通过构造函数传入的,分别代表切入点参数的名称和类型。binding 是一个 Object 类型的成员变量,用于存储参数绑定的值。getName() 方法用于获取参数名称,getType() 方法用于获取参数类型,getBinding() 方法用于获取运行时方法的参数值,setBinding() 方法用于设置运行时方法的参数值。

解析器

FIT 框架提供了 ExpressionParser 接口用于表达式解析,定义了表达式的解析、内容获取、关键字匹配等方法。同时内嵌了内部接口 Result 用于表示解析结果,接口内包含了用于检测运行的类是否能匹配上的 couldMatch 方法,用于检测表达式是否是参数绑定的 isBinding 方法等,用于对解析结果进行处理。FIT 用 BaseParser 类实现了 ExpressionParser 接口,以此基类作为切点表达式解析器的通用抽象实现。通过继承基类,并提供解析逻辑,我们就可以设计出一个个需要的解析器实现类,提供ExpressionParser 接口中 parse() 方法的具体实现。

运算符解析器

FIT 框架中有运算符 && 的解析器 AndParser,运算符 ! 的解析器 NotParser,运算符 || 的解析器 OrParser ,运算符 ( 的解析器LeftBracketParser 和运算符 ) 的解析器 RightBracketParser(运算符 () 的解析器一般用于在 pointcut 解析时自动添加,来解决多个pointcut 运算优先级的问题)。下面以 AndParser 为例介绍:

public class AndParser extends BaseParser {

    @Override
    protected PointcutSupportedType parserType() {
        return PointcutSupportedType.AND;
    }

    @Override
    protected Result createConcreteParser(String content) {
        return new AndResult(content);
    }

    class AndResult extends BaseParser.BaseResult {
        public AndResult(String expression) {
        super(expression, null);
    }
}

如上所示,每个运算符解析器都重写了父类 BaseParserparserType() 方法用于返回解析器类型。重写了 createConcreteParser() 方法,用于根据解析内容创建并返回一个解析器实例。AndResult 类是一个内部类,它接受一个表达式作为参数,并将表达式传给 BaseResult 的构造函数。

关键字解析器

同样的,通过继承 BaseParser 类也可创建一系列关键字解析器。例如关键字 within 的解析器 WithParser ,关键字 target 的解析器TargetParser,关键字 execution 的解析器 ExecutionParser 等(由于 execute 表达式较为复杂,我们还构建了 ExecutionExpression 类用于execute 表达式的正则匹配,并将表达式解析结果包装为一个 ExecutionModel 对象)。

FIT 框架用 DefaultPointcutParser 类实现了 PointcutParser 接口,其中的 parse() 方法实现了切点表达式的解析,它遍历所有的表达式,对每个表达式找到能够解析它的解析器并进行解析。如果解析结果的类型是被支持解析的类型,那么就再次调用 DefaultPointcutParser 进行递归的解析,将解析结果加入到结果列表中,直到解析结果类型不再是被支持的类型。 getParserList 方法则可以根据切面类和切点参数,调用 PointcutParserFactory 生成一个表达式解析器列表并返回。

2.5.2 FIT 中的 Advice API

包:modelengine.fitframework.aop.interceptor

拦截器

FIT 框架提供了一个用于处理 AOP 的最基本的接口 MethodInterceptor

public interface MethodInterceptor {

    @Nonnull
    MethodPointcut getPointCut();

    @Nullable
    Object intercept(@Nonnull MethodJoinPoint methodJoinPoint) throws Throwable;
}

如上所示,getPointcut() 方法用于获取方法拦截器所定义的方法切点集合。intercept() 方法执行拦截逻辑。如果在执行拦截逻辑时抛出异常,则需要根据异常类型进行相应的处理。FIT 中的 AbstractMethodInterceptor 类实现了该接口,作为方法拦截器的通用抽象实现。

为了获取上下文信息,FIT 框架还提供了 AdviceMethodInterceptor 接口,它继承了上面提到的MethodInterceptor接口:

public interface AdviceMethodInterceptor extends MethodInterceptor {

    @Nullable
    Object getAdvisorTarget();

    @Nonnull
    Method getAdvisorMethod();
}

如上所示,getAdvisorTarget() 方法用于获取调用通知的对象,getAdvisorMethod() 方法用于获取调用通知的方法。FIT 框架用AbstractAdviceMethodInterceptor 类实现了该 AdviceMethodInterceptor 接口,并继承了 AbstractMethodInterceptor 类的部分方法,作为带建议的方法拦截器的通用抽象实现。

通过继承 AbstractAdviceMethodInterceptor 类,我们可以为每一种通知创建一个方法拦截器(命名规则:xxxxInterceptor),在其中编入拦截器所需的逻辑。再通过用 AspectxxxxInterceptor 继承 xxxxInterceptor 类,我们就获得了 xxxxInterceptor 的 Aspect 实现。以AspectAfterInterceptor 为例:

public class AspectAfterInterceptor extends AfterInterceptor {

    public AspectAfterInterceptor(BeanFactory aspectFactory, Method interceptMethod) {
        // ...
    }

    private void validateParameter(Method interceptMethod) {
        // ...
    }

    @Override
    protected Object[] getAdvisorArgs(MethodJoinPoint joinPoint, @Nullable Object returnValue,
            @Nullable Throwable throwable) {
        // ...
    }
}

如上所示,构造函数通过真实拦截对象的工厂 BeanFactory 和真实拦截方法 Method 来实例化一个 AspectAfterInterceptor 对象,并调用父类的构造函数validateParameter() 方法用于进行参数校验(第一个参数类型不能是 ProceedingJoinPoint),getAdvisorArgs() 方法则可以用于获取拦截器的参数。

拦截器工厂

FIT 框架提供了 MethodInterceptorFactory 接口,表示方法拦截器的工厂:

public interface  MethodInterceptorFactory {

    boolean isInterceptMethod(@Nonnull Method method);

    MethodInterceptor create(BeanFactory factory, @Nonnull Method interceptMethod);
}

如上所示,isInterceptMethod() 方法用于判断指定方法是不是一个方法拦截器的定义。create() 方法可以根据指定方法以及其所在的 Bean 的工厂,创建方法拦截器。具体的创建逻辑也需要在实现这个接口的类中实现。AbstractAnnotatedInterceptorFactory 类实现了该接口,表示通过方法注解来生成方法拦截器的工厂。 FIT 中的 AbstractAspectInterceptorFactory 类继承自 AbstractAnnotatedInterceptorFactory,并实现了 MethodInterceptorFactory 接口。它是一个抽象的切面拦截器工厂类,用于将带有 FIT AOP 特定注解的方法包装成 AdviceMethodInterceptor。通过继承 AbstractAspectInterceptorFactory类,我们可以为每一种通知创建一个专门的工厂类(命名规则为AspectxxxxInterceptorFactory)用于创建各种拦截器。以下以 AspectAfterInterceptorFactory类为例,通过这个工厂类,可以将带有@After注解的方法包装成AdviceMethodInterceptor(本例中为AspectAfterInterceptor):

public class AspectAfterInterceptorFactory extends AbstractAspectInterceptorFactory {

    public AspectAfterInterceptorFactory() { super(After.class); }

    @Override
    protected AdviceMethodInterceptor createConcreteMethodInterceptor(BeanFactory aspectFactory, Method method) {
        return new AspectAfterInterceptor(aspectFactory, method);
    }

   @Override
    protected String getExpression(@Nonnull Method method) {
        return this.getAnnotations(method).getAnnotation(After.class).pointcut();
    }

    @Override
    protected String getArgNames(@Nonnull Method method) {
        return this.getAnnotations(method).getAnnotation(After.class).argNames();
    }
}

如上所示,在构造函数中指定了注解类型为 After.class,重写了 createConcreteMethodInterceptor() 方法,用于创建并返回具体的拦截器,在本例中为 AspectAfterInterceptor 类型。重写了 getExpression()getArgNames() 方法,分别用于获取注解中的 pointcutargNames 属性。

MethodInterceptor 执行顺序

对于被定义在同一个切面类中的增强方法,FIT 将根据它们的增强类型按以下顺序分配优先级,从高到低的优先级依次为: 1.@Around 2.@Before 3.@After 4.@AfterReturning 5.@AfterThrowing 当不论是否在同一个切面类中的同类型增强(建议)方法想要在同一个连接点执行时,默认的执行顺序是未定义的。默认情况下,如果两个不同切面的同一类型的拦截器在同一个连接点执行时,会有两种都合理的执行情况(方法1前方法2后或者方法2前方法1后)。但在一些特殊场景下,我们要求两者之间有严格的执行顺序,此时我们可以通过设置优先级来控制执行顺序,即通过 modelengine.fitframework.annotation.Order 注解实现。以下代码为例:

@Order(1)
public class TestAspect1 {
    /**
     * 表示一个方法前置拦截器
     */
    public void before1() {
    }
}
@Order(-1)
public class TestAspect2 {
    /**
     * 表示一个方法前置拦截器
     */
    public void before2() {
    }
}

通过在类上添加 @Order 注解,可以实现对执行顺序的控制。对于给定的两个 @Order 注解,Order.value() 的返回值越低,那么该类的优先级越高。 此外,@Order 注解同样可以添加在方法上,我们考虑下面一种特殊的场景: 现在有两个切面类 TestAspectATestAspectB, 要求 TestAspectA 中除了前置拦截器外的其他拦截器的优先级要高于 TestAspectB。 以下代码为例:

@Order(-1)
public class TestAspectA {
    /**
     * 表示一个方法前置拦截器
     */
    @Order(2)
    public void before1() {
    }

    /**
     * 表示一个方法环绕拦截器
     */
    public void around1() {
    }
}
@Order(1)
public class TestAspectB {
    /**
    * 表示一个方法前置拦截器
    */
    public void before2() {
    }

    /**
    * 表示一个方法环绕拦截器
    */
    public void around2() {
    }
}

对于增强(建议)方法,若要通过 @Order 获取优先级,首先会从方法的 @Order 注解中获取,如果没有注解,则从类的 @Order 注解中获取。如果都没有,则为 @Order 注解的默认值。在 FIT 框架中,对 MethodInterceptor 列表的排序是通过 MethodInterceptorComparator 类实现的。

2.5.3 AOP 代理

当前, FIT 主要支持 Jdk动态代理和ByteBuddy动态代理这两种代理方式。

包:modelengine.fitframework.aop.proxy

代理对象

FIT 框架为用户提供了 FITProxy 接口,作为 FIT 生成的代理的统一接口:

public interface FITProxy {

    Class<?> $fit$getActualClass();
}

如上所示,其中的 $fit$getActualClass() 方法被用于获取代理对象的真正类型。

通过实现FITProxy接口,FIT 创建出 AbstractAopProxy 类作为 AOP 调用的核心抽象代理。其中提供的 invoke() 方法包含了 AOP 的核心调用逻辑,在方法被调用时,根据调用方法过滤出所有被代理对象的方法拦截器(如果没有拦截器则直接调用被代理对象,FIT 会在过滤后的方法拦截器列表最后添加一个 ProxiedInterceptor 用于调用被代理对象),然后从最后一个拦截器开始以此向前构造当前方法拦截器的参数,在构造完所有方法拦截器的参数后,调用第一个拦截器的 intercept() 方法。

FIT 框架中的 JdkDynamicProxy 类和 ByteBuddyAopProxy 类都继承了 AbstractAopProxy 类并实现了 InvocationHandler 接口,作为 Jdk动态代理ByteBuddy代理 的回调。 以下仍以 JdkDynamicProxy 类为例:

public class JdkDynamicProxy extends AbstractAopProxy implements InvocationHandler {

    public JdkDynamicProxy(InterceptSupport support) {
        super(support);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        // ...
    }
}

如上所示,我们可以传入一个包含了所有的拦截器信息的 InterceptSupport 实例,用于实例化 JdkDynamicProxy。当调用代理对象的方法时,会调用invoke()方法,以实现代理对象的逻辑。

InvocationHandler 接口是 Java 提供的一个接口,是 Java 反射机制的一部分,InvocationHandler 接口只有一个方法:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

这个方法在代理对象的方法被调用时执行。方法的参数包括:

  • proxy:代理对象。
  • method:被调用的方法。
  • args:方法的参数数组。

代理工厂

FIT 框架提供了AopProxyFactory接口,表示 AOP 代理的工厂:

public interface AopProxyFactory {

    boolean support(Class<?> targetClass);

    Object createProxy(InterceptSupport support);

    static List<AopProxyFactory> all() {
        // ...
    } 
}

如上所示,接口提供了support()方法用于判断指定类型是否可以创建 AOP 代理。提供了 createProxy() 方法用于通过指定的拦截事件支持信息创建 AOP 代理。而 all() 方法则用于获取所有按照@Order注解的值排序好的 AopProxyFactory 实例。

通过实现 AopProxyFactory 接口, FIT 创建出 JdkDynamicAopProxyFactory 类和 ByteBuddyAopProxyFactory 类,分别作为 JDK动态代理 的代理工厂和 ByteBuddy代理 的代理工厂。

JdkDynamicAopProxyFactory 类为例,重写的 support() 方法判断是否支持创建代理对象(检查目标类是否是接口、代理类或 Lambda 表达式)。重写的 createProxy() 方法先获取目标类和 FitProxy 类的公共子类加载器,确保代理对象和目标对象在同一个类加载器下。然后使用 newProxyInstance() 方法创建出一个新的代理实例。

newProxyInstance() 方法是 Java 中创建代理实例的方法,以 JdkDynamicAopProxyFactory 中的代码为例:

return Proxy.newProxyInstance(classLoader, new Class[] {support.getTargetClass(), FitProxy.class}, new JdkDynamicProxy(support));

如上所示,它接受了三个参数:

  • classLoader:用于加载代理类的类加载器。
  • interfaces:这是一个数组,表示代理类实现的接口列表。本例中,传入的接口数组是目标类和 FitProxy类,通过 support.getTargetClass() 以及FitProxy.class 传入。
  • handler:这是一个 InvocationHandler 实例,用于处理代理对象的方法调用。本例中即为 JdkDynamicProxy 实例。

方法的返回值是一个实现了interfaces中所有接口的代理对象。当调用这个代理对象的方法时,会调用 handlerinvoke() 方法。

AOP 代理对象的创建

FIT 框架定义了 InterceptSupport 接口,用于获取被代理对象的信息,DefaultInterceptSupport 类实现了该接口。运用它们提供的方法,我们可以实例化一个 InterceptSupport,用其创建出一个代理对象,本例中使用 ByteBuddy 代理模式:

InterceptSupport support = new DefaultInterceptSupport(xxxxxxxxx);
AopProxyFactory aopProxyFactory = new ByteBuddyAopProxyFactory();
Object proxy = aopProxyFactory.createProxy(support);

如上所示,在构造函数中传入被代理对象类型、被代理对象提供者、指定方法拦截器列表,可以创建出一个 InterceptSupport 对象 support 。再将拦截器支持信息传入 AopProxyFactory 中,我们就可以使用代理工厂的 createProxy() 方法实例化出一个代理对象 proxy。将 proxy 进行类型转换后即可调用其方法。