以前一篇文章分析了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了。