近期公司内部使用 jenkins 进行了升级,设置中也增加了 JDK8 的环境选项
使用 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>
mvn -Dmaven.javadoc.skip=true
内部 jenkins 主要是测试部署和 CI 集成使用,线上环境均使用 jdk7 运行,为减少对源码的修改,这里选择第三种方案,即修改 job build pom.xml 的 goals 设置为:
mvn -clean package -Dmvn.javadoc.compiler.skip
设置成功后,重新构建 job,项目可以使用 jdk8 正常构建。
在测试环境 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 高编译低运行错误中,作者给出了两种解决办法:
第二种方案对代码有侵入,直接放弃掉。第一种方法对单个类的示例操作,不能直接使用于 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