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基本的初始化过程,例如:
- 设置Jenkins初始密码
- 设置系统插件更新地址为国内镜像源,目前比较好用的有Jenkins中文社区、清华镜像源、华为镜像源等。这里我选择了社区的镜像源地址:https://updates.jenkins-zh.cn/update-center.json
- 设置远程扩展库 (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版本迭代也比较快,定期的维护升级可以保证稳定和高效运行