【免费下载链接】eclim
Expose eclipse features inside of vim.
Eclim 是一个把 Eclipse 的能力"搬进" Vim 的开源项目(Expose Eclipse Features Inside Of Vim)。在前一篇介绍整体架构后,本篇我们深入 Eclim 的 Java 插件源码,解析 Java 重构(重命名、移动)与代码生成(构造器、Getter/Setter、方法桩)背后共同依赖的核心技术——AST(抽象语法树),看看 Eclim 是如何把 Vim 里一个简单的光标偏移量,最终变成一次安全的重构和一段格式完美的生成代码的。
一、为什么 Java 重构与代码生成必须依赖 AST
对新手来说,最容易想到的实现方式是"字符串替换":把源码里所有Foo换成Bar不就行了?
问题是,字符串层面无法区分:
- 哪个
Foo是类名、哪个只是注释或字符串里的普通单词; - 重命名字段时,对应的
getFoo()/setFoo(...)要不要跟着改; - 要为一个字段生成构造器时,字段到底是什么类型、父类有没有可用构造器。
这些都是"语义"层面的问题,只有把源码解析成一棵抽象语法树(AST),配合 Eclipse JDT 的**绑定(Binding)**机制,才能准确回答。Eclim 的选择非常聪明:它不自己写解析器,而是直接复用 Eclipse JDT 内部的 AST 与重构引擎,Vim 端只负责传递参数和展示结果。
二、关键桥梁:Vim 的光标偏移如何变成 Java 元素
Eclim 的命令都通过统一的命令行参数传递,其中最重要的一个是offset(光标在文件中的字符偏移)。Java 插件里的JavaUtils会用偏移量通过codeSelect(offset, length)或getElementAt(offset)反查出对应的IJavaElement(类、方法、字段……),这一步是所有重构与生成命令的共同入口。
真正操作 AST 的工具类是 ASTUtils.java,核心就三步:
ASTParser parser = ASTParser.newParser(JLS_LATEST); parser.setSource(src); CompilationUnit cu = (CompilationUnit)parser.createAST(null);getCompilationUnit:按最新 JLS 级别把源文件解析成 AST 的CompilationUnit;findNode(ASTUtils.java):用NodeFinder按偏移量定位 AST 节点,再结合元素类型向上回溯到目标节点;commitCompilationUnit:修改 AST 后调用node.rewrite(...)生成TextEdit,应用回文件——只改动 AST 标记过的区域,其余文本原样保留,这是生成代码"不破坏原有排版"的关键。
另外,TypeUtils.getType(TypeUtils.java)会处理"光标在类体任意位置"的情况,自动向上归位到所在的主类或内部类,保证你在类中间任何一行执行命令都能找到正确的目标类型。
三、Eclim 的 Java 重构实现解析
3.1 统一入口:AbstractRefactorCommand 的重构流水线
所有重构命令都继承 AbstractRefactorCommand.java,它把 Eclipse LTK(通用重构框架)的执行流程封装成一条清晰的流水线:
- 刷新项目:非预览模式下先
refresh当前项目及所有引用它的项目,确保 Eclipse 视图与磁盘一致; - 创建重构:由子类
createRefactoring()返回具体Refactoring; - 校验:
checkAllConditions检查是否可执行,失败则抛出RefactorException并把错误回传 Vim; - 生成变更:
createChange得到描述"要改哪些文件、怎么改"的Change对象; - 预览或执行:带
-v参数时,把Change拆成TextFileChange列表返回 Vim 端渲染成 diff 预览;否则用PerformChangeOperation执行,并注册ResourceChangeListener收集被修改的文件列表,Vim 端据此自动重新加载对应缓冲区。
值得注意的细节(AbstractRefactorCommand.java):执行时把变更挂到了 LTK 的UndoManager上,这正是 Vim 里:RefactorUndo/:RefactorRedo命令能撤销、重做重构的原因——撤销栈完全复用 Eclipse 的重构撤销管理器。
3.2 重命名重构:RenameCommand 按元素类型分发处理器
Eclim 支持的:JavaRename对应 RenameCommand.java。流程非常典型:
src.codeSelect(offset, length)用光标选区取出IJavaElement;- 根据元素类型分派到不同的
JavaRenameProcessor(RenameCommand.java):
| 元素类型 | 处理器 | 特殊处理 |
|---|---|---|
| 包 | RenamePackageProcessor | 反射强制开启"同时重命名子包" |
| 类/接口 | RenameTypeProcessor | 文件会一并重命名 |
| 方法 | RenameVirtual/NonVirtualMethodProcessor | 先判断方法是否为 virtual |
| 字段 | RenameFieldProcessor | 自动同步更新 getter/setter 方法名 |
| 泛型参数 | RenameTypeParameterProcessor | — |
| 局部变量 | RenameLocalVariableProcessor | — |
- 统一设置
UPDATE_REFERENCES标志,保证引用处的更新。
可以看到,Eclim 本身几乎没有"重命名逻辑",它做的是把光标位置翻译成元素、把用户意图翻译成正确的 Eclipse 处理器,语义正确性全部由 JDT 引擎保证。
3.3 移动重构:MoveCommand 与 LTK 移动策略
:JavaMove对应 MoveCommand.java。它的思路是"先校验、再策略":
- 先用
IPackageFragmentRoot.getPackageFragment(packName)解析目标包,若目标文件已存在直接报错; - 通过
ReorgPolicyFactory.createMovePolicy(...)创建移动策略(IMovePolicy),交给JavaMoveProcessor,再包装成MoveRefactoring交给上面 3.1 的统一流水线执行。
移动时的引用更新、编译错误检测同样由JavaMoveProcessor负责,Eclim 只需处理好无头环境下所需的Shell等依赖注入。
四、Eclim 的 Java 代码生成实现解析
代码生成类命令位于command/impl/目录,虽然功能各异,但都遵循同一套套路:
定位类型 → 获取 AST 与 Binding → 选择合适的内部 Operation → 运行 → 格式化插入区域
4.1 构造器生成:ConstructorCommand
ConstructorCommand.java(对应:JavaConstructor)是这套套路的最小完整样本:
SharedASTProviderCore.getAST获取共享 AST(避免重复解析),ASTNodes.getTypeBinding(cu, type)取得当前类的类型绑定;findParentConstructor(ConstructorCommand.java)决定要调用哪个父构造器:有参时优先找Object()无参构造,否则用StubUtility2Core.getVisibleConstructors取出所有可见父构造器并按参数个数升序排序,选参数最少的——这与文档中"父类没有无参构造器时自动生成调用参数最少父构造器的代码"的行为一一对应;- 用
AddCustomConstructorOperation执行插入,通过getSibling计算"插在哪"(已有构造器之后、否则第一个方法之前); - 最后调用
JavaUtils.format对新插入区域做格式化。
4.2 Getter/Setter 生成:BeanPropertiesCommand
:JavaGetSet等命令对应 BeanPropertiesCommand.java。它的亮点在插入位置的智能计算(BeanPropertiesCommand.java 的getSibling):
- 优先插在同一字段已有的 getter/setter 旁边;
- 否则插在"下一个属性的 Bean 方法"之前,保证按字段声明顺序成组排列;
- 数组字段的索引访问器(
:JavaGetSet!)由于 Eclipse 原生不支持,是拿标准 stub 模板做正则改写(加int index参数、[index]访问)后手工createMethod插入的——一个"AST 能力 + 文本补丁"的折中案例。
4.3 方法桩生成:ImplCommand
:JavaImpl(列出可覆写/可实现的方法并生成桩代码)对应 ImplCommand.java,它是 AST 与 Binding 结合得最紧密的命令:
- 用
RefactoringASTParser解析 AST,取出类型绑定ITypeBinding; StubUtility2Core.getOverridableMethods遍历类继承链收集所有可覆写方法的方法绑定IMethodBinding,再用Bindings.isVisibleInHierarchy过滤掉包私有等不可见方法(ImplCommand.java);- Vim 端用户勾选方法后,命令带回来方法签名列表,Eclim 按声明类过滤出对应绑定,交给
AddUnimplementedMethodsOperation生成桩; - 一个精妙的细节(ImplCommand.java):插入方法可能同时触发新增 import 语句,导致"新代码偏移"整体后移。Eclim 会比较插入前后 import 区域的长度差,把格式化的 offset/length 修正回来,确保格式器作用在正确的新代码区间上。
4.4 委托方法与新建类:DelegateCommand 与 NewCommand
- DelegateCommand.java 直接继承 ImplCommand复用整套"列表选择"流程,只是把"可覆写方法"换成
StubUtility2Core.getDelegatableMethods得到的"可委托方法",最终用AddDelegateMethodsOperation插入——展示了命令体系良好的可组合性; - NewCommand.java(
:JavaNew)则演示了"轻量路径":不碰 AST,用createCompilationUnit直接创建带模板内容的编译单元,再对全文跑一次格式化,同时展示了如何解析多 source root 的歧义。
五、细节亮点:JavaUtils.format 的格式化小心思
生成代码后"长得像人写的",靠的是 JavaUtils.format:
- 使用项目自己的 formatter 选项构建
DefaultCodeFormatter,保证与 Eclipse 编辑器格式化结果一致; - 格式化前把 offset向前回退到行首、长度向后扩展到换行,规避 Eclipse 缩进配置与 Vim 不一致时产生的缩进错位;
- 最后按项目偏好剥离行尾空白(JDT 格式器在 Javadoc 场景可能引入尾随空格)。
六、总结:Eclim 源码给我们的三个启示
- offset 是 Vim 与 Eclipse 之间的"通用货币":Vim 只发偏移量,
codeSelect/getElementAt/NodeFinder负责在 Java 模型与 AST 之间双向翻译; - 语义交给引擎,胶水留给自己:Eclim 几乎不实现重构/生成的语义逻辑,而是精确调用 JDT 内部 Processor/Operation,并处理无头环境(Shell 注入、子包重命名反射 hack、import 偏移修正)下的适配;
- LTK 的 Change/UndoManager 撑起体验闭环:预览(TextFileChange → Vim diff 窗口)、执行(PerformChangeOperation)、撤销重做(
:RefactorUndo)全部建立在同一套 LTK 抽象之上。
七、相关源码与文档索引
- AST 工具类:ASTUtils.java、TypeUtils.java、JavaUtils.java
- 重构命令:AbstractRefactorCommand.java、RenameCommand.java、MoveCommand.java
- 代码生成命令:ConstructorCommand.java、BeanPropertiesCommand.java、ImplCommand.java、DelegateCommand.java、NewCommand.java
- 官方文档:Java 重构文档、方法生成文档、重构通用配置
【免费下载链接】eclim
Expose eclipse features inside of vim.
相关推荐
Tilt 代码生成背后的 Gengo:一个 Go 代码生成框架的原理与实践
Tilt 代码生成背后的 Gengo:一个 Go 代码生成框架的原理与实践 本篇技术指南以 Gengo( k8s.io/gengo/v2 )框架的 README
开发工具云原生微服务sqlc代码生成原理:AST解析与代码转换技术详解
sqlc代码生成原理:AST解析与代码转换技术详解 SQL代码生成器sqlc通过 抽象语法树 AST 解析 与 智能代码转换 技术,将SQL查询语句自动转换为类
开发工具代码生成数据库揭秘gh_mirrors/auto/auto背后的代码生成原理与架构设计
揭秘gh_mirrors/auto/auto背后的代码生成原理与架构设计 gh_mirrors/auto/auto是一个Java代码生成器集合,旨在通过自动化处
代码生成开发工具后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考