AOP的簡單實現

以前一篇文章分析了Java AOP的核心 - 動態代理的實現,主要是基於JDK Proxycglib兩種不一樣方式。因此如今乾脆把這個專題作完整,再造個簡單的輪子,給出一個AOP的簡單實現。這裏直接使用到了cglib,這也是Spring所使用的方式。java

這裏是完整代碼,實現總的來講比較簡單,無非就是各類反射,以及cglib代理。須要說明的是這只是我我的的實現方式,功能也極其有限。我並無看過Spring的源碼,也不知道它的AOP實現方式具體是什麼樣的,但原理應該是相似的。git

原理分析

若是你熟悉了動態代理,應該不難構思出一個AOP的方案。要實現AOP的功能,無非就是把兩個部分串聯起來:github

  1. 切面(Aspect
  2. 切點(PointCut

只要一個類的方法中含有切點PointCut,那說明這個方法須要被代理,插入切面Aspect,因此相應的Bean就須要產生代理類。咱們只需找到全部的PointCut,以及它們對應的Aspect,整理出一張表,就能產生出代理類,而且能知道對應的每一個方法,是否有Aspect,以及如何調用Aspect函數。express

這裏關鍵就是把這張PointCut和Aspect的對應表創建起來。由於在代理方法時,關注點首先是基於PointCut,因此這張表也是由PointCut到Aspect的映射:編程

PointCut Class A

    PointCutMethod 1
        Aspect Class / Method
        Aspect Class / Method

    PointCutMethod 2
        Aspect Class / Method

    PointCutMethod 3
        Aspect Class / Method
        Aspect Class / Method
   ...

PointCut Class B

    PointCutMethod 1
        Aspect Class / Method

    PointCutMethod 2
        Aspect Class / Method
   ...

例如定義一個切面類和方法:segmentfault

@Aspect
public class LoggingAspect {
  @PointCut(type=PointCutType.BEFORE,
            cut="public void Greeter.sayHello(java.lang.String)")
  public static void logBefore() {
    System.out.println("=== Before ===");
  }
}

這裏的註解語法都是我本身定義的,和Spring不太同樣,不過意思應該很明瞭。這是一個前置通知,打印一行文字,切點是Greeter這個類的sayHello方法:app

public class Greeter {
  public void sayHello(String name) {
    System.out.println("Hello, " + name);
  }
}

因此咱們最後生成的AOP關係表就是這樣:框架

Greeter
    sayHello
        LoggingAspect.logBefore

這樣咱們在爲Greeter類生成代理類時就有了依據,具體來講就是在cglibMethodInterceptor.intercept()方法中,就能夠肯定須要在哪些方法,哪些位置,調用哪些Aspect函數。ide

代碼實現

做爲準備工做,首先咱們定義相應的註解類:函數式編程

Aspect是類註解,代表這是一個切面類,包含了切面函數。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {}

而後是切點PointCut,這是方法註解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PointCut {
  // PointCut Type, BEFORE or AFTER。
  PointCutType type();
  
  // PointCut expression.
  String cut();
}

不要和Spring的混起來了,我這裏簡單化了,直接用一個叫PointCut的註解,定義了兩個field,一個是切點類型type,這裏只有前置通知BEFORE和後置通知AFTER兩種,固然你也能夠添加更多。一個是切點表達式cut,語法上相似於Spring,但也簡單化了,去掉了execution語法,直接寫函數表達式,用分號;隔開多個函數,也沒有什麼複雜的通配符匹配。

Bean 和 BeanFactory

因爲要產生各類類的實例,咱們不妨也像Spring那樣定義一個BeanBeanFactory的概念,但功能很是簡單,只是用來管理全部的類而已。

Bean:

public class Bean {
  /* bean id */
  private String id;
  /* bean class */
  private Class<?> clazz;
  /* instance, singleton */
  private Object instance;
}

DefaultBeanFactory

public class DefaultBeanFactory {
  /* beanid ==> Bean */
  private Map<String, Bean> beans;

  /* bean id ==> bean aspects */
  protected Map<String, BeanAspects> aops;
  
  /* get bean */
  public Object getBean(String beanId) {
    // ...
  }
}

這裏的beans是管理全部Bean的一個簡單Map,key是bean id;而aops就是以前說到的維護PointCut和Aspect映射關係的表,key是PointCut類的bean id,而value是我定義的另外一個類BeanAspects,具體代碼就不貼了,這實際上又是一層嵌套的表,是一個PointCut類中各個PointCut方法,到對應的切面Aspect方法集的映射。這裏實際上有幾層表的嵌套,不過結構是很清楚的,就是從PointCut到Aspect的映射,能夠參照我上面的圖:

PointCut Class A

    PointCut Method 1
        Aspect Class / Method

    PointCut Method 2
        Aspect Class / Method

創建 PointCut 和 Aspect 關係表

如今的關鍵問題就是要創建這張關係表,實現起來並不難,就是利用反射而已。像Spring那樣,咱們須要掃描給定的package中的全部類,找出註解Aspect修飾的切面類,找到它所包含的PointCut修飾的切面方法,分析它們對應的切入點PointCut,把這張表創建起來就能夠了。

第一個問題是如何掃描java package,我用了guava中的ClassPath類:

ClassPath cp = ClassPath.from(getClass().getClassLoader());

// Scan all classes under a package.
for (ClassPath.ClassInfo ci : cp.getTopLevelClasses(pkg)) {
  Class<?> clazz = ci.load();
  // ...
}

而後用註解Aspect判斷一個類是不是切面類,若是是就用PointCut註解找出切面方法:

if (clazz.getAnnotation(Aspect.class) != null) {
  for (Method m : clazz.getMethods()) {
    PointCut pointCut = (PointCut)(m.getAnnotation(PointCut.class));
    if (pointCut != null) {
      /* Parse point cut expression. */
      List<Method> pointCutMethods = parsePointCutExpr(pointCut.cut());
      for (Method pointCutMethod : pointCutMethods) {
        /* Add mapping to aops table: mapping from poitcut to aspect. */
        /* ... */
      }
    }
  }
}

至於parsePointCutExpr方法如何實現,解析切點表達式,無非就是一堆正則匹配和反射,簡單粗暴,代碼比較冗長,這裏就不貼了,感興趣的童鞋能夠直接去看這裏的連接

代理類的生成

代理類什麼時候生成?應該是在調用getBean時,若是這個Bean類被切面介入了,就須要用cglib爲它生成代理類。我把這部分邏輯放在了Bean.java中:

if (!beanFactory.aops.containsKey(id)) {
   this.instance = (Object)clazz.newInstance();
} else {
   BeanAspects beanAspects = beanFactory.aops.get(id);
   // Create proxy class instance.
   Enhancer eh = new Enhancer();
   eh.setSuperclass(clazz);
   eh.setCallback(new BeanProxyInterceptor(beanFactory, beanAspects));
   this.instance = eh.create();
}

這裏先檢查這個bean是否須要AOP代理,若是不須要直接調構造函數生成 instance 就能夠;若是須要代理,則使用BeanProxyInterceptor生成代理類,它的intercept方法包含了方法代理的所有邏輯:

@Override
class BeanProxyInterceptor implements MethodInterceptor {
  public Object intercept(Object obj, Method method, Object[] args,
                          MethodProxy proxy) throws Throwable {
    /* Find aspects for this method. */
    Map<String, BeanAspects.AspectMethods> aspects = 
        beanAspects.pointCutAspects.get(method);
    if (aspects == null) {
      // No aspect for this method.
      return proxy.invokeSuper(obj, args);
    }
    
    // TODO: Invoke before advices.

    // Invoke the original method.
    Object re = proxy.invokeSuper(obj, args);
    
    // TODO: Invoke after advices.

    return re;
  }

咱們這裏只實現前置和後置通知,因此TODO部分實現出來就能夠了。由於咱們前面已經從PointCut和Aspect的關係表aops和子表BeanAspects裏拿到了這個PointCut類、這個PointCut方法對應的全部Aspect切面方法,存儲在aspects裏,因此咱們只需遍歷aspects並依次調用全部方法就能夠了。爲了簡明,下面是僞代碼邏輯:

for method in aspects.beforeAdvices:
  invokeAspectMethod(aspectBeanId, method)

// invoke original method
// ...

for method in aspects.afterAdvices:
  invokeAspectMethod(aspectBeanId, method)

invokeAspectMethod須要作一個簡單的static判斷,對於非static的切面方法,須要拿到切面類Bean的實例 instance。

void invokeAspectMethod(String aspectBeanId, Method method) {
  if (Modifier.isStatic(method.getModifiers())) {
    method.invoke(null);
  } else {
    method.invoke(beanFactory.getBean(aspectBeanId));
  }
}

測試

切面類,定義了三個切面方法,一個前置打印,一個後置打印,還有一個自增計數器,前兩個是static方法:

@Aspect
public class MyAspect {
  private AtomicInteger count = new AtomicInteger();

  // Log before.
  @PointCut(type=PointCutType.BEFORE,
            cut="public int aop.example.Calculator.add(int, int);" +
                "public void aop.example.Greeter.sayHello(java.lang.String);")
  public static void logBefore() {
    System.out.println("=== Before ===");
  }

  // Log after.
  @PointCut(type=PointCutType.AFTER,
            cut="public long aop.example.Calculator.sub(long, long);" +
                "public void aop.example.Greeter.sayHello(java.lang.String)")
  public static void logAfter() {
    System.out.println("=== After ===");
  }

  // Increment counter.
  @PointCut(type=PointCutType.AFTER,
            cut="public int aop.example.Calculator.add(int, int);" +
                "public long aop.example.Calculator.sub(long, long);" +
                "public void aop.example.Greeter.sayHello(java.lang.String);")
  public void incCount() {
    System.out.println("count: " + count.incrementAndGet());
  }
}

被切入的切點類是GreeterCalculator,比較簡單,裏面的方法簽名都是符合上面MyAspect類中的切點表達式的:

public class Greeter {
  public void sayHello(String name) {
    System.out.println("Hello, " + name);
  }
}
public class Calculator {
  public int add(int x, int y) {
    return x + y;
  }
  public long sub(long x, long y) {
    return x - y;
  }
}

關於 Aspect 和 PointCut 主次關係的一點思考

不難發現,從代理實現的角度來講,那張AOP關係表應該是基於切點PointCut的,以此爲主索引,從PointCut到Aspect,這也彷佛更符合咱們的常規思惟。然而像Spring這樣的框架,包括我上面給出的仿照Spring的例子,在定義AOP時,不管是基於XML仍是註解,寫法上都是以切面Aspect爲主的,由具體Aspect經過切點表達式來定義要切入哪些PointCut,這可能也是Aspect Oriented Programming的本意。因此上面的關係表的創建過程實際上是在反轉這種主次關係,把PointCut做爲主。

不過這彷佛有點麻煩,就我我的而言我仍是更傾向於在語法層面就直接使用前者,即基於PointCut。若是以Aspect爲主,對代碼的可維護性是一個挑戰,由於你在定義Aspect時,就須要用相應的表達式來定義PointCut,而隨着實際需求變化,例如PointCut函數的增長或減小,這個表達式每每須要改變,這樣的耦合性每每會給代碼維護帶來麻煩;而反過來若是隻簡單定義Aspect,而由具體的PointCut本身決定須要調用哪些切面,雖然註解量會略微增長,可是更容易管理。固然若是用XML配置可能會比較頭痛。

其實Python就是這樣作的,Python的函數註解就是自然的,基於PointCut的的AOP。Python註解其實是一個函數的wrapper,包裹了原函數,返回給你一個新的函數,但在語法層面上是透明的,在wrapper裏就能夠定義切面的行爲。這樣的AOP彷佛更符合人的直觀感覺,固然這也源於Python自己對函數式編程的良好支持,而Java因爲其對OOP的蜜汁堅持,目前來說確定是不會這樣作的,因此只能經過代理這樣」醜陋「的方式實現AOP了。

相關文章
相關標籤/搜索