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)系作者
  • 序言:七十年代末抖锥,一起剝皮案震驚了整個(gè)濱河市亿眠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌磅废,老刑警劉巖纳像,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異拯勉,居然都是意外死亡爹耗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門谜喊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人倦始,你說我怎么就攤上這事斗遏。” “怎么了鞋邑?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵诵次,是天一觀的道長(zhǎng)账蓉。 經(jīng)常有香客問我,道長(zhǎng)逾一,這世上最難降的妖魔是什么铸本? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮遵堵,結(jié)果婚禮上箱玷,老公的妹妹穿的比我還像新娘。我一直安慰自己陌宿,他們只是感情好锡足,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著壳坪,像睡著了一般舶得。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上爽蝴,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天沐批,我揣著相機(jī)與錄音,去河邊找鬼蝎亚。 笑死九孩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的颖对。 我是一名探鬼主播捻撑,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼缤底!你這毒婦竟也來了顾患?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤个唧,失蹤者是張志新(化名)和其女友劉穎江解,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體徙歼,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡犁河,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了魄梯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桨螺。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖酿秸,靈堂內(nèi)的尸體忽然破棺而出灭翔,到底是詐尸還是另有隱情,我是刑警寧澤辣苏,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布肝箱,位于F島的核電站哄褒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏煌张。R本人自食惡果不足惜呐赡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望骏融。 院中可真熱鬧链嘀,春花似錦、人聲如沸绎谦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窃肠。三九已至包个,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冤留,已是汗流浹背碧囊。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纤怒,地道東北人糯而。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像泊窘,于是被迫代替她去往敵國(guó)和親熄驼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

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