【问题处理】—— SpringBoot2 动态代理问题排查

简介: 【问题处理】—— SpringBoot2 动态代理问题排查

现象:springboot 1 -> 2 升级后业务异常

系统升级至springboot2 spring5 以后,系统大量接口报错,经定位,是在某一个方法切面中获取当前方法所在类的对象名时,获取到的是实现类,而我们需要类名的是接口类名,导致问题。


分析:动态代理获取类名有误

方法切面获取的是实现类,主要是因为切面生成的代理,采用的是CGLIB动态代理,这样获取的类就是实现类的名字,若采用spring自带的判断逻辑,有接口优先使用jdk动态代理,此时获取到的就是接口名了。


深入

主要原因就是动态代理选择的方式与原来不同,spring5的方法有接口优先选jdk动态代理,而springBoot2则指定默认的代理选择CGLIB,因此生成代理时全是CGLIB。


第一阶段:套用网上教程,结果无效

根据网上资料,在application.properties设定参数 spring.aop.proxy-target-class=false,但结果无效,继续分析原因

springBoot2采用的CGLIB的源码在其一个配置类AopAutoConfiguration=中,可以看到如果不配置spring.aop.proxy-target-class=false,确实会走Cglib代理,配了才会走jdk,和网传的一样,但为什么还是没起作用呢?


866fea9f2f7c4a4b81be233040aff1a6.png

第二阶段:分析网上教程的实际作用,以及此处为什么无效

先看看如果不配spring.aop.proxy-target-class=false,为什么就会走cglib代理? 这个CglibAutoProxyConfiguration类我们可以看到是空的,除了类名什么也没有,因此关键不在于这个类,而是其上的 @EnableAspectJAutoProxy(proxyTargetClass = true)注解

我们可以看到,这个注解被设定了属性proxyTargetClass = true,

048a39bed48042cdbfbbd8f323c08ed6.png

进入查看详情,发现这个注解自己也带有注解,这次是Import了AspectJAutoProxyRegistrar.class,

3297a56794d4450582b57cd4c29d0ce3.png

这个AspectJAutoProxyRegistrar类我们不再继续深究,只简单看一眼,可以看到这个类实现了ImportBeanDefinitionRegistrar,表明这个类专门就是给别人import的,而且在被import的时候能获取别人的属性,并根据获取到的属性执行特定代码。

此处这个类就获取到了我们放在@EnableAspectJAutoProxy(proxyTargetClass = true)注解上的值proxyTargetClass = true,从而执行了一段代码,看名字就得知,该段代码就是强制代理创建使用CGLIB的方式。

273cfe6efcc442c58499f0c747225695.png

那么问题来了,当我们配置了spring.aop.proxy-target-class=false,就不会走这段逻辑,不会强制使用CGLIb,但代码为什么还是强制用的CGlib呢?

我们可以在这个强制使用Cglib的方法里打个断点,然后启动项目

55061239f85c44e8884e887e1f1c5722.png


发现该方法被多次调用执行,通过栈去找调用的源头,发现除了SpringBoot2自己那个AopAutoConfiguration配置,还有很多注解都有类似的功能,比如我们经常用来开启事务的注解@EnableTransactionManagement

2599a0bc536c4061a1a9e0f11e41351a.png



还有开启方法缓存的 @EnableCaching 注解都包含proxyTargetClass的设置,同时也都import了一个类,在这个类中会对设置的proxyTargetClass判断,若为true,则执行强制CGLIB的方法

aba7f20a42904d64840ec08229634a54.png


最关键的是proxyTargetClass不是项目的参数,它并不唯一,它只是这些注解的属性,不同的注解,甚至同一个注解在不同位置的使用都可以独立的设值。而只要有一处设置为true,就会执行一段使用CGlib的代码,这就意味着,如果想要阻止其强制使用CGLIB,就要把上述所有注解的在所有位置的使用都设置成proxyTargetClass = false。我们项目中的使用可以自己控制,springboot2内置的也可以通过spring.aop.proxy-target-class=false控制,但有些注解的使用是在第三方包中,它设定了proxyTargetClass = true后,我们也没办法去改动,从而导致一定会执行该段代码


第三阶段:分析代理使用CGlib的原因

要想解决问题,还是得看这段代码执行了什么,为什么执行后就会强制CGlib,是否执行后还有抢救的余地?我们可以看到这个方法实际上是寻找一个叫做“org.springframework.aop.config.internalAutoProxyCreator”的Bean定义,为它增加一个proxyTargetClass = true的属性。但是,spring源码中其实并没有一个叫internalAutoProxyCreator的类,因此我们还必须搞清楚代理创建的真实位置。


dd57efdb13024a389bcf67715a8ccd67.png


通过debug发现,这个Bean实际类型是可变的,对应着三个类,由优先级控制,列表越后优先级越高,它可以是InfrastructureAdvisorAutoProxyCreator,也可以被后两者顶替,三者全是AbstractAutoProxyCreator的子类,因此带有后置处理器(SmartInstantiationAwareBeanPostProcessor),可以在初始化后对Bean进行处理


05b6140826a54a83b31ed03817c8e62d.png

bf010249500e4cfca7d0a62792bbde58.png


而且从名字可以看出来,这个类(AbstractAutoProxyCreator的某个子类)是基类,是帮助创建代理的,更具体的说,在为某一个对象创建代理时,会先新建一个代理的配置,此时这个类(AbstractAutoProxyCreator的某个子类)的属性会被复制到代理配置里。如下图,可以看到,代理配置里会根据这个属性生产代理,这里就是创建并直接返回了cglib代理

66e11d69252843b8abdcec14b412fd04.png

70968243ee6247269bd03c3d9c809a05.png


至此,问题引起的全流程,都已经完全清楚了,整体来说,就是在任一位置设定的proxyTargetClass = true 都会在“代理创建基类”(AbstractAutoProxyCreator的某个子类) 内增添一个proxyTargetClass = true的属性,且无法再设为false,而这个属性会在代理生成时使其走向某个分支,那么在这个分支内就会返回CGlib代理了,只是问题该如何解决呢


第四阶段:利用Bean创建流程解决问题

我们现在要做的事很明确,将“代理创建基类”(AbstractAutoProxyCreator的某个子类) 的proxyTargetClass属性设置为false,只有这样,当这个基类去创建动态代理时,才能采用JDK动态代理。

一般情况下,如果要在一个Bean被我们使用前去改它的属性,可以通过BeanPostProcessor(Bean后置处理器)去修改,但是如果这个Bean本身就是一个BeanPostProcessor,那他们的优先级就是一样的,自然无法相互作用。这个时候,如果我们想更改这种Bean的属性,就只能更加提前,在所有Bean(包括BeanPostProcessor这种特殊的Bean)都没实例化之前,去对其做修改。我们能现在能自然的想到就是BeanFactoryPostProcessor,或者其子接口BeanDefinitionRegistryPostProcessor,实现这两者,就可以在实例化之前直接修改任何Bean的定义,这样当他们实例化时,就带着我们设定的属性值了。

f6c24597ba1c4f438655fec702dcf70a.png


此处,可以自定义一个类,实现BeanDefinitionRegistryPostProcessor接口,这个接口可以在Bean定义阶段完成后,对Bean定义做自定义处理


结果

经测试,所有动态代理均被设置为jdk动态代理,保持了 springboot1 时的默认配置,项目上实现了兼容


目录
相关文章
|
设计模式 监控 前端开发
SpringBoot拦截器和动态代理有什么区别?
SpringBoot拦截器和动态代理有什么区别?
111 0
|
IDE Java 开发工具
深入Spring Boot: 怎样排查 java.lang.ArrayStoreException
java.lang.ArrayStoreException 分析 这个demo来说明怎样排查一个spring boot 1应用升级到spring boot 2时可能出现的java.lang.ArrayStoreException。
7876 0
|
9月前
|
设计模式 安全 Java
深入理解Spring Boot AOP:CGLIB代理与JDK动态代理的完全指南
深入理解Spring Boot AOP:CGLIB代理与JDK动态代理的完全指南
2205 1
|
SpringCloudAlibaba Java Nacos
springboot整合nacos做配置中心的一次问题排查
这里先把问题陈述一下:**springboot整合nacos做配置中心,使用@NacosConfigurationProperties注解注入bean属性,当nacos前台客户端没有配置这个bean的属性,项目
1154 0
|
Java 数据库连接 应用服务中间件
Springboot 使用 Mybatis 启动失败排查定位
收获 当遇到项目启动失败,却没有错误日志打印出来的时候,试试在 run 方法上加个 try-catch,即可捕获到异常 Mybatis 的别名扫描路径不要指定的太宽泛,有可能会出现 Bean 名冲突,导致初始化失败 联想到之前碰到的问题,例如: 第一个想到的应该是加一层 try-catch,自己把异常捕获出来。如果能看到异常,就离解决问题不远了 (补充闪电侠名言:如果能问题能复现,基本上就快解决了)
|
Arthas Java 测试技术
深入Spring Boot:利用Arthas排查NoSuchMethodError
## 前言 有时spring boot应用会遇到`java.lang.NoSuchMethodError`的问题,下面以具体的demo来说明怎样利用[arthas](https://github.com/alibaba/arthas)来排查。 Demo: https://github.com/hengyunabc/spring-boot-inside/tree/master/dem
2726 0
|
IDE Java 开发工具
深入Spring Boot:怎样排查expected single matching bean but found 2的异常
写在前面 这个demo来说明怎么排查一个常见的spring expected single matching bean but found 2的异常。
6690 0
|
消息中间件 JavaScript 小程序
记一次SpringBoot启动异常,jar问题的排查分析
记一次SpringBoot启动异常,jar问题的排查分析
记一次SpringBoot启动异常,jar问题的排查分析
|
编解码 监控 NoSQL
疑案追踪:Spring Boot内存泄露排查记
疑案追踪:Spring Boot内存泄露排查记
935 0
疑案追踪:Spring Boot内存泄露排查记