持续集成 踩坑记录---jenkins 升级至 2.110 带来的编译问题

林小五 · March 30, 2018 · Last by pengshenshen replied at April 08, 2018 · 6668 hits

jenkins升级至高版本引发的编译问题

背景:jenkins版本升级

近期公司内部使用jenkins进行了升级,设置中也增加了JDK8的环境选项

问题1:无法正常编译的job

使用jdk8 来lanch maven,执行job构建时,发现部分job无法正常通过编译,报错为:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-javadoc-plugin:2.9:jar (attach-javadocs) on project wycds-console: MavenReportException: Error while creating archive:

初步排查是jdk8中不符合doclint规范的javadoc无法正常生成。

将job设置中jdk切换成低版本jdk7后继续构建发现以下异常:

Exception in thread "main" java.lang.UnsupportedClassVersionError: hudson/remoting/Launcher : Unsupported major.minor version 52.0
at java.lang.ClassLoader.defineClass1(Native Method)

从报错上看,应该是jenkins需要jdk8的支持。在jenkins官方文档jenkins maven构建中也找到了jenkins 中lauch maven 的jdk最低版本要求:

  • Jenkins >= 1.520 requires Java 6 thus Maven jobs must be launched with Java >= 6.
  • Jenkins >= 1.612 requires Java 7 thus Maven jobs must be launched with Java >= 7.
  • Jenkins >= 2.54 requires Java 8 thus Maven jobs must be launched with Java >= 8.

使用博客服务器上运行中的Jenkins版本检测的方法来查看机器jenkins版本情况

unzip -c jenkins.war META-INF/MANIFEST.MF | egrep ^Jenkins-Version: | awk '{print $2}' | tr -d '\r'
2.110

显然,本机运行jenkins是需要jdk8支持的。所以这个问题不能简单粗暴的通过降低jdk版本来解决。

解决

转向搜索如何禁用jdk8 doclint的相关问题,stackoverflow的问题因javac出错而无法编译的项目解答中给出了三种解决办法:

  • 修复javadoc,使其符合doclint的规范
  • 跳过严格的doclint检查,具体方案在项目pom文件的maven-javadoc-plugin插件中添加以下配置:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</plugin>
</plugins>
  • 跳过javadoc的编译 mvn -Dmaven.javadoc.skip=true

内部jenkins主要是测试部署和CI集成使用,线上环境均使用jdk7运行,为减少对源码的修改,这里选择第三种方案,即修改job build pom.xml的goals设置为:

mvn -clean package -Dmvn.javadoc.compiler.skip

设置成功后,重新构建job,项目可以使用jdk8 正常构建。

问题2:高编译低运行的异常

在测试环境B上执行相关测试时,client连接到server后未收到任何响应,客户端长时间处于等待状态。打开debug日志,发现相关异常信息

java.lang.NoSuchMethodError: java.util.concurrent.ConcurrentHashMap.keySet()Ljava/util/concurrent/ConcurrentHashMap$KeySetView;
at xxx.xxx.xxx.xxx.xx.utils.ConcurrentHashSet.iterator(ConcurrentHashSet.java:33) ~[jar-name.jar:na]

NoSuchMethodError报错信息比较异常,因为项目正常编译通过了,而且本地debug时,能正常执行。
google 搜索,发现简书博客Java高编译低运行错误记录了相同的异常信息,结论为jdk8高版本编译,低版本执行的问题。

对应到本项目,该项目对应的job在jenkins升级之后执行过部署操作,使用的jdk 确为JDK8,导致基于JDK 8的bootstrap class编译而成的keySet()方法,其返回值是JDK 8中ConcurrentHashMap$KeySetView这个新增内部类,在jdk7上执行时加载不到该新方法而抛出异常,而开发代码中未捕到该异常并做异常处理,导致服务端逻辑无法走到response client这一步,又client端超时时间设置比较长,所以观察到client一直处于等待状态,服务端一直未响应。

所以,导致异常现象出现的原因其实有两个,第一是高版本编译低版本运行的问题,第二是开发代码异常处理不到位。第一个问题是根源,所以本文关注第一个问题。

值得关注的一点是,其实项目中maven compiler 中指定了source 和 target的编译级别都是jdk7的,但正如博客中所说,source参数指的是源代码级别的语法兼容,而target参数指的是生成release版本的兼容性的class文件,降低版本号来编译,会导致生成class文件被标识为较低版本以供指定的JVM加载,但基于bootstrap class 编译的class文件依然是基于默认jdk的,无法保证运行时的正确性。
这点在apache maven官网文档设置source和target编译级别 中有详细说明。

解决

在博客Java高编译低运行错误中,作者给出了两种解决办法:

  1. 在编译期,使用javac 来指定bootstrap class的路径
  2. 修改代码,使用父类/接口替换子类,即ConcurrentMap替换ConcurrentHashMap声明

第二种方案对代码有侵入,直接放弃掉。第一种方法对单个类的示例操作,不能直接使用于maven compiler项目中。于是还是转向官网寻找解决办法。

还好,apache maven 官网文档使用不同jdk编译很直接地给出了两种解决方案:

  • maven 底层其实也是调用的javac 来执行编译的,故可指定maven-compiler执行时所使用的javac版本,示例如下:
<project>
[...]
<build>
[...]
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<verbose>true</verbose>
<fork>true</fork>
<executable><!-- path-to-javac --></executable>
<compilerVersion>1.3</compilerVersion>
</configuration>
</plugin>
</plugins>
[...]
</build>
[...]
</project>

指定的javac仅作用于maven compiler插件,不作用于其他插件。 javac_path可以写成硬编码,不过建议最好做成可配置项,具体如下:

<executable>${JAVA_1_4_HOME}/bin/javac</executable>

而JAVA_1_4_HOME 这一属性可以配置在maven的setting.xml文件中,或者设置为系统变量。
具体setting.xml文件的设置可以参考如下示例:

<settings>
[...]
<profiles>
[...]
<profile>
<id>compiler</id>
<properties>
<JAVA_1_4_HOME>C:\Program Files\Java\j2sdk1.4.2_09</JAVA_1_4_HOME>
</properties>
</profile>
</profiles>
[...]
<activeProfiles>
<activeProfile>compiler</activeProfile>
</activeProfiles>
</settings>
  • 使用maven toolchains插件。maven构建项目分为以下几步: 使用jdk执行编译(javac) ---->使用jdk来生成javadoc (javadoc) ----> 使用jdk来生成jar的签名信息(jarsigner)。在不使用toolchain的情况下,构建过程中的每个插件都需要一个jdk工具来运行。toolchains则提供了一种方式,以统一管理的方式指定构建过程时需要的jdk版本,而构建运行的jdk独立于运行maven的jdk。 前提是,maven版本需要在2.0.9及其以上,具体使用要求有以下两点:
    1. 在项目工程中依赖maven-toolchains-plugin插件
    2. toolchains.xml 需要在building 机器上。 以使用jdk 1.7为例,pom文件配置如下:
<plugins>
...
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-toolchains-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<goals>
<goal>toolchain</goal>
</goals>
</execution>
</executions>
<configuration>
<toolchains>
<jdk>
<version>1.7</version>
<vendor>sun</vendor>
</jdk>
</toolchains>
</configuration>
</plugin>
...
</plugins>

jdk的具体信息则被放置在toolchains.xml文件中。从maven3.3.1开始,可以使配置项 --global-toolchains file 来执行toolchains.xml文件的位置,但是依然推荐将文件安置在 ${user.home}/.m2路径下。

<?xml version="1.0" encoding="UTF8"?>
<toolchains>
<!-- JDK toolchains -->
<toolchain>
<type>jdk</type>
<provides>
<version>1.7</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>/path/to/jdk/1.7</jdkHome>
</configuration>
</toolchain>
<toolchain>
<type>jdk</type>
<provides>
<version>1.6</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>/path/to/jdk/1.6</jdkHome>
</configuration>
</toolchain>

<!-- other toolchains -->
<toolchain>
<type>netbeans</type>
<provides>
<version>5.5</version>
</provides>
<configuration>
<installDir>/path/to/netbeans/5.5</installDir>
</configuration>
</toolchain>
</toolchains>

个人更倾向方案1。
具体到当前的场景,解决方法为:

在jenkins所在机器上copy一份项目的pom.xml文件,路径为$path/pom.xml, $path为jenkins用户有权限访问路径即可。 在原文件的maven-compiler插件配置中添加如下属性:


<configuration>
<verbose>true</verbose>
<fork>true</fork>
<executable>$jdk7_path/bin/javac</executable>
<compilerVersion>1.7</compilerVersion>
</configuration>

$jdk7_path 为jenkins机器上jdk7的安装路径。

在对应jenkins job设置中添加build 的pre steps,勾选 execute shell。 添加脚本,在项目编译前替换掉pom.xml文件

cp -f $path/pom.xml  ./pom.xml
参考引用
共收到 1 条回复 时间 点赞
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up