Language Support for Java 1.1.0 版本包含了一项重要更新:现在插件在导入新的 Java 项目时,项目元数据文件(.project,.classpath,settings等)默认将不再生成于项目路径下。这一问题 自 2018 年被记录至今已有超过三年的时间。本文旨在记录并分享我们解决这一问题的过程和最后的解决方案。
悬在头顶的“达摩克利斯之剑”
随着 VS Code Java 的功能逐渐丰富,用户数量也在稳步上升。但是由于 Java 插件在导入项目时,会在项目目录下生成元数据文件的问题,我们得到了不少的1星差评。可以预见,随着用户基数增加,因这一问题而造成的差评数量也会增加。这就如同一把悬在我们头顶的“达摩克利斯之剑”,如果不及时解决,问题随时都有可能爆发。
其实这并不是我们产品组不想彻底修复这一问题,根本原因需要从 Java 语言服务的架构说起:
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 机制将这些元数据文件隐藏的,它的实现原理如下图所示:
可以看到项目的实际路径放在了 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。在问题讨论的过程中他们给予了非常有用的建议,对问题的解决起到了非常关键的作用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。