之前一篇文章分析了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本身对函数式编程的良好支持。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。