首先还原一下问题
这边我开发的一个service类 :ContractProjectFlowServiceImpl,和它的接口 :ContractProjectFlowService。
然后同事在他自己的类里面是这么注入的:
@Autowired
private ContractProjectFlowServiceImpl projectFlowService;
直接注入的实现类,这样也没什么问题一直跑的好好的,但是昨天我写一个别的功能的时候,在ContractProjectFlowServiceImpl类中加了一个新的方法用到了@Transcational注解之后(当然还改了很多别的代码,但是最后确定就是因为加了这个注解导致的),这个注入就开始报错,只贴出一部分:
很常见,很熟悉的报错,说的是spring在自动注入ContractProjectFlowServiceImpl这个bean的时候找不到对应的bean,为什么会这样呢?毫无疑问是跟这边声明依赖的时候写的是实现类ContractProjectFlowServiceImpl 而不是它的接口ContractProjectFlowService有关,把这个地方改过来果然就好了,但是为什么之前好好的,这会突然报错了呢,仔细检查昨天提交的代码,最后把问题确定到了@Transcational注解上了。那么为什么加了@Transcational,就会导致这个问题呢?经过自己的思考和追踪源码,:
首先,回忆一下,spring是如何实现@transaction注解的事物控制的呢?答案是切面——通过切面spring把对事物传播的控制和事物失败回滚的控制放到了你的业务逻辑代码之前和之后。而对于像这种有接口的实现类,spring会通过java的动态代理机制来实现切面。也就是说当加了@Transcational注解之后,spring会给ContractProjectFlowServiceImpl生成一个动态代理类,启动容器的时候放到容器中的就不再是ContractProjectFlowServiceImpl而是它的代理类。这里复习一下java动态代理的知识,对ContractProjectFlowServiceImpl这个类生成的动态代理类大概是这样的:
$Proxy0 extends java.lang.reflect.Proxy implements ContractProjectFlowService
{
java.lang.reflect.Method m4;
java.lang.reflect.Method m2;
java.lang.reflect.Method m0;
java.lang.reflect.Method m3;
java.lang.reflect.Method m1;
void ContractProjectFlowService();
int hashCode();
boolean equals(java.lang.Object);
java.lang.String toString();
}
它会实现也会实现ContractProjectFlowService接口,但与ContractProjectFlowServiceImpl是完全不同的类型。当在容器中根据ContractProjectFlowServiceImpl这个类型去找对应的bean的时候当然就会因为找不到而报错。
到这里为止这个错误的原因就解析清楚了,简而言之就是对于用到了动态代理的类应该注入他的接口而不能注入他的实现类。那么到这里就不免会产生几个疑问:
疑问一.我们知道@autowired是按照类型来注入的所以会找不到对应的bean报NoSuchBeanDefinitionException,但是@resouce注解是根据name去注入的,应该能够找到代理类的bean,那么用@resouce去注入能成功吗?
这里再复习一下@autowired 和 @resouce 注解:
@autowired注入的步骤是
1、按照属性类型查找对应的类型的bean,如果没有则报错。如果有多个实现类,开始执行第二步。
2、查看实现类是有“@Primary”注解,或者在调用类中使用“@Qualifier("")”执行具体的实现类,如果都没有使用,开始执行第三步。
3、通过反射属性的变量名作为beanName,通过那么去查找,如果找不到报错。
@resouce 注入的步骤是
1、先通过首先resouce注解有没有指定name属性,如果没有,开始执行第二步。有则通过name去ioc容器找对应的bean,找不到或者找到多个就会直接报错
2、通过反射属性的变量名作为beanName去ioc容器里寻找,如果这是还找不到,开始执行第三步
3、按照属性类型查找对应的类型的bean。
@resouce应该能找到代理类生成的bean,但是这边声明的类型是ContractProjectFlowServiceImpl ,和找到的bean 的类型对不上,推测应该会报类型转换异常之类的错,实际实验了一下发现果然如此:
疑问二. 我们知道spring有2种方式实现切面,一种就是上面提到的jdk的动态代理,另一种则是cglib,如果是通过cglib的方式去实现切面是不是就不会出现这个问题了呢?
首先还是复习一下spring在什么时候会选择jdk的动态代理来实现aop什么时候选择cglib。这里可以直接看代码,在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);
}
}
简单说就是如果一个类有接口,则默认使用jdk的动态代理来代理,如果直接是一个类,则使用cglib代理。
好回到这问题,那么如果ContractProjectFlowServiceImpl 没有接口的话,使用@transaction注解,spring会用cglib对他生成代理,是否就不会报这个错了呢?
答案是肯定的,经过验证,当把接口类删除之后确实也不会报错。
原因在于CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。也就是说通过cglib去生成的代理类是ContractProjectFlowServiceImpl的子类,是可以通过类型匹配到父类的。
以上~
ps:
补充一点
另外在配置文件中将proxy-target-class指定为true也可以直接使用cglib来生成代理
https://blog.csdn.net/shaoweijava/article/details/76474652
<tx:annotation-driven proxy-target-class="true" transaction-manager="appTransactionManager" />