上周自己写代码的时候发现了一件怪事,好好研究分析了一下JDK动态代理和CGLib2AopProxy一些基本原理,这篇文章写了蛮久的,特与大家分享。
先上代码,片段1是LoanRateAdjustTransaction的单元测试代码,片段2是LoanRateAdjustTransaction中的实现:
[code]
public void test1() {
// do something
LoanRateAdjustRateParam param = new LoanRateAdjustParam();
loanRateAdjustTransaction.validateParam(param);
Result result = loanRateAdjustTransaction.execute(param);
// do some asserts
}
[/code]
[code]
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
}
...
}
[/code]
问题:发现在执行到loanAcctManager.doSomething1();会NullPointerException,即loanAcctManager是null,loanAcctManager.doSomething2()时却不会报NullPOinterException。
“同一个线程,同一个bean”中两行不同代码所引用的对象(loanAcctManager)竟然一个是null(code2 line4),一个却不是null(code2 line13)!!!
难道片段1的line4和line5两行代码所使用的loanRateAdjustTransaction不是同一个对象不成?不可能吧,Spring没这么脑残的把?!下面做一个简单的试验。
实验一:code1中加上两行日志输出代码,
[code]
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
}
[/code]
运行结果日志:
[quote]INFO impl.LoanRateAdjustTransactionTest - 405739368
INFO impl.LoanRateAdjustTransactionTest - 405739368[/quote]
两行日志输出hashCode是一样的,说明是同一个对象!
那还能有什么别的原因? validateParam和execute方法存在一个细微的差别:execute方法上头有个@Transactional,而validateParam方法是final的。
难道原因前者?后者?或是两者?
实验二:在code2两个方法结束的地方加入日志代码
[code]
loanAcctManager.doSomething1();
logger.error(this.hashCode());
...
loanAcctManager.doSomething2();
logger.error(this.hashCode());
[/code]
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方法,即
[code]
// proxy.transactionManager.beginTransaction();
proxy.loanRateAdjustTransaction.execute(param);
…
// proxy.transactionManager.commitTransaction();
[/code]
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
先上代码,片段1是LoanRateAdjustTransaction的单元测试代码,片段2是LoanRateAdjustTransaction中的实现:
[code]
public void test1() {
// do something
LoanRateAdjustRateParam param = new LoanRateAdjustParam();
loanRateAdjustTransaction.validateParam(param);
Result result = loanRateAdjustTransaction.execute(param);
// do some asserts
}
[/code]
[code]
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
}
...
}
[/code]
问题:发现在执行到loanAcctManager.doSomething1();会NullPointerException,即loanAcctManager是null,loanAcctManager.doSomething2()时却不会报NullPOinterException。
“同一个线程,同一个bean”中两行不同代码所引用的对象(loanAcctManager)竟然一个是null(code2 line4),一个却不是null(code2 line13)!!!
难道片段1的line4和line5两行代码所使用的loanRateAdjustTransaction不是同一个对象不成?不可能吧,Spring没这么脑残的把?!下面做一个简单的试验。
实验一:code1中加上两行日志输出代码,
[code]
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
}
[/code]
运行结果日志:
[quote]INFO impl.LoanRateAdjustTransactionTest - 405739368
INFO impl.LoanRateAdjustTransactionTest - 405739368[/quote]
两行日志输出hashCode是一样的,说明是同一个对象!
那还能有什么别的原因? validateParam和execute方法存在一个细微的差别:execute方法上头有个@Transactional,而validateParam方法是final的。
难道原因前者?后者?或是两者?
实验二:在code2两个方法结束的地方加入日志代码
[code]
loanAcctManager.doSomething1();
logger.error(this.hashCode());
...
loanAcctManager.doSomething2();
logger.error(this.hashCode());
[/code]
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方法,即
[code]
// proxy.transactionManager.beginTransaction();
proxy.loanRateAdjustTransaction.execute(param);
…
// proxy.transactionManager.commitTransaction();
[/code]
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