1.问题背景
最近有个老的项目,叫做spring-boot-dubbox(这老项目名字取的也比较随意),一直没做成放到gitlab的CICD上去走自动化部署,在有些情况下打包比较麻烦,所以这次打算将其放cicd上做成自动化的方式。
2.问题出现
这个项目本地一启动就报错,直接报bean的定义冲突。

如果有经验的话,就能大致的知道这种情况一般出现就表示在服务里面有2个相同的Bean,名字都是applicationContextUtil。
3.定位
首先查看这个类在哪些jar出现:

发现在
xxx-boss-common-0.0.2.jar和load-balance-0.0.4.jar都有。对应的定义:
在
xxx-boss-common-0.0.2.jar中:
在load-balance-0.0.4.jar中:

那么出现这种问题修复也不麻烦,修复的方式有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的名字

这里是覆盖掉了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名字的冲突。
这里面更深的探究可以后续再出一篇。

修复 - 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.jar的ApplicationContextUtil,不会报错。
那么再看一下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" ":" # 同上,转成 : 分隔
` # 命令替换结束
改成这样后,再次构建部署,可以成功启动!