由because it is a JDK dynamic proxy that implements温习Spring的代理
- 项目场景
- 原因分析
- 1、报错位置
- 2、错误原因
- 3、业务需求
- 解决方案
- 1、注入CGlib代理
- 2、取出原生对象
项目场景
昨日在启动一个SpringBoot项目时,发现启动失败,并在日志中出现了这样的报错:
The bean ‘XXXXManageImpl‘ could not be injected as a ‘XXXXManageImpl‘ because it is a JDK dynamic proxy that implements : XXXXManage
有些Spring版本则是 The bean ‘XXXXX’ could not be injected because it is a JDK dynamic proxy,如下:
我们先来看一下产生报错的相关代码
// 类的定义
@Service
public class XXXXManageImpl extends AbstractManage implements XXXXManage {}
// 其他地方要注入 XXXXManageImpl
@Autowired
XXXXManageImpl XXXXManageImpl;
原因分析
1、报错位置
形如
***************************
APPLICATION FAILED TO START
***************************
这种报错实际上是 SpringBoot 分析后的报错,而根据输出的内容看,其原始报错类型其实为 BeanNotOfRequiredTypeException
, 经由BeanNotOfRequiredTypeFailureAnalyzer
分析后输出的指导性错误。那么顾名思义,这应该是在Bean装配过程中,出现了指定类型与Bean实际类型不符的场景。
2、错误原因
其实从报错中也不难看出来,说的就是我们希望注入一个类型为”XXXXManageImpl“的Bean,系统确认了这个Bean是存在的,但是现在这个Bean的实例的类型却并不是 ”XXXXManageImpl“,而是一个JDK代理类
(com.sum.Proxy),两者虽然都实现了XXXXManager,但只属于兄弟类,此时自然无法注入成功
如果你还不太清楚动态代理,可以在过去的文章中复习一下《Spring核心特性—— AOP(面向切面编程)》。 一个类如果带有@Component
或者@Service
等注解,就会被实例化后放进Spring容器。至于最终放进去的可能是这个类的实例,也可能是这个类的代理类的实例
而至于这个类什么情况下会生成动态代理?其实原因有很多,比如这里我们使用的这个 ”XXXXManageImp“ ,在他的方法中存在着@Transactionol
注解,正因为这个注解,导致这个类在创建Bean,并放入Spring容器时,其实放进去的是这个类的代理,而且是JDK代理 (可参见 《Spring事务畅谈 —— 由浅入深彻底弄懂 @Transactional注解》)
而JDK代理的实例类型为 com.sum.Proxy
jdk.proxy3.$ProxyXXXX
等(不同jdk版本可能有所不同),但肯定不是我们的业务类 XXXXManageImpl, 所以这次注入是肯定会失败的。
3、业务需求
一般来说,如果我们对一个接口 XXXXManage 有一个实现类 XXXXManageImpl,那么我们在其他地方注入这个Bean 时,最好使用注入的是接口,即如下:
@Autowired
XXXXManage XXXXManage;
但是原代码为什么注入的是实现类呢? 是因为这个实现类本身除了实现了XXXXManage接口外,还继承了一个名叫 AbstractManage 的抽象类,也就是说这个类不单纯是某个接口的实现,他还肩负着一些额外的功能与方法
// 类的定义
@Service
public class XXXXManageImpl extends AbstractManage implements XXXXManage {}
而业务需要调用的就是这个类的其他方法(而非来自接口XXXXManage的方法),所以不能使用注入XXXXManage 的方式。
解决方案
结合我们的业务,我们的核心问题是:我们想要注入一个类,并使用该类自身的方法。但是这个类实际在Spring容器中以JDK代理形式存在,所以如果注入其代理 -> 则无法用到该类自身方法; 注入原生类 -> 启动报错。
所以问题的解决思路可以有以下几种:
1、注入CGlib代理
这也是Spring推荐的一种策略:其原文如下:
Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxiesby setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.
考虑将该 Bean 注入为其接口之一,或者通过在 @EnableAsync 和/或 @EnableCaching 上设置 proxyTargetClass=true 来强制使用基于 CGLib 的代理。
但是我们这里的动态代理,实际是由 @Trasactionol引入的,而@Trasactionol 并没有proxyTargetClass
属性。我们当然可以使用全局的 proxyTargetClass 来解决,比如在配置文件中配置,或者在启动类上的@EnableTransactionManagement注解里加这个属性
@EnableTransactionManagement(proxyTargetClass = true)
而且SpringBoot2就已经将CGlib动态代理设为默认配置,但是由于项目本身是个老项目,积重难返,这个项目一旦全体采用 CGLib 的代理 会导致很多问题。(详见《【问题处理】—— SpringBoot2 动态代理问题排查》)
所以应该说:如果没有历史负债的项目,可以使用这种方式,从而解决该问题。
2、取出原生对象
既然不能使用CGlib,可以采用原生对象解决该问题,即我们虽然是注入代理对象,但在要用到类自己的方法时,可先取出代理的原生对象,然后调用原生对象的方法
// 注入时还是注入JDK代理
@Autowired
XXXXManage XXXXManage;// 需要获取原对象时
XXXXManageImpl obj = (XXXXManageImpl)AopProxyUtils.getSingletonTarget(XXXXManage);
通过AopProxyUtils的方法,就可以通过代理对象,获取其原始对象,进而开始使用了。需要注意的是我们取得并使用原对象,就意味着失去了代理的那些功能。所以这种操作还是需要谨慎一些的。