你裝好了插件,請 Claude 找出某個 Java 方法的使用位置,它卻又開始搜文字;更麻煩的是,另一個模組明明有呼叫,結果清單卻少了一筆。這篇 Claude Code LSP 排查,帶你分清「插件裝了」「伺服器啟動」「專案匯入」與「查詢可信」四種不同狀態。
本文寫給第一次處理語言伺服器、願意照著終端機操作的開發工具新手。先用白話理解零件,再建立三模組 Java 練習,最後用同名方法、跨模組介面與產生碼逐項核對。概念部分不用 Java 背景;編譯練習需要已備妥的 JDK、Gradle 與本機 Claude Code。
以下依 2026 年 11 月 2 日查閱的官方文件與原始碼整理。AlphaLab 本次建立並檢查了練習檔案,主機缺少可用 Java runtime,未執行 Gradle 編譯或 Claude Code LSP 查詢;文中的驗收目標是依程式結構預先寫好的答案,不是實測成績。
先說結論:可用索引=啟動成功+專案匯入+已知引用驗收
可用索引=啟動成功+專案匯入+已知引用驗收。把它想成圖書館:櫃台開門、書籍入庫、你知道的幾本書都找得到,才能相信這套目錄。看見插件名稱,只等於櫃台掛上招牌。
- 先用
/plugin與錯誤紀錄確認插件和執行檔,再看 JDK;啟動失敗時,等待索引並不能修好它。 - 從包含
settings.gradle的共同根目錄開始,核對子模組與依賴。 - 用已知位置驗收 definition、references、implementation 與 call hierarchy;零筆結果先當待查。
- 產生碼要查處理器、實際檔案與語言伺服器看到的來源目錄,三者分開留證。

一、Claude Code LSP 排查:先分清四個零件
LSP(Language Server Protocol,語言伺服器通訊協定)像櫃台與專業館員之間的共同語言。Claude Code 是提問者,插件是接線設定,jdtls 是懂 Java 的館員,Gradle 專案模型則是書架與借閱關係。少一層,外觀看起來相似,修法卻完全不同。
依 Anthropic 程式碼智慧功能文件,插件與語言伺服器執行檔要分別安裝。官方也明確區分終端機與 cloud session:雲端工作階段不啟動插件語言伺服器。本篇走的是你自己電腦的終端機路線,先確認工作跑在哪裡。
當前官方插件設定把 jdtls 接到 .java;Kotlin 另有 kotlin-lsp。因此先完成純 Java 探針,再另做 Kotlin 及 Java↔Kotlin 查詢,分開記錄結果。不要把 Java 探針通過,延伸成混合語言專案已驗完。整體執行層可搭配 Agent Harness 白話解析理解。
二、先做一個答案已知的三模組練習
直接拿五十萬行專案排錯,像在整座城市找遺失的門牌。先用小型對照組:api 放介面,app 放呼叫者與一個無關的同名方法,processor 在編譯時產生一個類別。下列 Python 3 腳本只建立文字檔;把它存成 make_fixture.py,在新空目錄執行 python3 make_fixture.py。
這是本篇完整練習來源,不需要外部 Java 函式庫。腳本會在目錄已存在時停止,以免覆蓋舊檔。處理器為教學刻意使用 * 接收註解集合並只產生一次固定類別;它不是可直接套到正式專案的通用產生器。Java Processor 規格說明了處理回合與處理器的發現機制。
from pathlib import Path
root = Path('java-lsp-lab')
if root.exists():
raise SystemExit('java-lsp-lab already exists; choose a new empty directory')
files = {
'settings.gradle': '''rootProject.name = 'java-lsp-lab'
include 'api', 'processor', 'app'
''',
'build.gradle': '''subprojects {
apply plugin: 'java-library'
tasks.withType(JavaCompile).configureEach {
options.release = 17
}
}
''',
'app/build.gradle': '''dependencies {
implementation project(':api')
annotationProcessor project(':processor')
}
tasks.named('compileJava') {
options.compilerArgs += ['-processor', 'demo.MakeGreeting']
}
''',
'api/src/main/java/demo/Greeter.java': '''package demo;
public interface Greeter {
String greet();
}
''',
'app/src/main/java/demo/Welcome.java': '''package demo;
public class Welcome implements Greeter {
public String greet() { return "hello"; }
}
''',
'app/src/main/java/demo/Other.java': '''package demo;
public class Other {
public String greet() { return "other"; }
}
''',
'app/src/main/java/demo/Main.java': '''package demo;
public class Main {
public static void main(String[] args) {
Greeter g = new Welcome();
System.out.println(g.greet());
System.out.println(new Other().greet());
System.out.println(GeneratedGreeting.text());
}
}
''',
'processor/src/main/java/demo/MakeGreeting.java': r'''package demo;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
@SupportedAnnotationTypes("*")
public class MakeGreeting extends AbstractProcessor {
private boolean made;
@Override public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override public boolean process(Set<? extends TypeElement> types,
RoundEnvironment round) {
if (!made && !round.processingOver()) {
made = true;
try (Writer w = processingEnv.getFiler()
.createSourceFile("demo.GeneratedGreeting").openWriter()) {
w.write("package demo; public class GeneratedGreeting {"
+ "public static String text() { return \"generated\"; }}");
} catch (IOException e) { throw new RuntimeException(e); }
}
return false;
}
}
''',
'EXPECTED.md': '''# Hand-authored acceptance targets, not measured LSP results
- Main.java g.greet() -> Greeter.greet declaration in api
- Greeter implementations -> Welcome (Other is unrelated)
- Greeter.greet references -> inspect Main.java g.greet() call
- Other.greet must not be treated as a reference to Greeter.greet
- Main.java GeneratedGreeting.text() -> generated class after compile
- main outgoing calls: check Greeter.greet, Other.greet, GeneratedGreeting.text
- Declarations/overrides may be listed separately; compare source identities
''',
'.gitignore': '''.gradle/
'''
}
for name, text in files.items():
p = root / name
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(text, encoding='utf-8')
print('Created java-lsp-lab; Java/Gradle/LSP have not been executed by this script')
在已備妥、與 JDK 相容的 Gradle 環境,進入 java-lsp-lab,執行 gradle wrapper 建立這份練習的 Wrapper。依 Gradle Wrapper 文件,之後用專案內的 Wrapper 啟動;Windows 命令提示字元改用 gradlew.bat。第一次執行可能下載對應 Gradle 發行包。原有正式專案直接用自己的 Wrapper,別為了排查改掉版本。
cd java-lsp-lab
./gradlew --version
./gradlew projects
./gradlew :app:compileJava
rg -n --no-ignore "greet|GeneratedGreeting" api/src app/src app/build/generated
預期結構是 Main 透過 Greeter 型別呼叫 greet();Welcome 實作介面;Other.greet() 同名卻沒有實作關係;GeneratedGreeting.text() 由處理器產生。EXPECTED.md 是手寫驗收目標。先保存編譯輸出及上述行號,若編譯失敗,從錯誤開始修,別先把它算成索引漏查。
三、五步 Claude Code LSP 排查:每一步都要有出口
第 1 步:「接線了嗎?」分開檢查插件與執行檔
痛點是「安裝成功」卻查不到。解法是驗證兩邊:在啟動 Claude 的同一個終端機跑 command -v jdtls;PowerShell 用 Get-Command jdtls。macOS 的官方插件 README 提供 brew install jdtls。這是讀者缺少執行檔時的安裝選項;安裝完仍須重查 PATH。
在 Claude Code 內執行 /plugin install jdtls-lsp@claude-plugins-official,到 /plugin 查看 Installed 與 Errors,並依安裝回覆完成重新載入或重開工作階段。官方排錯文件以 Executable not found in $PATH 與 claude --debug 的伺服器啟動錯誤協助定位。本關出口:知道載入的是哪個插件、要啟動哪個路徑,且有啟動證據。
請在這份練習的檔案中,讓 Claude 暫時引入一個型別錯誤再修好,展開對話的 diagnostics。這遵循官方的啟動檢查方式;只對練習檔做,不去污染正式工作樹。診斷出現代表已有一段通路工作,跨模組是否完整仍留待後面驗收。插件來源檢查可接著讀 Claude Code Plugin 安全驗收。
第 2 步:「館員能上班嗎?」把三種 Java 版本拆開
痛點是把啟動錯誤誤認為索引太慢。解法是記三份版本:啟動 jdtls 的 JDK、執行 Gradle 的 JVM、編譯專案使用的 toolchain/release。先跑 java -version、javac -version、./gradlew --version,再對照啟動器實際選到的 Java;只看殼層的 java 仍可能漏掉啟動器另外選用的路徑。
本次查閱有一個具體落差:插件 README仍寫 Java 17+;JDT LS v1.63.0 文件要求至少 Java 21,而 開發分支 main已寫 Java 25。先辨認你安裝的 JDT LS 版本,再查它對應的要求。不要把其中任何一個數字當成全部版本的答案。
練習用 options.release = 17 限定編譯目標,與語言伺服器啟動需求是不同層。Gradle toolchain 文件進一步區分執行 Gradle 與編譯程式的 Java。本關出口:留下版本、路徑與第一個啟動錯誤;若 jdtls 尚未成功啟動,先處理這一層。
第 3 步:「整套書進來了嗎?」核對根目錄與跨模組依賴
痛點是同檔案查得到、跨模組卻漏掉。解法是對齊專案邊界:先到含 settings.gradle 的 java-lsp-lab 根目錄啟動 claude --debug,用 ./gradlew projects 核對 api、processor、app 三個子專案,再跑 ./gradlew :app:dependencies --configuration compileClasspath,檢查 app 的編譯依賴含 api。
Gradle 多專案文件把 include 與 project dependency 分開:列入同一份建置,和宣告誰使用誰,是兩件事。兩個資料夾都在 Git 裡,並不足以替代這條依賴。本關出口:根目錄一致、子模組齊全、編譯依賴可解。
故障演練只改這份練習:先備份 app/build.gradle,移除 implementation project(':api'),再跑 ./gradlew :app:compileJava。依來源結構,app 將失去介面的編譯依賴;用實際錯誤核對,再還原那一行並重新編譯。這個反例用來確認你檢查的確實是模組邊界,並非用等待掩蓋建置錯誤。
第 4 步:「目錄能用了嗎?」用三個探針判斷就緒
痛點是看到 initialized 或等了一分鐘,就宣布全部完成。解法是讓已知查詢通過。先等待專案匯入/建置進度結束,處理當前診斷,再對同一組位置重查。這裡的「就緒」是本篇的驗收定義:下列目標在你記錄的版本與專案範圍內能被確認,而非保證整座 Repo 的每條關係都已找到。
- 跨模組 definition:從 Main 的
g.greet()查定義,確認能定位 api 的Greeter.greet;再查介面實作,核對Welcome。 - 同名反例:查
Greeter.greet的 references,逐筆讀檔核對 Main 的介面呼叫;把Other.greet當作必須辨開的無關符號。宣告與覆寫是否另列,先記清口徑,不拿總筆數硬比。 - 產生碼 definition:完成編譯後,從 Main 的
GeneratedGreeting.text()查向產生的類別;檔案存在與能被定位要各留一份證據。
官方 LSP 工具說明列有定義、引用、實作與呼叫階層查詢。請 Claude 展開實際工具呼叫與結果,區分它真的用了 LSP,還是用文字搜尋代答。call hierarchy 可再查 Main 的對外呼叫,核對三個自訂方法;介面呼叫與執行時派送要分開描述。保存檔案位置、操作與結果,比一句「找到三筆」更可追查。
本關出口:保存匯入初期與完成後的兩份查詢記錄;尚未取得的結果寫待查,別預填成「修好」。若原始碼探針通過而產生碼失敗,下一步只查產生碼那條路,縮小改動範圍。
第 5 步:「新書真的入庫嗎?」拆開處理器、產物與來源目錄
痛點是 compileJava 成功,Agent 還說類別不存在。解法是三層對帳:先查 ./gradlew :app:dependencies --configuration annotationProcessor,確認 processor 依賴;再查 app/build/generated 下實際的 GeneratedGreeting.java;最後查看語言伺服器的匯入/classpath 記錄,確認它辨識的來源目錄。
annotation processor(註解處理器)像編譯時才開工的印刷機。Gradle Java 插件文件區分編譯與處理器依賴;CompileOptions則有處理器路徑與產生來源輸出目錄。現有專案可能自訂輸出位置,要跟著實際設定找;本練習只示範普通來源檔產生,不把 Lombok 的改寫機制一併算進來。
第二個故障演練:備份後移除 annotationProcessor project(':processor'),執行 ./gradlew :app:clean :app:compileJava。本練習還明確指定 -processor demo.MakeGreeting,因此要檢查的是「處理器找不到」或後續產物缺失的實際錯誤。還原設定,再編譯、重新確認專案匯入,最後重跑同一個產生碼 definition。只在隔離練習清除 app 建置產物。
JDT LS 的 Gradle 匯入實作有獨立的註解處理設定同步路徑。因此建置成功而導航異常時,應分別查建置輸出與匯入模型。這是排查方向,並非判定所有 Gradle 多模組都存在同一個 bug;也不要直接把網路上關閉處理器或平行建置的設定抄進正式專案。
還要加查一層查詢範圍。官方 Claude Code 更新紀錄曾修正 LSP 回傳 Git 忽略檔案的結果;產生檔若在忽略目錄,應核對目前版本的過濾與存取範圍,不能只判為匯入失敗。本練習刻意沒有把 build 加進忽略清單,方便觀察產生碼;正式專案保留原規則,先記錄被排除的是哪個路徑,勿為查詢而開放所有檔案。
四、把排查交給 Agent:要求證據,再決定改哪裡
你可以把下面的工作單貼入練習用 Claude Code session。它的價值是把「查不到」拆成可定位的結果,而非讓模型自由猜一個修法。
只檢查這份 java-lsp-lab,先不要修改設定。
1. 記錄 Claude Code、jdtls、Java 與 Gradle 版本和啟動路徑。
2. 核對根目錄、三個子模組、compileClasspath 與 annotationProcessor。
3. 對照 EXPECTED.md,逐項呼叫 LSP,保存操作、檔案位置和結果。
4. 文字搜尋只能補候選,請明列哪些結果來自 LSP、哪些來自搜尋。
5. 比較匯入初期與完成後的結果,未證實項目標「待查」。
6. 提議一個最小修復,列出重跑哪個編譯與哪個原查詢;等我選定再改。
實際專案先挑一個你能人工確認的跨模組呼叫,再擴充探針。若你已讀過 GitNexus 與 Blast Radius 教學,這裡補的是語言伺服器輸入是否完整;確認這一層後,再用 大型 Repo 安全改版流程處理變更範圍。兩者的驗收問題不同。
五、八個常見問題:別讓零筆結果替你下結論
裝了 jdtls-lsp 就包含 jdtls 嗎?
不包含。官方要求另外安裝語言伺服器執行檔,再由插件接線;先檢查啟動 Claude 的殼層能否找到它。
只要等久一點,跨模組引用就會出現嗎?
不一定。先確認伺服器已啟動、建置依賴可解、匯入沒有卡住,再用同一個探針重查。等待時間本身不是本篇的通過條件。
definition 找到 Welcome 才算成功嗎?
看查詢位置。練習的 g 是 Greeter 型別,先核對介面宣告,再另查 implementations;別把兩種操作混成同一個預期。
references 回傳零筆,能刪掉這個方法嗎?
先保留。對照已知呼叫、實際工具結果與建置範圍,完成驗收後才評估變更;零筆是一份查詢結果,不是刪除理由。
Gradle 編譯通過,LSP 一定完整嗎?
還要另驗。編譯證據確認那次建置,definition/references 證據確認那次導航;在同一份來源與版本下比較。
Java 通過,Kotlin 也算通過嗎?
分開驗。官方另列 Kotlin 插件;混合專案應加上 Kotlin 以及雙向跨語言探針,逐條記錄。
jdtls 用 Java 25,專案也得改成 Java 25 嗎?
先分層。語言伺服器 runtime、Gradle JVM 與編譯目標各自有需求;核對已安裝版本與 toolchain,再決定環境設定。
LSP 好了,還需要文字搜尋嗎?
需要保留為交叉檢查。搜尋適合補出候選與確認文字位置,語意關係再讀程式核對;本篇沒有測量兩者速度,也不替它們排快慢。
新手帶走三件事
- 把「接線、啟動、匯入、查詢」分開記錄;每一層成功的證據只回答它自己的問題。
- 先寫已知答案,再查工具輸出;同名反例與產生碼,比單純多找幾個方法更能揭露邊界。
- 每次只改一項設定,修完重跑同一個編譯和原查詢,保存修復前後的差異。
接著閱讀
左右滑動查看更多推薦
下一步:先替一個跨模組呼叫寫下答案
今天先選一個你知道宣告與呼叫位置的方法,照五步留下版本、編譯與查詢記錄。再加上一個同名反例和一個產生類別,讓「可用索引=啟動成功+專案匯入+已知引用驗收」變成你真的能重做的工作。想把這套方式放進日常開發,可從 AlphaLab AI 課程繼續學習,也可在 AI 文章專區挑下一個練習。






