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 版本检测的方法来查看机器 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 出错而无法编译的项目解答中给出了三种解决办法:

<plugins>
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-javadoc-plugin</artifactId>
    <configuration>
      <additionalparam>-Xdoclint:none</additionalparam>
    </configuration>
  </plugin>
</plugins>

内部 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 编译很直接地给出了两种解决方案:

<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>
<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
参考引用


↙↙↙阅读原文可查看相关链接,并与作者交流