以前一篇文章分析了Java AOP的核心 - 动态代理的实现,主要是基于JDK Proxy
和cglib
两种不一样方式。因此如今干脆把这个专题作完整,再造个简单的轮子,给出一个AOP的简单实现。这里直接使用到了cglib
,这也是Spring所使用的方式。java
这里是完整代码,实现总的来讲比较简单,无非就是各类反射,以及cglib
代理。须要说明的是这只是我我的的实现方式,功能也极其有限。我并无看过Spring的源码,也不知道它的AOP实现方式具体是什么样的,但原理应该是相似的。git
若是你熟悉了动态代理,应该不难构思出一个AOP的方案。要实现AOP的功能,无非就是把两个部分串联起来:github
Aspect
)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
类生成代理类时就有了依据,具体来讲就是在cglib
的MethodInterceptor.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语法,直接写函数表达式,用分号;
隔开多个函数,也没有什么复杂的通配符匹配。
因为要产生各类类的实例,咱们不妨也像Spring那样定义一个Bean
和BeanFactory
的概念,但功能很是简单,只是用来管理全部的类而已。
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
如今的关键问题就是要创建这张关系表,实现起来并不难,就是利用反射而已。像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()); } }
被切入的切点类是Greeter
和Calculator
,比较简单,里面的方法签名都是符合上面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; } }
不难发现,从代理实现的角度来讲,那张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了。