Medusar's Blog
敬畏知识,谦逊前行
Toggle navigation
Medusar's Blog
主页
Booklist
Resources
About Me
归档
标签
记一个Spring动态代理的坑
spring
坑
2016-04-29 12:29:25
1750
0
0
medusar
spring
坑
## 问题描述 项目中有一个接口`ConfigService`,该接口有一个实现类`RedisDBConfigService`,实现类中有个方法使用了Spring的事务注解`@Transactional`,代码大致如下: ``` java @Service public class RedisDBConfigService implements ConfigService { @Autowired private DataMapper mapper; //为了做单元测试,提供一个set方法 public void setDataMapper(DataMapper dataMapper) { this.mapper = dataMapper; } //基于Spring事务控制 @Transactional(rollbackFor = Exception.class) @Override public void updateConfig(Data data) throws Exception { doSomething(); } } ``` Spring的事务配置如下: ``` xml <!-- 注解方式配置事物 --> <tx:annotation-driven transaction-manager="transactionManager"/> ``` 这种方式有一个隐含条件就是:Spring为需要事务控制的类创建代理的时候,默认使用JDKProxy来创建。 以上是前提条件。这里要说明一下,程序并没有任何问题,上面的设置也是正确的,可以正确无误运行。那么这样的设置出现了什么问题呢? ## 问题回放 问题出在单元测试上。如果仅仅测试一下updateConfig方法能否正确运行,是很简单的,也不会有问题。但是单元测试的意义不仅于此,有时候我们需要测试一些异常情况。 比如这个例子中,我需要测试一下DataMapper执行出错抛异常的时候程序的运行结果。为了让程序抛异常,我的思路是自己实现一个DataMapper,然后在方法中直接抛出异常,然后将该DataMapper赋值给RedisDBConfigService,所以就有了上面类中的`setDataMapper方法,其实就是为了做测试`。暂且不说这样的测试方式好不好,我们来看下会有什么问题。 单元测试类: ``` java public class ConfigServiceTest extends SpringTestBase { @Autowired private ConfigService service; @Test public void testRedisExceptionGetConfig() throws Exception { RedisDBConfigService dbConfigService = (RedisDBConfigService) this.service; dbConfigService.setDataMapper(new DataMapper() { @Override public String update(String key) { throw new RuntimeException("mock time out"); } }); dbConfigService.updateConfig(xxx); } } ``` 上面代码需要注意的是,因为ConfigService接口中并没有setDataMapper方法,只有子类RedisDBConfigService中有,所以设置的时候需要先将service向下转换成子类。 运行程序,发现抛异常了,异常信息如下: ``` bash java.lang.ClassCastException: sun.proxy.$Proxy42 cannot be cast to xxx.xxx.xxx.RedisDBConfigService ``` 意思是,强制转换失败了,而我是要将Proxy的对象转换成RedisDBConfigService的对象,那当然会失败啊! ## 问题分析 为什么会有这种情况呢?为什么注入进来的ConfigService实例会是Proxy呢?经过一番分析,原因如下: 因为我们的RedisDBConfigService中使用了@Transactional注解,即需要Spring事务支持,而Spring的事务实现就是靠生成目标对象的代理来实现的,Spring的代理生成有两种:JDK的Proxy和AspectJ,而默认是JDK的Proxy,所以虽然我们代码中写的是ConfigService,但实际上它的对象是生成后的Proxy,而Proxy对象和RedisDBConfigService没有任何继承关系,所以强制转换是失败的。 那么是不是这样呢? ## 问题验证 ### 推测1: 去掉Spring的Transactional注解,测试代码应该不会出现强转失败的问题 验证结果:正确。 原因:去掉事务注解,就不需要为Bean创建动态搭理,所以就不存在这个问题。 ### 推测2: 将Spring生成代理的方式改成AspectJ,测试代码也应该不会有问题 修改Spring配置文件: ``` xml <tx:annotation-driven transaction-manager="transactionManager" mode="aspectj"/> ``` 验证结果:正确 原因:AspectJ是使用CGLib,它创建的代理类是基于实现类本身的,所以不会出现强制转换失败的问题。 ### 推测3:如果将`ConfigServiceTest`类中声明的`ConfigService`直接改成它的子类`RedisDBConfigService`会怎样?(还是使用JDK动态代理) 即: ``` java public class ConfigServiceTest extends SpringTestBase { @Autowired private RedisDBConfigService service; } ``` 结果:NoSuchBeanDefinitionException。 原因:因为JDK动态代理是基于接口的,如果我们声明类改成了实现类,那肯定是找不到的。但如果将@Transactional注解去掉,代码是可以正确运行的。因为不需要生成代理。 ### 分析: 通过JDKProxy动态代理生成的实例实际上是Proxy类的对象,如下图:  而通过AspectJ生成的动态代理,本身就是RedisDBConfigService类的对象,如下图:  ## 总结: 1. Spring事务的实现是基于动态代理的,Spring支持两种动态代理生成,JDKProxy和AspectJ,默认使用前者。 2. JDKProxy是基于接口的,而AspectJ由于是使用CgLib,所以不需要实现接口也可以实现代理功能。 3. Spring默认创建Bean是不会生成代理的,只有需要实现一个额外功能,比如AOP的时候,才会生成代理。 4. JDKProxy生成的代理类,是无法强制转换成目标接口的某个实现类的对象的 5. 需要注意的是,如果是使用AspectJ作为动态搭理生成,@Transactional注解必须标注在实现类上,而不能标注在接口上。
上一篇:
为什么HashMap要自己实现writeObject和readObject方法?
下一篇:
设计模式在Spring中的应用
0
赞
1750 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
Please enable JavaScript to view the
comments powered by Disqus.
comments powered by
Disqus
文档导航