和 .project 檔案說“再見”—— VS Code Java 1.1.0 背後的故事

微軟技術棧發表於2021-12-11

Language Support for Java 1.1.0 版本包含了一項重要更新:現在外掛在匯入新的 Java 專案時,專案後設資料檔案(.project,.classpath,settings等)預設將不再生成於專案路徑下。這一問題自2018年被記錄至今已有超過三年的時間。本文旨在記錄並分享我們解決這一問題的過程和最後的解決方案。

懸在頭頂的“達摩克利斯之劍”

隨著 VS Code Java 的功能逐漸豐富,使用者數量也在穩步上升。但是由於 Java 外掛在匯入專案時,會在專案目錄下生成後設資料檔案的問題,我們得到了不少的1星差評。可以預見,隨著使用者基數增加,因這一問題而造成的差評數量也會增加。這就如同一把懸在我們頭頂的“達摩克利斯之劍”,如果不及時解決,問題隨時都有可能爆發。

其實這並不是我們產品組不想徹底修復這一問題,根本原因需要從 Java 語言服務的架構說起:

1510adb95d5faa855f48633cc94f6294.jpg
JDT Java Language Server 架構示意圖

VS Code Java 專案背後所採用的 Java 語言服務的正式專案名稱是 Eclipse JDT Language Server™,由微軟和紅帽聯手開發。在上面的專案架構圖中可以看到,我們在實現中複用了 Eclipse 的一些模組,而這些自動生成的後設資料檔案也正是由其中一些上游模組所產生。在 Eclipse 的討論區中可以找到一條相關的討論帖子。這條帖子的建立時間甚至可以追溯到2004年。由於在實現時,這些後設資料檔案的路徑就已經作為常量被硬編碼在了程式碼裡,這些常量又被各個不同的 Eclipse 模組甚至是外掛引用,經年累月下來這一問題從某種意義上已經成為了“歷史包袱”。

考慮到改變上游模組的行為包含了太多的未知和不確定性,在過去我們嘗試給使用者提供一些變通方法,比如讓這些後設資料檔案在 VS Code 的檔案瀏覽器中隱藏,並引導使用者將他們新增至 .gitignore 當中。但從使用者的反饋來看,這些方式並沒有讓使用者感到滿意。為了能夠徹底解決這個已經困擾了我們以及使用者三年多之久的“頑疾”,我們在今年下半年決定再做一次嘗試,希望能將其“根治”。

方案一、使用 Symbolic Link(失敗)

我們最先想到的方法是使用 Symbolic Link。在匯入專案時,可以將被匯入的專案通過 Symbolic Link 的方式連結到一個使用者看不到的地方,從而讓後設資料檔案生成在連結後的路徑下。但很快這一方案就遇到了問題——在某些作業系統下建立 Symbolic Link 需要特定的許可權,否則會丟擲 FileSystemException,這顯然不是我們想要的效果,因此這個方案馬上被否決了。

方案二、使用 Eclipse Linked Resources(放棄)

和 Symbolic Link 的思路類似地,我們還可以選擇使用 Eclipse Linked Resources:

Linked Resources: Linked resources are files and folders that are stored in locations in the file system outside of the project's location.

上文是 Linked Resources 的一段官方定義,它可以作為專案的一部分,但又允許儲存在專案路徑之外的其他位置。在 VS Code Java 中,我們對於 Unmanaged Folder(無構建系統的專案),就是通過 Linked Resources 機制將這些後設資料檔案隱藏的,它的實現原理如下圖所示:

488365ba19ac6197bd957b3615c0741f.jpg
Unmanaged Folder 實現原理

可以看到專案的實際路徑放在了 Language Server workspace storage 中,使用者通常並不知曉這一路徑,同時在 .project 檔案裡我們定義了 Linked Resources 的目標路徑,也就是使用者在 VS Code 開啟的資料夾位置,它作為專案的一部分,會像其他專案一樣參與到構建過程當中,其開發體驗是類似的。

相同的原理可以應用到 Maven 專案和 Gradle 專案的匯入過程當中來解決這一問題,因此,我們在 M2E 模組上進行了一些實驗。M2E 模組在 Java 語言服務中負責 Maven 專案的匯入,通過改動模組中的相關程式碼,並利用 Linked Resources 機制就可以將後設資料檔案生成到專案路徑之外的地方。

最終的實驗結果是可行的,但是這套方案的缺點也非常明顯:

  • 改動較大:需要改動的程式碼散落在整個模組的不同檔案中(大約十幾處),同時因為程式碼規模較大,沒有辦法在短時間內確定這些改動是否是完備的。
  • 對下游模組不透明:因為多了一層 Linked Folder,這會讓 Java 專案檢視在展示專案結構時,多出一層代表了 Linked Folder 的目錄結構。在 Java 專案檢視的實現中需要增加一些額外的控制邏輯,讓專案結構的展示和正常專案一樣。
  • 可行性未知:對於 Maven 和 Gradle 構建系統的支援模組 M2E 和 Buildship 都是上游專案,這一概念能否被採納接受是個未知數。
  • 擴充套件性差:如果要支援一套新的構建系統,需要將類似的邏輯再實現一遍。

考慮到上述原因,團隊在經過討論之後決定暫時放棄 Eclipse Linked Resources 方案,並繼續尋找更優的解決辦法。

發現“銀彈

放棄第二套方案還有另一個原因:Eclipse 自發布至今二十載,在保證穩定執行的同時,可以不斷地增加新的功能且提供了出色的擴充能力,在這背後一定蘊含了優秀的架構設計和可擴充性。直覺上讓我們覺得應該還會有更優雅的解決辦法。

因此,這一次我們直接從 Eclipse 底層檔案系統入手分析,並最終發現了一枚解決問題的“銀彈”:File System Provider 和 FileStore(注:雖然在軟體工程領域,人們的共識是沒有銀彈,不過對於這一特定的問題,我們確實找到了一種比較“奇巧”的解決辦法)。

Eclipse 工作空間結構與 FileStore

Eclipse 在執行過程中會為整個工作空間維護一顆樹形結構,樹的節點代表了檔案系統中的檔案或目錄,同時還儲存了檔案的一些重要資訊,如修改時間等。

Eclipse 底層通過 FileStore 類將這些節點和檔案系統中的檔案進行關聯。FileStore 類還有一個重要特性:如果對映的物件是單個檔案,那麼 FileStore 還會負責提供這一檔案的輸入輸出流。

這一特性為問題的解決帶來了非常重要的思路:只要能夠將後設資料檔案的輸入輸出流重定向到專案目錄之外的位置,問題也許就能得以解決。帶著這個假設,我們又發現了另一個關鍵線索:File System Provider。

方案三、File System Provider

File System Provider 是 Eclipse 平臺對外開放的一個擴充套件點,它允許開發人員實現一個 Eclipse 檔案系統介面(org.eclipse.core.filesystem.IFileSystem),並將其註冊到擴充套件點上,用以處理具有特定 URI scheme 的檔案請求。

於是我們從 File System Provider 這一擴充點入手,繼承並覆蓋了 Eclipse 預設處理 URI scheme 為 file 的檔案系統,通過覆寫其中的一些方法,讓檔案系統在處理後設資料檔案時,將檔案路徑重定向到專案路徑之外的地方進行讀寫。相比於方案二,這一套方案的優點在於:

  • 對其他模組完全透明,基本不需要進行修改就能正常工作,這同時還意味著較好的擴充性
  • 程式碼量很小,最終的實現,算上 JavaDoc 和註釋,一共只有 300 行左右。

當然這個方案也並非完美,因為它要求其他模組通過 Eclipse 提供的 API 進行對後設資料檔案的讀寫操作。我們在實現過程中就發現上游 Buildship 在處理後設資料檔案時直接通過 JDK 中的檔案 I/O API 進行讀寫,為此我們提交了一份變更請求將相關操作遷移到了 Eclipse API 上。

總結

在權衡了利弊之後,我們最終選取了第三套方案並解決了這一困擾了 VS Code Java 使用者三年多時間的問題。雖然最終的實現並不複雜,但探尋答案的過程卻非常具有戲劇性。

最後特別感謝 Eclipse Platform 專案成員 Mickael Istria 以及 Alexander Fedorov。在問題討論的過程中他們給予了非常有用的建議,對問題的解決起到了非常關鍵的作用。

反饋與建議:

請積極使用我們的產品!您的反饋和建議對我們非常重要,並將幫助我們做得更好。有幾種方法可以給我們留下反饋

資源:


歡迎關注微軟中國MSDN訂閱號,獲取更多最新發布!
image.png

相關文章