如何实现一个简易版的 Spring - 如何实现 AOP(下)

前言

前面两篇 如何实现 AOP(上)如何实现 AOP(中) 做了一些 AOP 的核心基础知识简要介绍,本文进入到了实战环节了,去实现一个基于 XML 配置的简易版 AOP,虽然是简易版的但是麻雀虽小五脏俱全,一些核心的功能都会实现,通过实现这个简易版的 AOP,相信你会对 AOP 有深入的理解,不止知其然,还能知其所以然。AOP 的顶层接口规范和底层依赖基础组件都是由一个叫 AOP Alliance 的组织制定的,我们经常听到的 AspectJ、ASM、CGLIB 就是其中被管理的一些项目,需要明确的一点是,在 Spring 中只是使用了 AspectJ 的核心概念和核心类,并不是像 AspectJ 那样在编译期实现的 AOP,而是在运行期。话不多说,下面开始进入主题。

解析 XML 中的 pointcut 定义及方法解析

假设有一个 OrderService 类(P.S. 这里的 @Component 是我自定义的注解,详见 这篇),其中有一个下单的方法 placeOrder(),我们想实现的效果是想给这个 placeOrder() 方法加上 数据库事务,即执行方法之前开启事务,执行过程中发生异常回滚事务,正常执行完成提交事务。OrderService 类的代码如下:

/**
 * @author mghio
 * @since 2021-06-06
 */
@Component(value = "orderService")
public class OrderService {

  public void placeOrder() {
    System.out.println("place order");
  }

}  

很明显,这里的 pointcut 就是 placeOrder() 方法,在 XML 配置文件中的配置如下:

<aop:pointcut id="placeOrder" expression="execution(* cn.mghio.service.version5.*.placeOrder(..))"/>

我们需要一个类去表达这个概念,pointcut 要实现的功能是给定一个类的方法,判断是否匹配配置文件中给定的表达式。总的来看 pointcut 由方法匹配器和匹配表达式两部分组成,方法匹配器可以有各种不同的实现,所以是一个接口,pointcut 同样也可以基于多种不同技术实现,故也是一个接口,默认是基于 AspectJ 实现的,类图结构如下:

pointcut-implemented.png

实现类 AspectJExpressionPointcut 是基于 AspectJ 实现的,方法的匹配过程是委托给 AspectJ 中的 PointcutExpression 来判断给定的方法是否匹配表达式,该类的核心实现如下:

/**
 * @author mghio
 * @since 2021-06-06
 */
public class AspectJExpressionPointcut implements Pointcut, MethodMatcher {

  private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();

  static {
    SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
  }

  private String expression;
  private ClassLoader pointcutClassLoader;
  private PointcutExpression pointcutExpression;

  @Override
  public MethodMatcher getMethodMatcher() {
    return this;
  }

  @Override
  public String getExpression() {
    return expression;
  }

  @Override
  public boolean matches(Method method) {
    checkReadyToMatch();

    ShadowMatch shadowMatch = getShadowMatch(method);
    return shadowMatch.alwaysMatches();
  }

  private void checkReadyToMatch() {
    if (Objects.isNull(getExpression())) {
      throw new IllegalArgumentException("Must set property 'expression' before attempting to match");
    }
    if (Objects.isNull(this.pointcutExpression)) {
      this.pointcutClassLoader = ClassUtils.getDefaultClassLoader();
      this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
    }
  }

  private PointcutExpression buildPointcutExpression(ClassLoader classLoader) {
    PointcutParser pointcutParser = PointcutParser
        .getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(SUPPORTED_PRIMITIVES, classLoader);
    return pointcutParser.parsePointcutExpression(replaceBooleanOperators(getExpression()));
  }

  private String replaceBooleanOperators(String pcExpr) {
    String result = StringUtils.replace(pcExpr, " and ", " && ");
    result = StringUtils.replace(result, " or ", " || ");
    result = StringUtils.replace(result, " not ", " ! ");
    return result;
  }

  private ShadowMatch getShadowMatch(Method method) {
    ShadowMatch shadowMatch;
    try {
      shadowMatch = this.pointcutExpression.matchesMethodExecution(method);
    } catch (Exception e) {
      throw new RuntimeException("not implemented yet");
    }
    return shadowMatch;
  }

  // omit other setter、getter ...

}

到这里就完成了给定一个类的方法,判断是否匹配配置文件中给定的表达式的功能。再来看如下的一个完整的 AOP 配置:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.e3.org/2001/XMLSchema-instance"
  xmlns:aop="http://www.springframework.org/schema/aop"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/beans/spring-context.xsd">

  <context:scann-package base-package="cn.mghio.service.version5,cn.mghio.dao.version5" />

  <bean id="tx" class="cn.mghio.tx.TransactionManager"/>

  <aop:config>
    <aop:aspect ref="tx">
      <aop:pointcut id="placeOrder" expression="execution(* cn.mghio.service.version5.*.placeOrder(..))"/>
      <aop:before pointcut-ref="placeOrder" method="start"/>
      <aop:after-returning pointcut-ref="placeOrder" method="commit"/>
      <aop:after-throwing pointcut-ref="placeOrder" method="rollback"/>
    </aop:aspect>
  </aop:config>
</beans>

在实现各种 XXXAdvice 之前需要定位到这个 Method,比如以上配置文件中的 start、commit、rollback 等方法,为了达到这个目标我们还需要实现的功能就是根据一个 Bean 名称(比如这里的 tx)定位到指定的 Method,然后通过反射调用这个定位到的方法。实际上也比较简单,这个类命名为 MethodLocatingFactory,根据其功能可以定义出目标 Bean 的名称 targetBeanName、需要定位的方法名称 methodName 以及定位完成后得到的方法 method 这三个属性,整体类图结构如下所示:

method-locating.png

根据名称和类型定位到方法主要是在 setBeanFactory() 方法中完成的,前提是对应的目标 Bean 名称和方法名称要设置完成,方法定位的类 MethodLocatingFactory 类的代码如下所示:

/**
 * @author mghio
 * @since 2021-06-06
 */
public class MethodLocatingFactory implements FactoryBean<Method>, BeanFactoryAware {

  private String targetBeanName;

  private String methodName;

  private Method method;

  public void setTargetBeanName(String targetBeanName) {
    this.targetBeanName = targetBeanName;
  }

  public void setMethodName(String methodName) {
    this.methodName = methodName;
  }

  @Override
  public void setBeanFactory(BeanFactory beanFactory) {
    if (!StringUtils.hasText(this.targetBeanName)) {
      throw new IllegalArgumentException("Property 'targetBeanName' is required");
    }
    if (!StringUtils.hasText(this.methodName)) {
      throw new IllegalArgumentException("Property 'methodName' is required");
    }

    Class<?> beanClass = beanFactory.getType(this.targetBeanName);
    if (Objects.isNull(beanClass)) {
      throw new IllegalArgumentException("Can't determine type of bean with name '" + this.targetBeanName);
    }

    this.method = BeanUtils.resolveSignature(this.methodName, beanClass);
    if (Objects.isNull(this.method)) {
      throw new IllegalArgumentException("Unable to locate method [" + this.methodName + "] on bean ["
          + this.targetBeanName + "]");
    }
  }

  @Override
  public Method getObject() {
    return this.method;
  }

  @Override
  public Class<?> getObjectType() {
    return Method.class;
  }
}

实现各种不同类型的 Advice

各种不同类型的 Advice(BeforeAdvice、AfterAdvice 等)目标都是需要在指定对象的指定方法执行前后按指定次序执行一些操作(称之为 拦截器),比如以上示例中的一种执行次序为:BeforeAdvice -> placeOrder -> AfterAdvice。这里的一个关键问题就是如何去实现按照指定次序的链式调用?,这里先卖个关子,这个问题先放一放等下再介绍具体实现,先来看看要如何定义各种不同类型的 Advice,我们的 Advice 定义都是扩展自 AOP Alliance 定义的 MethodInterceptor 接口,Advice 部分的核心类图如下:

advice-implemented.png

其实到这里如果有了前面两篇文章(如何实现 AOP(上)如何实现 AOP(中))的基础了,实现起来就相对比较简单了,就是在方法执行之前、之后以及发生异常时调用一些特定的方法即可,AbstractAspectJAdvice 类定义了一下公共的属性和方法,核心实现源码如下:

/**
 * @author mghio
 * @since 2021-06-06
 */
public abstract class AbstractAspectJAdvice implements Advice {

  protected Method adviceMethod;
  protected AspectJExpressionPointcut pc;
  protected AopInstanceFactory adviceObjectFactory;

  public AbstractAspectJAdvice(Method adviceMethod, AspectJExpressionPointcut pc, AopInstanceFactory adviceObjectFactory) {
    this.adviceMethod = adviceMethod;
    this.pc = pc;
    this.adviceObjectFactory = adviceObjectFactory;
  }

  @Override
  public Pointcut getPointcut() {
    return pc;
  }

  protected void invokeAdviceMethod() throws Throwable {
    adviceMethod.invoke(adviceObjectFactory.getAspectInstance());
  }

  public Object getAdviceInstance() throws Exception {
    return adviceObjectFactory.getAspectInstance();
  }

  // omit getter ...

}

有了这个公共抽象父类之后其它几个 Advice 的实现就很简单了,AspectJBeforeAdvice 就是在执行拦截方法之前调用,核心源码如下:

/**
 * @author mghio
 * @since 2021-06-06
 */
public class AspectJBeforeAdvice extends AbstractAspectJAdvice {

  // omit constructor ...

  @Override
  public Object invoke(MethodInvocation mi) throws Throwable {
    this.invokeAdviceMethod();
    return mi.proceed();
  }
}

同理,AspectJAfterReturningAdvice 就是在方法正常执行结束后调用,核心源码如下:

/**
 * @author mghio
 * @since 2021-06-06
 */
public class AspectJAfterReturningAdvice extends AbstractAspectJAdvice {

  // omit constructor ...

  @Override
  public Object invoke(MethodInvocation mi) throws Throwable {
    Object result = mi.proceed();
    this.invokeAdviceMethod();
    return result;
  }
}

剩下的 AspectJAfterThrowingAdvice 想必你已经猜到了,没错,就是在方法执行过程中发生异常时调用,对应 Java 的异常机制也就是在 try{...}catch{...} 的 catch 中调用,核心源码如下:

/**
 * @author mghio
 * @since 2021-06-06
 */
public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice {

  // omit constructor ...

  @Override
  public Object invoke(MethodInvocation mi) throws Throwable {
    try {
      return mi.proceed();
    } catch (Throwable t) {
      this.invokeAdviceMethod();
      throw t;
    }
  }
}

我们支持的三种不同的 Advice 已经定义好了,接下来就是如何组装调用的问题了,同时也处理了如何去实现按照指定次序的链式调用?的问题,这里的方法调用我们也是扩展 AOP Alliance 定义的规范,即方法调用 MethodInvocation 接口。

由于这里的方法调用是基于反射完成的,将该类命名为 ReflectiveMethodInvocation,要使用反射来调用方法,很显然需要知道目标对象 targetObject、targetMethod 以及方法参数列表 arguments 等参数,当然还有我们的拦截器列表(也就是上文定义的 Advice)interceptors,因为这个是一个类似自调用的过程,为了判断是否已经执行完成所有拦截器,还需要记录当前调用拦截器的下标位置 currentInterceptorIndex,当 currentInterceptorIndex 等于 interceptors.size() - 1 时表示所有拦截器都已调用完成,再调用我们的实际方法即可。核心的类图如下:

ReflectiveMethodInvocation-implemented.png

其中类 ReflectiveMethodInvocation 的核心源码实现如下,强烈建议大家将 proceed() 方法结合上问定义的几个 Advice 类一起看:

/**
 * @author mghio
 * @since 2021-04-05
 */
public class ReflectiveMethodInvocation implements MethodInvocation {

  protected final Object targetObject;
  protected final Method targetMethod;
  protected Object[] arguments;
  protected final List<MethodInterceptor> interceptors;
  private int currentInterceptorIndex = -1;

  public ReflectiveMethodInvocation(Object targetObject, Method targetMethod,
      Object[] arguments, List<MethodInterceptor> interceptors) {
    this.targetObject = targetObject;
    this.targetMethod = targetMethod;
    this.arguments = arguments;
    this.interceptors = interceptors;
  }

  @Override
  public Object proceed() throws Throwable {
    // all interceptors have been called.
    if (this.currentInterceptorIndex == interceptors.size() - 1) {
      return invokeJoinpoint();
    }

    this.currentInterceptorIndex++;
    MethodInterceptor methodInterceptor = this.interceptors.get(this.currentInterceptorIndex);
    return methodInterceptor.invoke(this);
  }

  private Object invokeJoinpoint() throws Throwable {
    return this.targetMethod.invoke(this.targetObject, this.arguments);
  }

  // omit other method ...

}

至此,各种不同类型的 Advice 的核心实现已经介绍完毕,本来打算在这边介绍完 AOP 剩下部分的实现的,但是鉴于文章长度太长,还是放到下一次再开一篇来介绍吧。

总结

本文主要介绍了 AOP 在 XML 配置的 pointcut 解析实现、方法匹配定位以及各种不同类型的 Advice 的实现,特别是 Advice 的实现部分,建议自己动手实现一版,这样印象会更加深刻,另源码已上传至 GitHub,可自行下载参考,有任何问题请留言交流讨论。

posted @ 2021-06-06 19:46  mghio  阅读(354)  评论(0编辑  收藏  举报