Jenkins加载Spring扩展库出错排查

Photo by Roberto Nickson from Pexels

1. 问题描述

这段时间隔壁的PHP部门正在将原本使用Walle的交付体系逐渐迁移到Jenkins平台上,在Jenkins的使用上我也做过一段时间的探索,学习社区内优秀的思想。

然而今天PHP部门的同事告诉我,原本由我参与开发的Jenkins扩展库无法运行在他们搭建的Jenkins上,且异常的内容看起来非常奇怪:

java.lang.NoSuchMethodError: org.springframework.util.CollectionUtils.unmodifiableMultiValueMap(Lorg/springframework/util/MultiValueMap;)Lorg/springframework/util/MultiValueMap;
	at org.springframework.web.util.HierarchicalUriComponents.<clinit>(HierarchicalUriComponents.java:60)
	at org.springframework.web.util.UriComponentsBuilder.buildInternal(UriComponentsBuilder.java:469)
	at org.springframework.web.util.UriComponentsBuilder.build(UriComponentsBuilder.java:459)
	at org.springframework.web.util.UriComponentsBuilder.build(UriComponentsBuilder.java:446)
	at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.build(DefaultUriBuilderFactory.java:403)
	at org.springframework.web.util.DefaultUriBuilderFactory.expand(DefaultUriBuilderFactory.java:154)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
	at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1213)
	at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
	at org.codehaus.groovy.runtime.callsite.PojoMetaClassSite.call(PojoMetaClassSite.java:47)
	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48)
	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:113)
	at com.cloudbees.groovy.cps.sandbox.DefaultInvoker.methodCall(DefaultInvoker.java:20)

从异常信息上来看是Spring工具类org.springframework.util.CollectionUtils下的unmodifiableMultiValueMap方法没有找到,进入Spring源码看这个方法是自Spring 3.1开始就提供的方法,算是比较早的了。

2. 异常复现

首先简单的模拟一下问题环境,Jenkins扩展库方面只引入一个Spring Core来复现工具类的问题,pom文件:

        <!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>3.0.8</version>
            <type>pom</type>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.3.3</version>
        </dependency>

然后加一个简单的类,提交到Git远程仓库中作为扩展库。

/**
 @author fengxiao
 @date 2021-09-03
 */
class CollectionUtilTest {

    void collectionIsEmpty(){
        println CollectionUtils.unmodifiableMultiValueMap(null)
    }

}

现在在Docker上运行较低版本的Jenkins容器:

docker run --name jenkins1 --rm  -d -p 8081:8080 -p 50001:50000 jenkinsci/blueocean:1.23.2

这里略过了Jenkins基本的初始化过程,例如:

  1. 设置Jenkins初始密码
  2. 设置系统插件更新地址为国内镜像源,目前比较好用的有Jenkins中文社区、清华镜像源、华为镜像源等。这里我选择了社区的镜像源地址:https://updates.jenkins-zh.cn/update-center.json
  3. 设置远程扩展库 (Configure System -> Global Pipeline Libraries)

接下来编写一个Demo流水线:

@Library('jenkins-support@master') _
import com.landscape.jenkins.lib.CollectionUtilTest

node{
    stage('test'){
        println "Test Spring Utils"
        new CollectionUtilTest().collectionIsEmpty()
    }
}

运行后复现出了相同的问题,图中的异常是hudson.remoting.ProxyException: groovy.lang.MissingMethodException.

这是因为上面的Groovy代码中直接调用了不存在的方法,而如果是通过Spring的类库间接引用到就是java.lang.NoSuchMethodError

3. 探究原因

其实从上面的异常就已经大致能猜到是类加载错误导致的问题,但是日常使用Jenkins的时候很少会出现这样的问题,Google和StackOverflow上相似的情况非常少,

在StackOverflow上有一个最相似的情况:https://stackoverflow.com/questions/39313324/nosuchmethoderror-when-developing-jenkins-plugin-with-spring-social-framework

但并没有回答说明原因,所以我知道这个问题其实很简单,也简单的记录下,或许帮助遇到这类问题的同学。

对于这类问题有一个非常简单粗暴的方案,即在Groovy中获取类的Jar包信息:

/**
 @author fengxiao
 @date 2021-09-03
 */
class CollectionUtilTest {

    String collectionIsEmpty() {
        return CollectionUtils.class.getResource("CollectionUtils.class").toString()
    }

}

从前面Spring源码中可以看到这个方法实际上是在Spring 3.1才加入到core包中,而这里实际上是从Spring2.5版本中获取的工具类,那当然找不到方法。

熟悉Jenkins的都知道,Jenkins是基于Spring框架的项目,Jenkins的运行必定会加载特定版本的Spring类库,而再遇到相同的类时,即使我们指定了Spring版本也不会重新加载。

所以解决这个问题的方案不是清空maven版本库,也不是添加hpi插件,而是提升Jenkins版本,在你的Jenkins_URL/about 页面可以看到Jenkins的依赖信息,本例运行的Jenkins镜像中的Jenkins是 2.249.1 ,依赖于Spring 2.5.6。

而我日常工作所稳定运行的Jenkins版本是2.278,仅仅相隔一年不到,底层依赖的Spring已经提升到了spring-core:5.2.11。看来也是经过了一次大范围的重构。

总结:流水线代码不是Java应用,没必要用花里胡哨的JVM技术来解决这种问题;Jenkins版本迭代也比较快,定期的维护升级可以保证稳定和高效运行

这是一条签名的小尾巴: 任何变化都不是突然发生的,都是自己无意间一点一点选择的。
原文地址:https://www.cnblogs.com/novwind/p/15225543.html