之前一篇文章分析了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 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());
}
}
被切入的切點類是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了。