問題
本地使用maven編譯和運(yùn)行時(shí)一切都正常含蓉,但是通過ci的方式以政,編譯、打包司顿、發(fā)布到部署環(huán)境芒粹,運(yùn)行時(shí)拋出了一條顯而易見的JDK版本的錯(cuò)誤。
錯(cuò)誤是這個(gè)樣子:
java.lang.NoSuchMethodError: java.util.concurrent.ConcurrentHashMap.keySet()
Ljava/util/concurrent/ConcurrentHashMap$KeySetView;
報(bào)的是的NoSuchMethodError: java.util.concurrent.ConcurrentHashMap的錯(cuò)誤大溜。所以不難排查出原因是ci使用了JDK 8來進(jìn)行編譯化漆,導(dǎo)致生成的字節(jié)碼包含了JDK 8更改的新方法keySet(). 其返回值是ConcurrentHashMap$KeySetView這個(gè)JDK8新增內(nèi)部類。
為了進(jìn)一步驗(yàn)證部署服務(wù)器上的class文件都是JDK 8編譯的钦奋,我使用javap這個(gè)JDK自帶的工具做了如下的驗(yàn)證:
javap -v a.class |grep major
返回的結(jié)果是
major version: 51
問題初露端倪座云,51對(duì)應(yīng)的JDK版本號(hào)應(yīng)該是1.7(或者7)疙赠,52才是JDK 8的major版本。這里出現(xiàn)了兩個(gè)疑惑:
- 為什么ci使用JDK 8編譯的class會(huì)是JDK 7的編譯結(jié)果朦拖?
- 既然是JDK 7編譯的class文件圃阳,那為何會(huì)出現(xiàn)JDK 8才有的內(nèi)部類?
先看第一個(gè)疑惑璧帝。之前說到ci也是通過maven compiler plugin進(jìn)行編譯的捍岳,pom.xml中可以配置language level如下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
這實(shí)際對(duì)應(yīng)于javac的-source和-target參數(shù),那么這兩個(gè)參數(shù)具體代表什么呢睬隶?
$ javac -help
-source <release> Provide source compatibility with specified release
-target <release> Generate class files for specific VM version
source參數(shù)指的是源代碼級(jí)別的語法兼容锣夹,而target參數(shù)指的是生成release版本的兼容性的class文件,不過只確保目標(biāo)VM能夠加載class文件苏潜,卻無法保證運(yùn)行時(shí)的正確性银萍。接下來,我們嘗試使用javac加上這些參數(shù)來編譯源碼窖贤。
首先我們寫一段程序砖顷,如下:
// App.java
package com.lambeta;
import java.util.concurrent.ConcurrentHashMap;
public class App {
public static void main(String[] args) {
ConcurrentHashMap map = new ConcurrentHashMap();
map.keySet();
}
}
我本機(jī)的java版本是1.8,直接使用javac來編譯App.java赃梧,結(jié)果如下
$ javac App.java
$ javap -v App.class |grep major
major version: 52
如果指定source和target參數(shù)滤蝠,再用javac編譯App.java
$ java -version
java version "1.8.0_45"
...
$ javac -source 7 -target 7 App.java
warning: [options] bootstrap class path not set in conjunction with -source 1.7
1 warning
$ ls
App.class App.java
這里有個(gè)警告,我們暫時(shí)不看授嘀。先使用javap反編譯App.class物咳,觀察major version以及keySet()這個(gè)方法的返回值。
$ javap -v App.class
...
major version: 51
...
9: invokevirtual #4
// Method java/util/concurrent/ConcurrentHashMap.keySet:()
Ljava/util/concurrent/ConcurrentHashMap$KeySetView;
...
這樣蹄皱,第二個(gè)疑惑也解開了览闰。可以初步得出一個(gè)結(jié)論巷折。
小結(jié)
在javac指定了這些參數(shù)压鉴,降低版本號(hào)來編譯,會(huì)導(dǎo)致生成class文件被標(biāo)識(shí)為較低版本以供指定的JVM加載锻拘。但是油吭,基于JDK 8的bootstrap class編譯而成的keySet()方法,其返回值依舊是JDK 8中ConcurrentHashMap$KeySetView這個(gè)新增內(nèi)部類署拟。運(yùn)行時(shí)婉宰,1.7的JVM嘗試加載這個(gè)class文件,一定找不到KeySetView作為返回值的keySet()方法推穷,出錯(cuò)心包。
解決方式
既然知道錯(cuò)在那里,就比較容易尋找到解決方案了馒铃。
- 編譯期間蟹腾,替換掉bootstrap class
- 使用父類/接口替換子類痕惋,即ConcurrentMap替換ConcurrentHashMap聲明
編譯期間,替換掉bootstrap class
javac編譯時(shí)岭佳,可以指定bootclasspath血巍,來替換默認(rèn)的加載路徑,如下:
javac -bootclasspath /Library/Java/JavaVirtualMachines/jdk1.7.0_60.jdk/Contents/Home/jre/lib/rt.jar \
-source 7 -target 7 App.java
// or
javac -Xbootclasspath:/Library/Java/JavaVirtualMachines/jdk1.7.0_60.jdk/Contents/Home/jre/lib/rt.jar \
-source 7 -target 7 App.java
這時(shí)候珊随,再去看看反編譯的結(jié)果述寡,就會(huì)是這樣:
...
major version: 51
...
9: invokevirtual #4
// Method java/util/concurrent/ConcurrentHashMap.keySet:()Ljava/util/Set;
此時(shí)major是51(JDK 7),而keySet()的返回值也是JDK 7中的java.util.Set類型了叶洞。
使用父類/接口替換子類鲫凶,即ConcurrentMap替換ConcurrentHashMap聲明
上一種方案雖然可行,但是卻不實(shí)用——因?yàn)椴荒芤骳i服務(wù)器上有兩個(gè)不同版本的JDK衩辟,也不能要求在maven構(gòu)建時(shí)傳遞與安裝路徑如此緊耦合的值作為bootclasspath的參數(shù)值螟炫。所以可以采取將具體實(shí)現(xiàn)類的聲明替換成為其接口的方式,如下:
package com.lambeta;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class App {
public static void main(String[] args) {
ConcurrentMap map = new ConcurrentHashMap();
map.keySet();
}
}
這樣編譯好的字節(jié)碼中就不會(huì)有ConcurrentHashMap$KeySetView這樣的返回值類型了艺晴。在JDK 7上運(yùn)行時(shí)昼钻,JVM動(dòng)態(tài)調(diào)用的一定是ConcurrentHashMap的keySet():java.util.Set方法了。
結(jié)論
- 保證編譯封寞、打包環(huán)境和最終部署環(huán)境JDK版本的一致性
- 如果無法保證然评,就盡量面向接口編程,尤其是JDK中提供的類狈究。原因是接口不易改變碗淌,而實(shí)現(xiàn)類遵循“寬收嚴(yán)發(fā)”原則,方法的入?yún)⒑统鰠⒍际且鬃兊摹?/li>