资讯专栏INFORMATION COLUMN

AOP的简单实现

Andrman / 3085人阅读

摘要:要实现的功能,无非就是把两个部分串联起来切面切点只要一个类的方法中含有切点,那说明这个方法需要被代理,插入切面,所以相应的就需要产生代理类。代码实现作为准备工作,首先我们定义相应的注解类是类注解,表明这是一个切面类,包含了切面函数。

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

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

原理分析

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

切面(Aspect

切点(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 beans;

  /* bean id ==> bean aspects */
  protected Map 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 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 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了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76578.html

相关文章

  • Spring AOP就是这么简单

    摘要:是一种特殊的增强切面切面由切点和增强通知组成,它既包括了横切逻辑的定义也包括了连接点的定义。实际上,一个的实现被拆分到多个类中在中声明切面我们知道注解很方便,但是,要想使用注解的方式使用就必须要有源码因为我们要 前言 只有光头才能变强 上一篇已经讲解了Spring IOC知识点一网打尽!,这篇主要是讲解Spring的AOP模块~ 之前我已经写过一篇关于AOP的文章了,那篇把比较重要的知...

    Jacendfeng 评论0 收藏0
  • 仿照 Spring 实现简单 IOC 和 AOP - 上篇

    摘要:不过那个实现太过于简单,和,相去甚远。在接下来文章中,我也将从易到难,实现不同版本的和。切面切面包含了通知和切点,通知和切点共同定义了切面是什么,在何时,何处执行切面逻辑。 1. 背景 我在大四实习的时候开始接触 J2EE 方面的开发工作,也是在同时期接触并学习 Spring 框架,到现在也有快有两年的时间了。不过之前没有仿写过 Spring IOC 和 AOP,只是宏观上对 Spri...

    layman 评论0 收藏0
  • 猫头鹰深夜翻译:使用SpringBoot和AspectJ实现AOP

    摘要:我们会写切面来拦截对这些业务类和类的调用。切面定义何时拦截一个方法以及做什么和在一起成为切面连接点当代码开始执行,并且切点的条件满足时,通知被调用。 前言 这篇文章会帮助你使用Spring Boot Starter AOP实现AOP。我们会使用AspectJ实现四个不同的通知(advice),并且新建一个自定义的注解来追踪方法的执行时间。 你将会了解 什么是交叉分割关注点(cross...

    meislzhua 评论0 收藏0
  • 《Python有什么好学》之修饰器

    摘要:然后煎鱼加了一个后再调用函数,得到的输出结果和加修饰器的一样,换言之等效于因此,我们对于,可以理解是,它通过闭包的方式把新函数的引用赋值给了原来函数的引用。 Python有什么好学的这句话可不是反问句,而是问句哦。 主要是煎鱼觉得太多的人觉得Python的语法较为简单,写出来的代码只要符合逻辑,不需要太多的学习即可,即可从一门其他语言跳来用Python写(当然这样是好事,谁都希望入门简...

    lewinlee 评论0 收藏0
  • 仿照 Spring 实现简单 IOC 和 AOP - 下篇

    摘要:在上文中,我实现了一个很简单的和容器。比如,我们所熟悉的就是在这里将切面逻辑织入相关中的。初始化的工作算是结束了,此时处于就绪状态,等待外部程序的调用。其中动态代理只能代理实现了接口的对象,而动态代理则无此限制。 1. 背景 本文承接上文,来继续说说 IOC 和 AOP 的仿写。在上文中,我实现了一个很简单的 IOC 和 AOP 容器。上文实现的 IOC 和 AOP 功能很单一,且 I...

    AlexTuan 评论0 收藏0
  • Spring AOP 源码分析系列文章导读

    摘要:在写完容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了天时间阅读了方面的源码。从今天开始,我将对部分的源码分析系列文章进行更新。全称是,即面向切面的编程,是一种开发理念。在中,切面只是一个概念,并没有一个具体的接口或类与此对应。 1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解。在写完 Spring IOC 容器源码分析系列...

    张春雷 评论0 收藏0

发表评论

0条评论

Andrman

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<