Skip to content

Commit 59a87d9

Browse files
puppylpgclaude
andcommitted
Add: Claude Code LSP 能力演示与原理解析
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a283b0e commit 59a87d9

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
---
2+
title: "Claude Code LSP 能力演示与原理解析"
3+
date: 2026-06-08 10:00:00 +0800
4+
categories: [ai, tool]
5+
tags: [claude-code, lsp, jdtls, java, code-navigation]
6+
description: "通过 8 种 LSP 操作的实际演示,深入理解 Claude Code 如何利用 jdtls 实现精确的代码导航,以及背后的索引原理。"
7+
---
8+
9+
LSP(Language Server Protocol)是 Claude Code 与代码库交互的核心能力之一。本文记录一次完整的演示过程:从铺开 8 种基础操作,到在真实业务类上做深度分析,再到追问"这一切背后怎么工作的"。理论讲解穿插在每次实际操作之后,因为看到结果再理解原理比纯讲理论更自然。
10+
11+
---
12+
13+
## 第一步:用一个简单接口铺开 8 种操作
14+
15+
`IndexOperations`——Elasticsearch 索引管理接口,方法清晰,方便聚焦在操作本身上。
16+
17+
**先摸清文件结构**
18+
19+
```plain
20+
LSP documentSymbol — IndexOperations.java
21+
→ 接口 IndexOperations
22+
├─ create(): boolean
23+
├─ create(Map): boolean
24+
├─ createWithMapping(): boolean
25+
├─ delete(): boolean
26+
├─ exists(): boolean
27+
├─ refresh(): void
28+
├─ createMapping(): Document
29+
├─ putMapping(Document): boolean
30+
├─ getMapping(): Map
31+
└─ alias(AliasActions): boolean 等共 15+ 个方法
32+
```
33+
34+
`documentSymbol` 相当于给文件做 X 光:返回所有类、方法、字段的完整符号树,不需要打开文件逐行阅读。
35+
36+
**跳到外部类型的定义**
37+
38+
```plain
39+
LSP goToDefinition — IndexOperations.java:23(Document 符号处)
40+
→ 跳转到 Document.java(org.springframework.data.elasticsearch.core.document 包)
41+
```
42+
43+
`goToDefinition` 直接跳到符号的定义位置,无论它在当前模块还是二方包里。
44+
45+
**找出所有实现类**
46+
47+
```plain
48+
LSP goToImplementation — IndexOperations.java:39
49+
→ IndicesTemplate(client/elc 包)
50+
→ IndexOperationsAdapter(core 包)
51+
```
52+
53+
2 个实现类,一次返回。这是继承/实现图的反向查询——jdtls 启动时已经扫描过整个项目,知道谁实现了这个接口。
54+
55+
**追踪接口被引用的位置**
56+
57+
```plain
58+
LSP findReferences — IndexOperations.java:39
59+
→ ElasticsearchOperations.java(indexOps() 方法返回类型声明处)
60+
→ SimpleElasticsearchRepository.java:71(字段类型声明)
61+
→ IndexOperationsAdapter.java:37(父接口)等
62+
```
63+
64+
**查看实现类的向下调用链**
65+
66+
实现方法 `IndicesTemplate.createWithMapping()` 负责按实体类注解创建索引:
67+
68+
```plain
69+
LSP outgoingCalls — IndicesTemplate.java:134(createWithMapping)
70+
→ doCreate(IndexCoordinates, Map, Document)
71+
→ createSettings()
72+
→ createMapping()
73+
```
74+
75+
**查看 createWithMapping 被谁调用**
76+
77+
```plain
78+
LSP incomingCalls — IndicesTemplate.java:134(createWithMapping)
79+
→ SimpleElasticsearchRepository.init() Line 94
80+
→ SimpleReactiveElasticsearchRepository.init() Line 76
81+
→ IndexOperationsAdapter.blocking() Line 60
82+
```
83+
84+
Repository 初始化时会自动调用 `createWithMapping()`,按实体类的注解定义创建索引结构和字段映射。
85+
86+
**另外两种操作**
87+
88+
```plain
89+
LSP hover — IndexOperations.java:73
90+
→ 显示 createWithMapping() 完整签名与 Javadoc:
91+
boolean createWithMapping() — Create an index with the settings and
92+
mapping defined for the entity this IndexOperations is bound to.
93+
94+
LSP prepareCallHierarchy — IndicesTemplate.java:134
95+
→ 为 incomingCalls/outgoingCalls 初始化调用链查询上下文
96+
```
97+
98+
---
99+
100+
## 第二步:换到真实业务类,看规模
101+
102+
`IndexOperations` 是接口,换 `AbstractElasticsearchTemplate`(875 行,核心操作抽象基类)看看 LSP 在真实代码上能做什么。
103+
104+
**先摸清规模**
105+
106+
```plain
107+
LSP documentSymbol — AbstractElasticsearchTemplate.java
108+
→ 5 个字段 + 2 个构造器 + 30+ 个方法(875 行)
109+
含 save()、index()、get()、multiGet()、exists()、delete()、
110+
update()、search()、searchForStream()、openPointInTime() 等
111+
```
112+
113+
**核心方法 save(T entity) 被谁调用**
114+
115+
```plain
116+
LSP incomingCalls — AbstractElasticsearchTemplate.java:213(save(T entity))
117+
→ 生产调用方:SimpleElasticsearchRepository.save(S entity) Line 190
118+
→ SimpleElasticsearchRepository.saveAll(Iterable<S>) Line 221(多处)
119+
```
120+
121+
生产链路通过 `SimpleElasticsearchRepository` 统一进入,可以快速评估改动影响范围。
122+
123+
**另一个方法 save(T, IndexCoordinates) 的依赖广度**
124+
125+
```plain
126+
LSP outgoingCalls — AbstractElasticsearchTemplate.java:221(save(T entity, IndexCoordinates))
127+
→ 4 个调用:maybeCallbackBeforeConvert(entity, index)、
128+
getIndexQuery(entityAfterBeforeConvert)、doIndex(query, index)、
129+
maybeCallbackAfterSave(query.getObject(), index)
130+
```
131+
132+
清晰展示了保存一个实体的完整生命周期:before-callback → 构建查询 → 写入 ES → after-callback。
133+
134+
**AbstractElasticsearchTemplate 类型的使用范围**
135+
136+
```plain
137+
LSP findReferences — AbstractElasticsearchTemplate.java:83
138+
→ 12 处引用,分布在 ElasticsearchTemplate、ElasticsearchConfiguration、
139+
ReactiveElasticsearchTemplate 等文件
140+
```
141+
142+
12 处——LSP 返回的是类型系统层面的引用,精确到每一个真正使用了这个类型的位置。
143+
144+
---
145+
146+
## 第三步:追问原理——LSP 怎么知道这一切?
147+
148+
看完演示,自然会问:这些查询为什么这么快?它怎么知道 `AbstractElasticsearchTemplate` 在 12 个文件里被引用了多少次?
149+
150+
答案是:**jdtls 启动时做了一次全量静态分析,把结果存在内存索引里,后续查询直接读索引,不重新解析源码。**
151+
152+
分析管线:
153+
154+
```plain
155+
源文件
156+
│ 解析
157+
158+
AST(抽象语法树)
159+
│ 类型推断(底层是 ECJ,Eclipse Compiler for Java)
160+
161+
符号表(类/方法/字段 → 完整类型信息)
162+
163+
├─▶ 继承/实现图:interface → impl class,class → subclass
164+
├─▶ 引用图(双向):符号 ↔ 所有引用位置
165+
└─▶ 调用图(双向):caller ↔ callee
166+
```
167+
168+
8 种 LSP 操作各自对应哪张索引:
169+
170+
| 操作 | 查的索引 |
171+
| --- | --- |
172+
| `documentSymbol` | AST + 符号表 |
173+
| `goToDefinition` | 符号表(定义位置) |
174+
| `goToImplementation` | 继承/实现图(反向) |
175+
| `findReferences` | 引用图(反向) |
176+
| `hover` | 符号表 + Javadoc |
177+
| `prepareCallHierarchy` | 调用图(初始化查询) |
178+
| `incomingCalls` | 调用图(反向) |
179+
| `outgoingCalls` | 调用图(正向) |
180+
181+
**为什么比 grep 准**:grep 是文本匹配,搜 `save` 会命中所有含该字符串的行,无法区分同名方法。LSP 工作在类型系统层面,`findReferences` 找的是"同一个符号",不会误匹配。
182+
183+
文件变更时,jdtls 只重新分析改动文件及其依赖方(增量更新),不会重跑全量分析。
184+
185+
---
186+
187+
## 第四步:再追问——这个进程谁启动的?什么时候?
188+
189+
**实测进程归属**
190+
191+
```plain
192+
$ ps aux | grep -i "jdt"
193+
24617 PPID=23335(claude) 启动于 15:58:46 /opt/homebrew/Cellar/jdtls/1.57.0/
194+
28798 PPID=28200(Cursor) 启动于 16:01:15 ~/.cursor/extensions/redhat.java/
195+
```
196+
197+
两个完全独立的 jdtls 进程:
198+
199+
+ Claude Code 的(homebrew 安装,stdio 通信):会话初始化时检测到 `pom.xml`,自动 fork
200+
+ Cursor 的(redhat.java 扩展,Unix socket 通信):打开 Java 项目时由 extension-host 启动
201+
202+
**验证生命周期:关掉 Cursor 的 Java 项目**
203+
204+
```plain
205+
$ ps aux | grep -i "jdt"
206+
24617 PPID=23335(claude) 启动于 15:58:46 /opt/homebrew/Cellar/jdtls/1.57.0/
207+
(PID 28798 消失)
208+
```
209+
210+
Cursor 的 jdtls 随项目关闭而销毁,Claude Code 的保持不变。两套索引完全独立,互不干扰——这也意味着在 Claude Code 里做的 LSP 查询和 Cursor 的智能提示是各自独立计算的。
211+
212+
---
213+
214+
## 结论
215+
216+
+ jdtls 启动时全量静态分析,建立 AST → 符号表 → 引用图 → 调用图,后续查询直接读内存索引
217+
+ 8 种操作覆盖代码导航核心场景:每种操作背后对应不同的索引查询,理解这一点才能用对工具
218+
+ Claude Code 与 Cursor 各自维护独立的 jdtls 进程和索引,生命周期与各自客户端绑定
219+
+ LSP 相比 grep 的本质优势:工作在类型系统层面,精确追踪同一符号,不受文本相似性干扰

0 commit comments

Comments
 (0)