踐)
搞Java開(kāi)發(fā)的人誰(shuí)電腦里沒(méi)裝過(guò)兩三個(gè)JDK版本老項(xiàng)目要Java 8新項(xiàng)目已經(jīng)升到Java 17甚至21偶爾還得試一下預(yù)覽版本的新特性。SDKMAN就是這樣一個(gè)命令行工具專門(mén)解決“裝JDK、換JDK、刪JDK”這一整套管理問(wèn)題支持在同一個(gè)系統(tǒng)里并存多個(gè)JDK版本并且隨時(shí)切換。不管你是剛?cè)腴T(mén)的新手還是被多版本折騰過(guò)好幾次的開(kāi)發(fā)者這篇文章能幫你把整套流程理順。我會(huì)直接告訴你怎么裝、怎么用、怎么切順便把我踩過(guò)的坑也一起列出來(lái)。1. 為什么你的電腦需要裝多個(gè)JDK版本1.1 日常開(kāi)發(fā)里的版本分裂場(chǎng)景很多人的第一反應(yīng)是JDK裝一個(gè)最新的不就行了嗎真不行。實(shí)際項(xiàng)目里版本分裂的情況太常見(jiàn)了。比如有些老業(yè)務(wù)系統(tǒng)用的是Java 8那一套生態(tài)依賴庫(kù)里有些組件在JDK 8之后就不再維護(hù)了強(qiáng)行用新版本跑起來(lái)會(huì)出現(xiàn)兼容性問(wèn)題。而新起的微服務(wù)項(xiàng)目一般會(huì)考慮Java 17或者Java 21帶來(lái)的新特性和性能改進(jìn)。兩邊同事協(xié)作時(shí)你手里如果只有一個(gè)最新的JDK切換項(xiàng)目時(shí)就會(huì)頻繁碰到編譯不過(guò)、運(yùn)行報(bào)錯(cuò)、依賴包反射調(diào)用失敗之類(lèi)的問(wèn)題。還有一類(lèi)場(chǎng)景平時(shí)不太容易想到你在本地跑一些中間件或者腳本工具時(shí)它們內(nèi)置的要求往往比項(xiàng)目本身更嚴(yán)格。有些工具只認(rèn)某個(gè)特定版本的JVM版本高了或者低了啟動(dòng)腳本的行為都不一樣。這種時(shí)候多版本共存幾乎是唯一省心的方案。1.2 手動(dòng)管理JDK的痛點(diǎn)沒(méi)有工具之前我見(jiàn)過(guò)不少同事是這樣的從官網(wǎng)下載壓縮包解壓到某個(gè)目錄再打開(kāi)系統(tǒng)環(huán)境變量設(shè)置界面手動(dòng)加JAVA_HOME再把$JAVA_HOME/bin拼到PATH里。這流程聽(tīng)起來(lái)不難真正煩的是切換。今天切到Java 8明天要切到Java 17你得重新打開(kāi)環(huán)境變量配置把JAVA_HOME改成另一個(gè)路徑然后保存、重開(kāi)終端運(yùn)行java -version確認(rèn)。改一次還行一天改好幾次就會(huì)特別煩躁而且萬(wàn)一有幾個(gè)終端窗口沒(méi)重開(kāi)內(nèi)存里的環(huán)境變量還是舊的還會(huì)產(chǎn)生“明明切換了但沒(méi)生效”的錯(cuò)覺(jué)。手動(dòng)解壓的方式還有一個(gè)隱藏問(wèn)題JDK版本信息是分散的。你下載了好幾個(gè)版本的壓縮包時(shí)間一長(zhǎng)你自己都分不清哪個(gè)目錄對(duì)應(yīng)哪個(gè)版本也不敢亂刪怕刪了某個(gè)正在跑的項(xiàng)目就崩了。系統(tǒng)的包管理器雖然能裝但版本更新通常滯后而且它會(huì)把JDK和軟件倉(cāng)庫(kù)的其他組件混在一起很多時(shí)候并不適合精確控制項(xiàng)目的運(yùn)行時(shí)。所以我之后換成專用的版本管理工具整個(gè)流程就清爽多了。2. 先弄懂SDKMAN是什么2.1 包管理器之外的另一個(gè)身份SDKMAN全稱是Software Development Kit Manager很多人把它當(dāng)成包管理器其實(shí)它的主要身份是“版本管理工具”。它不直接參與編譯你的代碼也不會(huì)把某個(gè)SDK“裝進(jìn)系統(tǒng)里”讓你在系統(tǒng)設(shè)置中看到它而是負(fù)責(zé)在你的用戶目錄下維護(hù)一套“候選版本”列表并且通過(guò)符號(hào)鏈接的方式隨時(shí)切換當(dāng)前生效的版本。這套思路和Node生態(tài)里的nvm、Python生態(tài)里的pyenv非常相似。如果你熟悉其中任何一個(gè)工具理解SDKMAN會(huì)很快。它會(huì)把自己的目錄放在~/.sdkman/candidates/java/下面每個(gè)已安裝的JDK對(duì)應(yīng)一個(gè)子目錄比如17.0.12-tem、21.0.3-tem。同時(shí)會(huì)有一個(gè)current符號(hào)鏈接指向當(dāng)前正在使用的版本。你的JAVA_HOME只需要指向~/.sdkman/candidates/java/current切換版本時(shí)SDKMAN會(huì)修改這個(gè)鏈接的指向其他所有配置都不用動(dòng)。2.2 為什么選它而不是去官網(wǎng)下載官網(wǎng)下載本身沒(méi)什么問(wèn)題但那種方式只適合你一次性裝好、再也不換版本的情況。只要你有兩個(gè)以上版本的需求手工管理方式的效率低到可以忽略。SDKMAN相比其他管理方式有幾點(diǎn)很實(shí)用。第一它維護(hù)的候選版本列表非常全不僅僅是原始JDK發(fā)行版還包括各種OpenJDK發(fā)行版比如Temurin、Zulu、Liberica等。第二所有操作都在用戶目錄里完成不需要sudo權(quán)限也不會(huì)污染系統(tǒng)級(jí)別的環(huán)境變量。第三它統(tǒng)一了安裝、卸載、切換的入口命令風(fēng)格非常一致學(xué)一次就能用在Java之外的SDK上比如Maven、Gradle、Spring Boot CLI等也都支持管理。還有一點(diǎn)對(duì)我個(gè)人來(lái)說(shuō)很省心它可以幫你記錄當(dāng)前安裝了哪些版本、每個(gè)版本對(duì)應(yīng)哪個(gè)項(xiàng)目。你不需要再維護(hù)一份文本清單來(lái)記錄誰(shuí)是誰(shuí)了。2.3 運(yùn)行環(huán)境的前置條件SDKMAN本身是用Shell腳本寫(xiě)的所以在Unix-like系統(tǒng)上運(yùn)行最順暢常見(jiàn)的Linux發(fā)行版、macOS都支持。你需要的基本上是一個(gè)能跑bash和curl的終端環(huán)境Windows下可以通過(guò)WSL或者Git Bash之類(lèi)的環(huán)境來(lái)用。安裝之前最好先確認(rèn)一下curl命令可用方法是在終端里執(zhí)行curl --version如果提示找不到命令先補(bǔ)上curl再繼續(xù)。3. SDKMAN的安裝與基礎(chǔ)配置3.1 安裝腳本的完整執(zhí)行過(guò)程官方給的安裝命令是一行比較長(zhǎng)的管道命令curl -s https://get.sdkman.io | bash執(zhí)行后腳本會(huì)在你的用戶目錄下創(chuàng)建一個(gè).sdkman目錄把運(yùn)行需要的腳本、配置、候選版本目錄結(jié)構(gòu)都準(zhǔn)備好。同時(shí)它還會(huì)自動(dòng)往你的shell配置文件中追加幾行目的是在每次開(kāi)啟新的終端窗口時(shí)自動(dòng)加載SDKMAN環(huán)境。如果你用的是bash它改的是~/.bashrc如果是zsh則改~/.zshrc。裝完之后立刻在當(dāng)前終端窗口內(nèi)執(zhí)行source $HOME/.sdkman/bin/sdkman-init.sh然后執(zhí)行sdk version如果能正常輸出版本號(hào)就說(shuō)明安裝成功了。下次打開(kāi)新的終端時(shí)就不需要再手動(dòng)執(zhí)行這條初始化命令了因?yàn)樗呀?jīng)被寫(xiě)進(jìn)了shell配置文件里。3.2 初始化與驗(yàn)證我建議安裝完以后先確認(rèn)三件事一是sdk version能正常打印版本號(hào)二是echo $SDKMAN_DIR能輸出路徑三是which sdk能找到可執(zhí)行命令。如果前兩步都正常但第三步提示找不到命令多半是當(dāng)前shell環(huán)境沒(méi)有重新加載配置導(dǎo)致的。對(duì)于zsh用戶如果sdk version在交互終端中能用但在腳本中調(diào)用失敗檢查一下.zshrc里加載SDKMAN的幾行代碼的位置是否太靠前。有個(gè)比較常見(jiàn)的問(wèn)題是用戶把a(bǔ)lias相關(guān)的代碼寫(xiě)在了SDKMAN初始化之前導(dǎo)致某些場(chǎng)景下初始化沒(méi)有正常執(zhí)行。這時(shí)候把init相關(guān)代碼調(diào)整到文件靠前位置基本就能解決。3.3 常見(jiàn)安裝報(bào)錯(cuò)排查安裝過(guò)程中最容易出的問(wèn)題就是下載腳本超時(shí)。如果curl執(zhí)行到一半卡住或者報(bào)連接失敗的錯(cuò)可以先檢查網(wǎng)絡(luò)環(huán)境是否正常然后嘗試把get.sdkman.io換成社區(qū)提供的鏡像源或者設(shè)置系統(tǒng)級(jí)的環(huán)境變量讓curl走可用的通道。這里不展開(kāi)細(xì)節(jié)但有一點(diǎn)要重點(diǎn)提示注意官方安裝腳本本質(zhì)是從遠(yuǎn)程下載并直接執(zhí)行因此在運(yùn)行之前最好先把它下載到本地快速瀏覽一遍腳本內(nèi)容確認(rèn)沒(méi)有問(wèn)題后再執(zhí)行。謹(jǐn)慎一點(diǎn)不是壞事。如果之前裝過(guò)SDKMAN但中途失敗重裝前建議先把~/.sdkman目錄和shell配置文件里相關(guān)的一小段配置清理掉否則可能出現(xiàn)兩個(gè)版本的初始化代碼互相干擾的情況。4. 用SDKMAN安裝JDK版本4.1 先列出可選版本安裝之前先看看官方倉(cāng)庫(kù)里目前有哪些版本可用執(zhí)行sdk list java輸出信息很豐富。每行會(huì)給出一個(gè)版本標(biāo)識(shí)符identifier比如17.0.12-tem、21.0.3-tem后面會(huì)標(biāo)明它屬于哪個(gè)發(fā)行版比如Temurin、Zulu、Liberica、Oracle等還會(huì)標(biāo)出當(dāng)前安裝狀態(tài)、已安裝和本地版本的區(qū)別。剛接觸這個(gè)命令的人容易對(duì)著屏幕發(fā)懵其實(shí)需要注意的只有四列標(biāo)識(shí)符、發(fā)行版、狀態(tài)、使用類(lèi)型。在這之前還有必要了解發(fā)行版之間的差異。Temurin是Eclipse社區(qū)維護(hù)的OpenJDK發(fā)行版穩(wěn)定且免費(fèi)適合大多數(shù)后端項(xiàng)目Zulu對(duì)多平臺(tái)的支持比較全移動(dòng)端和一些嵌入式場(chǎng)景經(jīng)常用到Liberica在桌面開(kāi)發(fā)和特殊架構(gòu)上的支持做得不錯(cuò)Oracle發(fā)行版則更多面向需要商業(yè)支持的場(chǎng)景。不同發(fā)行版之間整體API幾乎一致只是補(bǔ)丁節(jié)奏和支持策略不同。多數(shù)情況下選Temurin就能順利跑完整個(gè)項(xiàng)目周期。4.2 安裝指定發(fā)行版選好版本后直接輸入sdk install java 17.0.12-tem這個(gè)時(shí)候SDKMAN會(huì)開(kāi)始下載對(duì)應(yīng)的壓縮包自動(dòng)解壓到~/.sdkman/candidates/java/17.0.12-tem目錄下。下載速度取決于網(wǎng)絡(luò)環(huán)境和所選發(fā)行版一般幾分鐘內(nèi)能完成。安裝完成后它還會(huì)在終端里問(wèn)你是否把這個(gè)版本設(shè)為默認(rèn)版本輸入Y表示確認(rèn)輸入n表示僅安裝但不切換默認(rèn)值。如果你需要好幾個(gè)版本就可以反復(fù)執(zhí)行sdk install java 8.0.432-tem sdk install java 21.0.3-tem裝完整理后的目錄大致是這樣~/.sdkman/candidates/java/ ├── 8.0.432-tem ├── 17.0.12-tem ├── 21.0.3-tem └── current - 17.0.12-tem4.3 安裝到指定路徑的原理說(shuō)明如果你愿意也可以通過(guò)環(huán)境變量控制安裝目錄的頂層路徑。SDKMAN默認(rèn)把一切放在~/.sdkman下但這個(gè)路徑不是寫(xiě)死的靠的是SDKMAN_DIR環(huán)境變量。在安裝之前設(shè)置這個(gè)變量就能把整個(gè)SDKMAN的數(shù)據(jù)目錄挪到其他磁盤(pán)分區(qū)或者目錄。有些公司會(huì)有統(tǒng)一的開(kāi)發(fā)目錄規(guī)范利用這個(gè)變量可以避免把個(gè)人數(shù)據(jù)放在默認(rèn)目錄下。不過(guò)我不太建議普通用戶做這種自定義。因?yàn)楹芏喙ぞ吆虸DE默認(rèn)會(huì)搜索~/.sdkman這個(gè)路徑改了以后你需要額外告訴每個(gè)工具SDKMAN的位置反而多了一步配置。5. 多版本切換的幾種姿勢(shì)5.1 全局切換和當(dāng)前窗口臨時(shí)切換default和use是兩個(gè)容易混淆的命令但區(qū)別很清晰。default是持久化操作會(huì)讓某個(gè)版本成為以后每次打開(kāi)終端時(shí)的默認(rèn)版本修改的是~/.sdkman/etc/config中的配置。use則只管當(dāng)前這個(gè)終端窗口窗口關(guān)閉后也就失效了。sdk default java 21.0.3-tem sdk use java 17.0.12-tem實(shí)際使用中我會(huì)按照項(xiàng)目性質(zhì)來(lái)決定用哪個(gè)。長(zhǎng)期在維護(hù)的老項(xiàng)目就把默認(rèn)版本設(shè)置成Java 8臨時(shí)要測(cè)試一個(gè)新框架就用use在當(dāng)前窗口切換到Java 17測(cè)試完重開(kāi)一個(gè)終端就回到了默認(rèn)版本。5.2 按目錄自動(dòng)切換這個(gè)功能是SDKMAN里非常值得推薦的一個(gè)特性——.sdkmanrc。思路很簡(jiǎn)單在每個(gè)項(xiàng)目目錄下放一個(gè).sdkmanrc文件文件內(nèi)容指定這個(gè)項(xiàng)目要用哪個(gè)SDK版本。進(jìn)入目錄后執(zhí)行sdk envSDKMAN會(huì)讀取當(dāng)前目錄和上級(jí)目錄中最近的.sdkmanrc文件并按照文件內(nèi)容自動(dòng)切換版本。文件內(nèi)容很簡(jiǎn)單比如java8.0.432-tem如果把這一行寫(xiě)進(jìn)某個(gè)老項(xiàng)目根目錄的.sdkmanrc文件之后進(jìn)入這個(gè)目錄執(zhí)行sdk envJDK就會(huì)自動(dòng)切到Java 8。同理在新項(xiàng)目目錄里再寫(xiě)一行java21.0.3-tem切目錄時(shí)就能大幅減少手動(dòng)切換的工作量。需要注意sdk env只作用于當(dāng)前終端會(huì)話。如果你開(kāi)了多個(gè)終端窗口每個(gè)窗口都要單獨(dú)執(zhí)行一次不會(huì)自動(dòng)跨窗口生效。5.3 設(shè)置默認(rèn)版本默認(rèn)版本的設(shè)置非常值得單獨(dú)說(shuō)一下。我們經(jīng)常遇到“明明已經(jīng)切換了但刷新環(huán)境后版本又變了”的情況。這種時(shí)候一般在重構(gòu)某個(gè)老項(xiàng)目切回默認(rèn)版本再切到目標(biāo)版本時(shí)會(huì)發(fā)現(xiàn)java -version仍然顯示舊的。原因往往是IDE保留了之前的JAVA_HOME緩存或者終端會(huì)話是從舊環(huán)境繼承過(guò)來(lái)的。用default切換后重開(kāi)終端是保證生效最實(shí)在的辦法。如果不想重開(kāi)就重新執(zhí)行source $HOME/.sdkman/bin/sdkman-init.sh再驗(yàn)證。記住JAVA_HOME看著是current路徑但進(jìn)程里的環(huán)境變量是啟動(dòng)那一刻快照出來(lái)的不是實(shí)時(shí)動(dòng)態(tài)變化。6. 常用命令速查與腳本化技巧6.1 常用命令速查我整理了一份使用頻率最高的命令表方便復(fù)制到自己的筆記里。這里不包含全部功能只保留了日常能真正用到的部分。命令作用sdk list java查看所有可安裝的JDK版本sdk install java 17.0.12-tem安裝指定版本sdk uninstall java 17.0.12-tem卸載指定版本sdk use java 17.0.12-tem當(dāng)前窗口切換到指定版本sdk default java 17.0.12-tem全局默認(rèn)切換到指定版本sdk current查看當(dāng)前所有SDK版本的狀態(tài)sdk upgrade java升級(jí)Java相關(guān)的SDK包sdk offline進(jìn)入離線模式只使用本地已安裝版本sdk current這個(gè)命令經(jīng)常被低估。執(zhí)行以后能一次性看到Java、Maven、Gradle等所有由SDKMAN管理的候選版本當(dāng)前狀態(tài)比挨個(gè)java -version和mvn -version直觀得多。6.2 在CI或腳本環(huán)境中使用SDKMAN不只是給本地終端用的在服務(wù)器、構(gòu)建機(jī)上同樣能發(fā)揮作用。打包腳本或者持續(xù)集成流程里可以先通過(guò)source加載初始化腳本再安裝指定版本然后設(shè)置默認(rèn)值一次性把環(huán)境準(zhǔn)備到位。比如export SDKMAN_DIR$HOME/.sdkman source $HOME/.sdkman/bin/sdkman-init.sh sdk install java 17.0.12-tem sdk default java 17.0.12-tem這里有一個(gè)小技巧在腳本里反復(fù)執(zhí)行sdk install java時(shí)如果版本已經(jīng)安裝過(guò)了它會(huì)顯示“already installed”并直接跳過(guò)后續(xù)操作不會(huì)重復(fù)下載所以即使腳本被多次執(zhí)行代價(jià)也很低。如果需要完全清除后重裝可以先rm -rf ~/.sdkman/candidates/java/目標(biāo)版本再執(zhí)行安裝命令。6.3 離線模式的使用場(chǎng)景我有時(shí)會(huì)遇到構(gòu)建機(jī)網(wǎng)絡(luò)受限的情況此時(shí)SDKMAN無(wú)法訪問(wèn)遠(yuǎn)程倉(cāng)庫(kù)列表但本機(jī)上其實(shí)已經(jīng)裝好了需要的版本。執(zhí)行sdk offline進(jìn)入離線模式后安裝、切換、卸載都只基于本地緩存不會(huì)嘗試連接遠(yuǎn)程的版本列表。這個(gè)模式在隔離環(huán)境里特別有用。退出離線模式執(zhí)行sdk online即可操作很輕量。7. 踩坑記錄與避坑指南7.1 踩坑一版本列表拉不下來(lái)或超時(shí)表現(xiàn)是執(zhí)行sdk list java后長(zhǎng)時(shí)間沒(méi)有輸出或者提示Could not resolve host之類(lèi)的錯(cuò)誤。常見(jiàn)原因除了網(wǎng)絡(luò)不通還有可能是CDN節(jié)點(diǎn)響應(yīng)過(guò)慢。解決辦法是從官方發(fā)布頁(yè)獲取鏡像源信息修改~/.sdkman/etc/config里的sdkman_webservice配置換成可用的鏡像地址然后執(zhí)行sdk update刷新列表。如果只是臨時(shí)需要本地已有版本直接進(jìn)離線模式用就好。7.2 踩坑二IDE不識(shí)別SDKMAN路徑這個(gè)問(wèn)題常在配置開(kāi)發(fā)環(huán)境時(shí)出現(xiàn)。你在終端里java -version一切正常但I(xiàn)DE里項(xiàng)目SDK卻顯示無(wú)效路徑。原因是IDE啟動(dòng)時(shí)是從桌面圖標(biāo)或啟動(dòng)器讀取環(huán)境的它不一定繼承了終端里source過(guò)的SDKMAN配置。解決辦法有三個(gè)一是從終端直接啟動(dòng)IDE讓IDE繼承shell環(huán)境二是在IDE里手動(dòng)添加JDK路徑到~/.sdkman/candidates/java/特定版本三是把JAVA_HOME明確指向~/.sdkman/candidates/java/current并寫(xiě)進(jìn)IDE的環(huán)境變量配置里。我個(gè)人比較喜歡第三種因?yàn)橐院笄袚Q版本不用再回IDE改路徑。7.3 踩坑三切換版本后JAVA_HOME沒(méi)有變化有時(shí)候當(dāng)前目錄下執(zhí)行java -version顯示是Java 8但echo $JAVA_HOME還是指向Java 17的current你會(huì)覺(jué)得SDKMAN沒(méi)有生效。多數(shù)情況下是因?yàn)楫?dāng)前shell加載SDKMAN初始化時(shí)JAVA_HOME被設(shè)置過(guò)一次靜態(tài)路徑而你手動(dòng)用export JAVA_HOME...覆蓋過(guò)它。SDKMAN在use和default切換時(shí)會(huì)重新生成JAVA_HOME但如果之前手動(dòng)導(dǎo)出了固定路徑就可能會(huì)覆蓋掉SDKMAN的動(dòng)態(tài)設(shè)置。解決思路很簡(jiǎn)單別在~/.bashrc或項(xiàng)目腳本里手動(dòng)寫(xiě)死JAVA_HOME的絕對(duì)路徑讓SDKMAN來(lái)管理。如果非要在腳本里指定那就在設(shè)置完JAVA_HOME之后重新source一次初始化腳本把current鏈接的路徑重新掛到環(huán)境變量上。7.4 踩坑四升級(jí)SDKMAN后本地版本丟失SDKMAN本身升級(jí)是一個(gè)常見(jiàn)的動(dòng)作執(zhí)行sdk selfupdate后極少數(shù)情況下會(huì)把candidates/java/目錄下的部分內(nèi)容重置好在已安裝的JDK包并不會(huì)被刪除只是current鏈接恢復(fù)到了默認(rèn)。這種情況直接在sdk list java里把之前安裝過(guò)的版本標(biāo)記為已安裝再重新default一次就可以了。真正需要上心的是不要手動(dòng)去改~/.sdkman/candidates/java/目錄下的文件結(jié)構(gòu)比如往里面塞手動(dòng)解壓的文件夾否則很可能讓SDKMAN的版本檢測(cè)混亂。這么多年用下來(lái)我最深的一個(gè)體會(huì)是版本管理工具的價(jià)值不在于“能裝多個(gè)版本”而在于“切換成本足夠低”。當(dāng)“切一下環(huán)境”的成本從十分鐘變成一秒鐘你就會(huì)愿意為每個(gè)項(xiàng)目都準(zhǔn)備一個(gè)獨(dú)立且干凈的運(yùn)行環(huán)境而不是將所有項(xiàng)目都硬塞到同一個(gè)JDK上。如果你目前還在手動(dòng)解壓、手動(dòng)改PATH的階段我建議你花十分鐘把SDKMAN跑起來(lái)先把兩個(gè)版本裝好、切換試一遍然后配合.sdkmanrc給項(xiàng)目做自動(dòng)環(huán)境綁定。這個(gè)小習(xí)慣能讓你在同時(shí)維護(hù)多個(gè)項(xiàng)目時(shí)把糾結(jié)環(huán)境變量的精力省下來(lái)好好聚焦代碼本身。