一、起因
最近在做一个Spring 项目的时候,遇到了一个百思不得其解的诡异问题,请看下面的案例:
代码1 :
@Repository ( "personDAO" )
@Transactional (rollbackFor = Exception. class )
public class PersonDAOImpl implements PersonDAO{
// 模型处理方法(略)
//
}
代码 2 :
@RunWith (SpringJUnit4ClassRunner. class )
@ContextConfiguration ( "/applicationContext.xml" )
public class PersonDAOImplTest {
// 模型处理测试方法(略)
// 将 personDAO 注入进测试类
@Resource (name = "personDAO" )
public void setPersonDAO(PersonDAOImpl personDAO) {
this . personDAO = personDAO;
}
private PersonDAOImpl personDAO ;
}
运行结果:
Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException : Bean named 'personDAO' must be of type [cn.chris.s2sh.dao.impl.hibernate.PersonDAOImpl], but was actually of type [$Proxy17]
恩!报错!非常诡异!
首先我的 applicationContext.xml 里面没有任何定义 personDAO 的 bean 元素,我利用的是 annotation 的方式进行注入。
所以我想当然的认为这样注入应该是没什么问题的!( PS :这种写法我是无意当中写出来,以前做项目都是固定注入接口类型 - - )。
二、追查线索:
第一条线索:
好了,说下这个问题为什么这么诡异。因为我在代码 1 里面明确指明 PersonDAOImpl 是 @Repository 组件,也就是说这个类是需要被注入进去的。
在测试运行的时候,就出现了上面的那个异常(我翻译为“不是Bean 所依赖的类型之异常”)。
我以为是我哪块代码写错了,找了半天也没有发现出任何问题;这样我就将注解方式换成XML 配置文件方式,运行之后还是报同样的异常。
第二条线索:
虽然第一条线索被中断,但是我还有第二条线索!
我规规矩矩的将注入属性换成接口类型。
@RunWith (SpringJUnit4ClassRunner. class )
@ContextConfiguration ( "/applicationContext.xml" )
public class PersonDAOImplTest {
// 模型处理测试方法(略)
// 将 personDAO 注入进测试类
@Resource (name = "personDAO" )
public void setPersonDAO(PersonDAO personDAO) {
this . personDAO = personDAO;
}
private PersonDAO personDAO ;
}
好,测试通过!
然后我又突发奇想,去掉代码1 里面的实现接口代码:
@Repository ( "personDAO" )
@Transactional (rollbackFor = Exception. class )
public class PersonDAOImpl {
// 模型处理方法(略)
//
}
代码2 恢复原状:
@RunWith (SpringJUnit4ClassRunner. class )
@ContextConfiguration ( "/applicationContext.xml" )
public class PersonDAOImplTest {
// 模型处理测试方法(略)
// 将 personDAO 注入进测试类
@Resource (name = "personDAO" )
public void setPersonDAO(PersonDAOImpl personDAO) {
this . personDAO = personDAO;
}
private PersonDAOImpl personDAO ;
}
很好,继续测试通过!
这个问题勾起了我神圣的好奇心~~~ 我一定要搞清楚这是一种什么现象!
三、 小荷初露尖尖角
$Proxy17 的本来面目:这个标示符我以前在学Java 动态 代理 的时候见到过,它代表着一个代理对象的引用。
一开始报出的那个异常里面的意思就将是$Proxy17 注入到代码2 里面的那个setter ;从异常信息可以看出$Proxy17 和cn.chris.s2sh.dao.impl.hibernate.PersonDAOImpl 不匹配,也就是说被注入的是另外一个类!也就是说它代表着一个接口的代理对象(Java 动态 代理 只针对有接口的类),那个接口就是PersonDAO !接口的代理对象和personDAOImpl 是平级的层次关系,谁也不是谁的父类和子类。我怀疑这是Spring 框架底层做的手脚。
好了,离真相又进了一步。
四、 真相只有一个
开始读Spring 源代码!
各种追溯,漫长的追溯~~~ 追溯的过程很枯燥,也很痛苦!!!
我追到了 org.springframework.aop.framework.DefaultAopProxyFactory 这个类,其中有个方法:
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class targetClass = config.getTargetClass();
if (targetClass == null ) {
throw new AopConfigException( "TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation." );
}
if (targetClass.isInterface()) {
return new JdkDynamicAopProxy(config);
}
if (! cglibAvailable ) {
throw new AopConfigException(
"Cannot proxy target class because CGLIB2 is not available. " +
"Add CGLIB to the class path or specify proxy interfaces." );
}
return CglibProxyFactory.createCglibProxy (config);
}
else {
return new JdkDynamicAopProxy (config);
}
}
其中我看到有个条件判断:
if (targetClass.isInterface()) {
return new JdkDynamicAopProxy(config);
}
这个代码的意思就是说目标类(代码 2 里面的 setter 方法的参数类型)是否实现了一个接口,如果有就返回一个 Java 动态反射代理对象;如果没有就返回 CglibProxyFactory 对象。
好了,真相终于浮出水面。
原来是 Spring 底层利用 AOP 技术注入属性,并且判断被注入属性是否有接口类型,如果有就利用动态反射机制创建代理对象并包装原始对象注入到 setter ,没有则利用 CGLIB 类库创建代理对象。
另外在官方文档中也有这一说明,后悔没有先去看文档手册 (3.0 的官方文档 ) 。
Spring AOP uses either JDK dynamic proxies or CGLIB to create the proxy for a given target object. (JDK dynamic proxies are preferred whenever you have a choice).
If the target object to be proxied implements at least one interface then a JDK dynamic proxy will be used. All of the interfaces implemented by the target type will be proxied. If the target object does not implement any interfaces then a CGLIB proxy will be created.
大概的意思是这样的:
SpringAOP 使用了 JDK 动态代理机制或者 CGLIB 类库的其中的一个来为指定的目标对象创建代理。( JDK 动态代理在任何时候都是都合适的选择, Spring 暗示着开发者要针对接口编程,我个人理解)。
如果目标对象实现了至少一个接口, JDK 动态代理机制会为这个对象提供代理。这个代理对象实现了目标对象的所有接口。如果目标对象没有实现任何一个接口, CGLIB 类库将会创建。
从这个问题也可以引申出来深层次的话题就是 SpringAOP 技术底层就是这么运作的。
五、 总结:
最终的结论其实是很简单的,但是探索的过程确实很复杂。其中更多的实现细节因我功力不够,还是看的不大懂,但是知道了大体的实现机制,还算是有很大的收获的!
PS: 希望对更多遇到此类问题的朋友有所帮助,如果本文哪里说的不是很到位,欢迎大家批评指正 ~~~