AOP的简单实现

hyuan

之前一篇文章分析了Java AOP的核心 - 动态代理的实现,主要是基于JDK Proxycglib两种不同方式。所以现在干脆把这个专题做完整,再造个简单的轮子,给出一个AOP的简单实现。这里直接使用到了cglib,这也是Spring所使用的方式。

这里是完整代码,实现总的来说比较简单,无非就是各种反射,以及cglib代理。需要说明的是这只是我个人的实现方式,功能也极其有限。我并没有看过Spring的源码,也不知道它的AOP实现方式具体是什么样的,但原理应该是类似的。

原理分析

如果你熟悉了动态代理,应该不难构思出一个AOP的方案。要实现AOP的功能,无非就是把两个部分串联起来:

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

只要一个类的方法中含有切点PointCut,那说明这个方法需要被代理,插入切面Aspect,所以相应的Bean就需要产生代理类。我们只需找到所有的PointCut,以及它们对应的Aspect,整理出一张表,就能产生出代理类,并且能知道对应的每个方法,是否有Aspect,以及如何调用Aspect函数。

这里关键就是把这张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
   ...

例如定义一个切面类和方法:

@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方法:

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

所以我们最后生成的AOP关系表就是这样:

Greeter
    sayHello
        LoggingAspect.logBefore

这样我们在为Greeter类生成代理类时就有了依据,具体来说就是在cglibMethodInterceptor.intercept()方法中,就可以确定需要在哪些方法,哪些位置,调用哪些Aspect函数。

代码实现

作为准备工作,首先我们定义相应的注解类:

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本身对函数式编程的良好支持。

阅读 1.9k

naive programmer

406 声望
73 粉丝
0 条评论
你知道吗?

naive programmer

406 声望
73 粉丝
宣传栏