|
| 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