Spring AOP和CGLib动态代理的原理

上周自己写代码的时候发现了一件怪事,好好研究分析了一下JDK动态代理和CGLib2AopProxy一些基本原理,这篇文章写了蛮久的,特与大家分享。

先上代码,片段1是LoanRateAdjustTransaction的单元测试代码,片段2是LoanRateAdjustTransaction中的实现:

 

public void test1() {
// do something
LoanRateAdjustRateParam param = new LoanRateAdjustParam();
loanRateAdjustTransaction.validateParam(param);
Result result = loanRateAdjustTransaction.execute(param);
// do some asserts
}

 

public final void validateParam(LoanRateAdjustRateParam param) {
//do something
try{
loanAcctManager.doSomething1();
} catch(Exception e) {
// do nothing
}
}
@Transactional
public void execute(LoanRateAdjustRateParam param){
// do something
try{
loanAcctManager.doSomething2();
} catch(Exception e) {
// do nothing
}
...
}

问题:发现在执行到loanAcctManager.doSomething1();会NullPointerException,即loanAcctManager是null,loanAcctManager.doSomething2()时却不会报NullPOinterException。

“同一个线程,同一个bean”中两行不同代码所引用的对象(loanAcctManager)竟然一个是null(code2 line4),一个却不是null(code2 line13)!!!

难道片段1的line4和line5两行代码所使用的loanRateAdjustTransaction不是同一个对象不成?不可能吧,Spring没这么脑残的把?!下面做一个简单的试验。

实验一:code1中加上两行日志输出代码,

 

public void test1() {
// do something
LoanRateAdjustRateParam param = new LoanRateAdjustParam();
logger.info(loanRateAdjustTransaction.hashCode());
loanRateAdjustTransaction.validateParam(param);
logger.info(loanRateAdjustTransaction.hashCode());
Result result = loanRateAdjustTransaction.execute(param);
// do some asserts
}

运行结果日志:

[quote]INFO impl.LoanRateAdjustTransactionTest – 405739368

INFO impl.LoanRateAdjustTransactionTest – 405739368[/quote]

两行日志输出hashCode是一样的,说明是同一个对象!

那还能有什么别的原因? validateParam和execute方法存在一个细微的差别:execute方法上头有个@Transactional,而validateParam方法是final的。

难道原因前者?后者?或是两者?

实验二:在code2两个方法结束的地方加入日志代码

 

loanAcctManager.doSomething1();
logger.error(this.hashCode());
...
loanAcctManager.doSomething2();
logger.error(this.hashCode());

logger.error(this.hashCode());

运行日志:

[quote]INFO impl.LoanRateAdjustTransactionTest – 405739541

ERROR impl.LoanRateAdjustTransaction – 405739541

INFO impl.LoanRateAdjustTransactionTest – 405739541

ERROR impl.LoanRateAdjustTransaction – 33408816[/quote]

实验三:将execute方法上的@Transactional标签去掉,保留validateParam方法的final约束

单元测试不再抛NullPointerException,测试代码运行正常,问题不再出现!运行日志

[quote]INFO impl.LoanRateAdjustTransactionTest – 12563781

ERROR impl.LoanRateAdjustTransaction – 12563781

INFO impl.LoanRateAdjustTransactionTest – 12563781

ERROR impl.LoanRateAdjustTransaction – 12563781[/quote]

实验四:将validateParam方法的final约束去掉,保留execute方法上的@Transactional标签

单元测试不再抛NullPointerException,测试代码运行正常,问题不再出现!运行日志同实验三

看来,原因是@Transactional和final联合发生的作用,那具体原因是什么呢?下面需要对Spring AOP机制进行一些解释:

Spring AOP是基于CGLib来对Spring容器中bean的动态行为进行封装,在加载class文件时通过动态代理的方式修改、丰富原方法的行为,比如标签式事务(@Transactional)就是在方法调用前以AOP的方式启动了事务。

一. 静态地看(内存结构和类图),loanRateAdjustTransaction在Spring容器中的实际上是以类似下图的方式存在的:

[img]http://dl.iteye.com/upload/attachment/608241/4c9e770a-4dde-331b-944a-526bccb9494b.png[/img]

[img]http://dl.iteye.com/upload/attachment/608236/ca1ccfbf-3c7a-3685-8d56-325a2a10b18d.png[/img]

图一说明: loanRateAdjustTransaction已经被封装成两外一个对象了,code1的测试代码所使用的loanRateAdjustTransaction其实是代理类$ProxyN的对象,该对象是会持有loanOpenTransaction中的所有方法,并以Method xxx保存(有兴趣的同学可以再研究一下JDK动态代理的原理)。

图二说明: Spring通过CGLib2AopProxy,对定义的loanRateAdjustTransaction(object)进行代理,通过创建一个继承实现类的子类(JDK Proxy采取的方式是复用而非继承),在运行时动态修改子类的代码来实现的。CGLib2AopProxy在实例化后,其实是会根据一定的规则(如*)将从LoanRateAdjustTransaction(class)继承下来的Method作为自身的成员进行保存,并在这些Method被调用时进行拦截,而被final字段标示的Method是一个例外,因为final方法是不能被继承的,即:被final字段标识过的方法不会被代理! 如果定义一个class是final,而又有方法使用了类似于@Transactional的Spring标签时,那么如果在Spring的bean配置文件中加入该类型的bean实例,Spring容器初始化时是会报错的,因为CGlibAopProxy尝试继承这个class。

二, 动态地看(调用过程)

1. 所有被代理过的Method在被外部方法调用时,调用请求都首先会被代理CGlib2AopProxy拦截并执行框架所需的动作、添加其行为,

如loanRateAdjustTransaction.execute(param)之前会先通过Spring的TransactionManager启动事务,然后再调用CGlib2AopProxy实例中的loanRateAdjustTransaction引用的execute方法,即

 

// proxy.transactionManager.beginTransaction();
proxy.loanRateAdjustTransaction.execute(param);

// proxy.transactionManager.commitTransaction();

2. 而调用loanRateAdjustTransaction.validateParam(param)时,实质是proxy.super.validateParam(param),与proxy.loanRateAdjustTransaction.validateParam(param)有很大的差别,

差别在于proxy.loanRateAdjustTransaction是Spring容器中的对象,在容器初始化时经过了bean之间ref的适配,而proxy则没有,因此首先会看到实验二中的日志,testCase中的LoanRateAdjustTransaction实例和validateParam中的this都是proxy,而execute方法中的this是proxy. loanRateAdjustTransaction。

实验五:我们单独将validateParam(param)方法的final约束去掉

[quote]INFO impl.LoanRateAdjustTransactionTest – 28171907

ERROR impl.LoanRateAdjustTransaction – 27713748

INFO impl.LoanRateAdjustTransactionTest – 28171907

ERROR impl.LoanRateAdjustTransaction – 27713748[/quote]

日志表明,LoanRateAdjustTransactionTest中的loanRateAdjustTransaction相同(proxy),但不同于实际运行时和execute、validateParam方法中的this(spring容器中的loanRateAdjustTransaction)

如果大家自己理解了上面的五个实验,相信大家应该已经彻底理解了CGLib代理是如何一个情况了。

其他相关文章推荐:

http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/

http://www.ibm.com/developerworks/cn/java/j-lo-proxy2/?ca=drs-cn-0127

http://www.ibm.com/developerworks/cn/java/j-jtp08305.html

    原文作者:AOP
    原文地址: https://blog.csdn.net/iteye_19242/article/details/82236017
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞