Bean定义冲突的一次探究

1.问题背景

最近有个老的项目,叫做spring-boot-dubbox(这老项目名字取的也比较随意),一直没做成放到gitlab的CICD上去走自动化部署,在有些情况下打包比较麻烦,所以这次打算将其放cicd上做成自动化的方式。

2.问题出现

这个项目本地一启动就报错,直接报bean的定义冲突。


如果有经验的话,就能大致的知道这种情况一般出现就表示在服务里面有2个相同的Bean,名字都是applicationContextUtil

3.定位

首先查看这个类在哪些jar出现:

image.png

发现在xxx-boss-common-0.0.2.jarload-balance-0.0.4.jar都有。
对应的定义:
xxx-boss-common-0.0.2.jar中:
wmremove-transformed.jpeg

load-balance-0.0.4.jar中:

wmremove-transformed.png

那么出现这种问题修复也不麻烦,修复的方式有2种:
方式一:在springboot中使用排除其他名字都是applicationContextUtil的Bean,只保留其中一个
方式二:利用java的类加载特性,重写覆盖掉冲突的bean,修改bean的名字,保证2个bean的名字不一样即可

方式一:排除Bean

@SpringBootApplication(exclude = {MongoAutoConfiguration.class,MongoDataAutoConfiguration.class})
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = com.xxxxx.dubbo.lb.util.ApplicationContextUtil.class))
public class ApplicationMain {
    private final static Logger LOGGER = LoggerFactory.getLogger(ApplicationMain.class);

注意这里由于是ApplicationContextUtil是用的@Component注解,所以不能用@SpringBootApplication里面的exclude;因为@SpringBootApplication的exlude是排除那种@Configuration注解的bean

@SpringBootApplication和@ComponentScan排除对比
1.排除用户/第三方普通 @Component 类 → 用 @ComponentScan.excludeFilters
2.排除 Spring Boot 的自动配置类 → 用 @SpringBootApplication.exclude 或 excludeName

方式二:覆盖掉冲突的bean,修改bean的名字

wmremove-transformed2.png

这里是覆盖掉了load-balance-0.0.4.jar里面的相同路径的ApplicationContextUtil类,然后重写了Bean的名字,叫做了lbApplicationContextUtil

4.修复

修复 - part1

最终我这边选择了方式二,因为方式一写法在我们这个系统不行:
老的框架有点问题,这个项目是用的@SpringbootApplication,但是load-balance-0.0.4.jar还用了dubbo的xml配置方式,好巧不巧的里面又设置了bean的扫描,这里会导致@ComponentScan(excludeFilters = xxxx不起作用,因为这里的<context:conponent-scan>会再次对bean进行一次扫描,这次扫描和@SpringbootApplication不是同一次,并且用的对象也不同同一个,导致@ComponentScan(excludeFilters = xxxx失效,然后还会出现Bean名字的冲突。
这里面更深的探究可以后续再出一篇。

wmremove-transformed.png

修复 - part2

在我们覆盖重写了这个bean后,在idea中是能启动成功了,并且没有问题。
但是走CICD部署,发现这个问题又出现了!而且还是同样的Bean定义报错。

那么奇怪了,为什么本地intellij的debug的可以,但是走CICD不行,于是我经过一些排查代码是否是最新等等验证,大致猜测是加载顺序的问题,导致优先加载了load-balance-0.0.4.jar的ApplicationContextUtil类,进而影响到项目本身重写的类并没有优先加载到。

那么只需要对比一下本地intellij idea是怎么加载的,和服务器上加载的过程:
本地的启动参数

"C:\Program Files\Java\jdk1.8.0_191\bin\java.exe" 
省略...
Files\Java\jdk1.8.0_191\jre\lib\rt.jar;D:\workspace\project\xxxx-ocs-system-auth\spring-boot-dubbox\target\classes;
省略..
loadbalance\loadbalance\0.0.4\loadbalance-0.0.4.jar;
省略..
 com.xxxxxx.ApplicationMain

可以看出它是先将本地构建出的target目录优先的,然后再是loadbalance-0.0.4.jar
所以能覆盖loadbalance-0.0.4.jarApplicationContextUtil,不会报错。

那么再看一下gitlab上面CICD的启动参数,由于当时没打印启动的,但是从运行的shell脚本大致能看出来:

// 省略
LIB_DIR=$DEPLOY_DIR/lib
LIB_JARS=`ls $LIB_DIR|grep .jar|awk '{print "'$LIB_DIR'/"$0}'|tr "\n" ":"`

// 省略
exec java $JAVA_AGENT $JAVA_OPTS $JAVA_MEM_OPTS $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS $JAVA_OTHER_OPTS -classpath $CONF_DIR:$LIB_JARS com.xxx.ApplicationMain > /dev/null 2>&1

这里启动的classpath是通过变量$CONF_DIR:$LIB_JARS设置的,那么LIB_JARS又是通过命令ls查看目录里面的jar包来传递,也就是ls的jar包的顺序,就是启动时候,加载class的顺序

ls命令的输出排序规则

在 POSIX/C locale 下的排序规则(最常见情况)

大写字母 先于 小写字母(因为 ASCII 码:A=65 < a=97)
纯字母比较:l < s

所以文件名以这些开头时,顺序通常是:
开头典型例子排序位置(早 → 晚)
load-xxx.jarload-balance-xxx.jar 较早 spring->xxx.jar
spring-web-xxx.jar 较晚 Spring-xxx.jar(大写 S)最早(因为 S < l < s)

刚好我们项目jar包是spring-boot-dubbox,排在loadbalance.jar包的后面,所以优先加载loadbalance.jar

那么需要修复的话就需要改变包的顺序。

下面是修复shell脚本,将spring-boot-dubbox排序到最前面即可:

LIB_JARS=`ls $LIB_DIR | grep spring-boot-dubbox.*\.jar | awk '{print "'$LIB_DIR'/"$0}' | tr "\n" ":"`
LIB_JARS=$LIB_JARS`ls $LIB_DIR | grep .jar | grep -v spring-boot-dubbox | awk '{print "'$LIB_DIR'/"$0}' | tr "\n" ":"`

脚本分析:
第一行

ls $LIB_DIR                          # 列出目录下所有文件(文件名)
| grep spring-boot-dubbox.*\.jar     # 只保留文件名匹配 spring-boot-dubbox 开头 + .jar 结尾的行
| awk '{print "'$LIB_DIR'/"$0}'      # 在每个文件名前面拼接上路径前缀 $LIB_DIR/ (注意这里用了双引号嵌套单引号技巧来插入变量)
| tr "\n" ":"                        # 把每行末尾的换行符替换成冒号,变成 classpath 风格的单行字符串

第二行

LIB_JARS=$LIB_JARS                   # 保留第一行已经生成的字符串(不加引号,经典老写法)
`                                    # 命令替换开始
  ls $LIB_DIR                        # 再次列出所有文件
  | grep .jar                        # 先粗筛出包含 .jar 的文件名(不严谨,但常见写法)
  | grep -v spring-boot-dubbox       # 排除掉包含 spring-boot-dubbox 的行(-v = invert match)
  | awk '{print "'$LIB_DIR'/"$0}'    # 同上,加路径前缀
  | tr "\n" ":"                      # 同上,转成 : 分隔
`                                    # 命令替换结束

改成这样后,再次构建部署,可以成功启动!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容