如何實現一個簡易版的 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,可自行下載參考,有任何問題請留言交流討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM