摘要:要實現的功能,無非就是把兩個部分串聯起來切面切點只要一個類的方法中含有切點,那說明這個方法需要被代理,插入切面,所以相應的就需要產生代理類。代碼實現作為準備工作,首先我們定義相應的注解類是類注解,表明這是一個切面類,包含了切面函數。
之前一篇文章分析了Java AOP的核心 - 動態代理的實現,主要是基于JDK Proxy和cglib兩種不同方式。所以現在干脆把這個專題做完整,再造個簡單的輪子,給出一個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類生成代理類時就有了依據,具體來說就是在cglib的MethodInterceptor.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那樣定義一個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 Mapbeans; /* 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. */ ListpointCutMethods = 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. */ Mapaspects = 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; } }關于 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了。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76578.html
摘要:是一種特殊的增強切面切面由切點和增強通知組成,它既包括了橫切邏輯的定義也包括了連接點的定義。實際上,一個的實現被拆分到多個類中在中聲明切面我們知道注解很方便,但是,要想使用注解的方式使用就必須要有源碼因為我們要 前言 只有光頭才能變強 上一篇已經講解了Spring IOC知識點一網打盡!,這篇主要是講解Spring的AOP模塊~ 之前我已經寫過一篇關于AOP的文章了,那篇把比較重要的知...
摘要:不過那個實現太過于簡單,和,相去甚遠。在接下來文章中,我也將從易到難,實現不同版本的和。切面切面包含了通知和切點,通知和切點共同定義了切面是什么,在何時,何處執行切面邏輯。 1. 背景 我在大四實習的時候開始接觸 J2EE 方面的開發工作,也是在同時期接觸并學習 Spring 框架,到現在也有快有兩年的時間了。不過之前沒有仿寫過 Spring IOC 和 AOP,只是宏觀上對 Spri...
摘要:我們會寫切面來攔截對這些業務類和類的調用。切面定義何時攔截一個方法以及做什么和在一起成為切面連接點當代碼開始執行,并且切點的條件滿足時,通知被調用。 前言 這篇文章會幫助你使用Spring Boot Starter AOP實現AOP。我們會使用AspectJ實現四個不同的通知(advice),并且新建一個自定義的注解來追蹤方法的執行時間。 你將會了解 什么是交叉分割關注點(cross...
摘要:然后煎魚加了一個后再調用函數,得到的輸出結果和加修飾器的一樣,換言之等效于因此,我們對于,可以理解是,它通過閉包的方式把新函數的引用賦值給了原來函數的引用。 Python有什么好學的這句話可不是反問句,而是問句哦。 主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的代碼只要符合邏輯,不需要太多的學習即可,即可從一門其他語言跳來用Python寫(當然這樣是好事,誰都希望入門簡...
摘要:在上文中,我實現了一個很簡單的和容器。比如,我們所熟悉的就是在這里將切面邏輯織入相關中的。初始化的工作算是結束了,此時處于就緒狀態,等待外部程序的調用。其中動態代理只能代理實現了接口的對象,而動態代理則無此限制。 1. 背景 本文承接上文,來繼續說說 IOC 和 AOP 的仿寫。在上文中,我實現了一個很簡單的 IOC 和 AOP 容器。上文實現的 IOC 和 AOP 功能很單一,且 I...
摘要:在寫完容器源碼分析系列文章中的最后一篇后,沒敢懈怠,趁熱打鐵,花了天時間閱讀了方面的源碼。從今天開始,我將對部分的源碼分析系列文章進行更新。全稱是,即面向切面的編程,是一種開發理念。在中,切面只是一個概念,并沒有一個具體的接口或類與此對應。 1. 簡介 前一段時間,我學習了 Spring IOC 容器方面的源碼,并寫了數篇文章對此進行講解。在寫完 Spring IOC 容器源碼分析系列...
閱讀 2521·2023-04-26 02:57
閱讀 1403·2023-04-25 21:40
閱讀 2155·2021-11-24 09:39
閱讀 3557·2021-08-30 09:49
閱讀 760·2019-08-30 15:54
閱讀 1166·2019-08-30 15:52
閱讀 2069·2019-08-30 15:44
閱讀 1274·2019-08-28 18:27