由because it is a JDK dynamic proxy that implements温习Spring的代理

项目场景

昨日在启动一个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的方法,就可以通过代理对象,获取其原始对象,进而开始使用了。需要注意的是我们取得并使用原对象,就意味着失去了代理的那些功能。所以这种操作还是需要谨慎一些的

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

战斧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值