Spring AOP跟CGLib动态代理的原理

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
}

运行结果日志:
引用
INFO impl.LoanRateAdjustTransactionTest - 405739368
INFO impl.LoanRateAdjustTransactionTest - 405739368

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

那还能有什么别的原因? validateParam和execute方法存在一个细微的差别:execute方法上头有个@Transactional,而validateParam方法是final的。
难道原因前者?后者?或是两者?
实验二:在code2两个方法结束的地方加入日志代码
    loanAcctManager.doSomething1();
    logger.error(this.hashCode());
    ...
    loanAcctManager.doSomething2();
    logger.error(this.hashCode());

logger.error(this.hashCode());
运行日志:
引用
INFO impl.LoanRateAdjustTransactionTest - 405739541
ERROR impl.LoanRateAdjustTransaction - 405739541
INFO impl.LoanRateAdjustTransactionTest - 405739541
ERROR impl.LoanRateAdjustTransaction - 33408816


实验三:将execute方法上的@Transactional标签去掉,保留validateParam方法的final约束
单元测试不再抛NullPointerException,测试代码运行正常,问题不再出现!运行日志
引用
INFO impl.LoanRateAdjustTransactionTest - 12563781
ERROR impl.LoanRateAdjustTransaction - 12563781
INFO impl.LoanRateAdjustTransactionTest - 12563781
ERROR impl.LoanRateAdjustTransaction - 12563781

实验四:将validateParam方法的final约束去掉,保留execute方法上的@Transactional标签
单元测试不再抛NullPointerException,测试代码运行正常,问题不再出现!运行日志同实验三

看来,原因是@Transactional和final联合发生的作用,那具体原因是什么呢?下面需要对Spring AOP机制进行一些解释:
Spring AOP是基于CGLib来对Spring容器中bean的动态行为进行封装,在加载class文件时通过动态代理的方式修改、丰富原方法的行为,比如标签式事务(@Transactional)就是在方法调用前以AOP的方式启动了事务。
一. 静态地看(内存结构和类图),loanRateAdjustTransaction在Spring容器中的实际上是以类似下图的方式存在的:
Spring AOP跟CGLib动态代理的原理
Spring AOP跟CGLib动态代理的原理

图一说明: 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约束去掉
引用
INFO impl.LoanRateAdjustTransactionTest - 28171907
ERROR impl.LoanRateAdjustTransaction - 27713748
INFO impl.LoanRateAdjustTransactionTest - 28171907
ERROR impl.LoanRateAdjustTransaction – 27713748

日志表明,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