本文将简单介绍如何用Kotlin实现一个AOP框架(在此之前本人先行编写了IOC框架,IOC实现为采用xml配置的方式,比较简单,可直接在GitHub参阅源码,这里就不赘述了)。
基本注意事项
Spring AOP构建在动态代理基础之上,因此Spring对AOP的支持局限于方法的拦截;
目标对象实现了接口,Spring默认采用JDK动态代理实现AOP;
条件满足时,CGLIB可以强行取代JDK动态代理来实现AOP;
如果目标对象没有实现接口,则采用CGLIB实现AOP;
框架类/接口介绍
Spring AOP的核心是代理模式,而代理模式有多种,包括JDK动态代理以及CGLIB,这里首先采用JDK动态代理来实现。
和Spring一样,本框架倚赖AspectJ框架,以期快速完成框架的编写。
下面介绍一些主要的类/接口。
TargetSource
被代理的目标对象包,之所以称之为包是因为它除了包含目标对象,还包含目标对象的元数据。
Advisor
用于获得Advice,Advice是AspectJ框架提供的接口。
Pointcut
切点接口,表示在哪里切入,它包括两个方法,分别用来获取ClassFilter和MethodMatcher,一个是类过滤器,一个是方法匹配器。
ClassFilter & MethodMatcher
类过滤器接口和方法匹配器接口,分别包含匹配方法判断是否满足匹配条件。
PointcutAdvisor
切面接口,提供切点和advice的获取方法。
AdvisedSupport
一个支持类,它包含目标对象包引用、拦截器对象引用以及判断方法匹配接口的引用。MethodInterceptor是拦截器接口,本质上仍然是一个Advice,MethodMatcher代表切点,所以AdvisedSupport提供了“在什么地方对什么样的目标对象进行什么样的拦截”的信息。
AopProxy
代理类的接口,不同模式的代理都实现该接口。
ReflectiveMethodInvocation
该类的作用主要是通过反射调用目标对象的方法。ReflectiveMethodInvocation是MethodInvocation的具体实现,MethodInvocation是AspectJ框架提供的接口,该接口还继承了AspectJ提供的另一接口Invocation。
AspectJExpressionPointcut
切点的具体实现类。AspectJ框架提供了AspectJExpressionPointcut类,本人在此基础上修改了一些内容,去掉了暂时不需要的部分。
AspectJExpressionPointcutAdvisor
切面的实现类。
BeanFactoryAware
设置工厂类的接口。
JdkDynamicAopProxy
JDK动态代理类,实现AOP的核心。
AspectJAwareAdvisorAutoProxyCreator
它是一个BeanPostProcessor的实现类,用于给拦截到的Bean对象创建代理实例。
JDK动态代理
首先贴出代码:
|
|
和通常JDK动态代理的实现最大区别是,增加了MethodInterceptor和ReflectiveMethodInvocation对象的参与,比较if…else的两个返回部分就可以知道一切都是为了将逻辑更进一层的封装,这样通过实现MethodInterceptor就可以完成前置Advice、后置Advice等等切面工作。
比如在后文还将运用于项目代码的SimpleInterceptor:
|
|
这就是一个简单的环绕Advice拦截器。
测试JDK动态代理
写一个测试来检测目前写的代码。
首先写一个环切Advice:
|
|
然后是目标被代理类:
|
|
测试:
|
|
输出为:
|
|
成功完成动态代理的工作。
Kotlin编写问题 & 解决
暂时停下框架的编写,讨论在之前使用Kotlin语言的过程中积累的几个问题。
问题一
这是编写ReflectiveMethodInvocation的方法proceed()时产生的问题。如果是代理没有显示参数的方法,原本的代码在IDE下会报IllegalArgumentException异常。内部调用的是public Object invoke(Object obj, Object… args),由于可变参数需要星号投影,且如果args是null需要生成零长null型数组,所以解决代码如下:
|
|
问题二
编写TargetSource类时传入的可变参数(vararg)赋值给数组出现Type mismatch问题。本人解决的方法:
|
|
问题三
在编写initializeBean方法时,出现参数Val cannot be reassigned的问题。用代码描述就是:
|
|
在本文最后的参考链接Function parameters are “val” not “var”?下的回答中,语言设计的作者给出了问题产生原因。在这里,本人用返回值来解决,结合
|
|
问题四
继承父类get、set方法和Kotlin的属性机制冲突。这个问题可以通过@JvmField解决:
|
|
CGLIB动态代理
继续代码的编写,创造一个类CglibAopProxy,整个代码结构和JdkDynamicAopProxy是很相似的,直接贴出:
|
|
intercept方法中的逻辑和JDK动态代理的一样,AspectJ框架能将引用部分的代码和被代理的方法解耦合。
测试CglibAopProxy
|
|
测试逻辑基本上和JdkDynamicAopProxyTest一摸一样,只有两点不同:
- HelloJDKProxy要声明为open;
- 生成的代理对象转换为被代理类而非公共接口;
这也说明了JDK动态代理和CGLIB动态代理的区别:前者被代理的类需要实现接口,而后者被代理的类不需要实现接口,但是该类不能是final的,是需要open的。
完成框架主逻辑
这一步即完善程序,在和之前测试条件相同的情况下,运行Main方法达到环切的效果。
整个程序的执行流程在于方法refresh:
|
|
loadBeanDefinitions方法用来将对xml解析的(name, BeanDef)键值对装入工厂的容器中;
registerBeanPostProcessors方法用来注册BeanPostProcessor,BeanPostProcessor对象可以在initializeBean方法中拦截bean,并将动态代理作用在他们身上;
onRefresh方法通过beanId获得创建的bean对象,其中最重要的是getBean方法;
getBean()代码如下:
|
|
如果只是创建对象,那么应该使用doCreateBean()而不是getBean():
|
|
在代码中为避免创建多个对象产生覆盖问题,可以使用if语句在创建前先进行判断bean是否已然存在。
最后要说明的是,动态代理产生的代理对象需要在beandefMap中取代原本的bean,不然在最终调用时获得的仍然是没有被代理的对象,例如:
|
|
小结
通过编写本项目了解AOP的实现原理,对用Kotlin编程中遇到的问题进行了分析和解决。
参考
Spring AOP对AspectJ的支持:介绍了几种原语;
Why is Kotlin throw IllegalArgumentException when using Proxy
Kotlin function parameter: Val cannot be reassigned
Function parameters are “val” not “var”?
What’s the difference between Foo::class.java and Foo::javaClass?
Spring Aop原理之Advisor过滤:内有matches方法的详解