Java高編譯低運(yùn)行錯(cuò)誤(ConcurrentHashMap.keySet)

問題

本地使用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>

參考鏈接
[1] Using Java 7 to target much older JVMs

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home...
    光劍書架上的書閱讀 4,178評(píng)論 2 8
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,527評(píng)論 19 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,633評(píng)論 18 399
  • 一:java概述:1,JDK:Java Development Kit,java的開發(fā)和運(yùn)行環(huán)境,java的開發(fā)工...
    ZaneInTheSun閱讀 2,800評(píng)論 0 11
  • 文/陌宇軒 (一) 從前我們把乳名喊出去 叫親情 后來 我們把乳名藏在心里 叫成熟 再后來 我們幾乎不用自己的乳名...
    小哲小詩閱讀 228評(píng)論 0 0

友情鏈接更多精彩內(nèi)容