Neovim 打造 Java8 开发环境
这是一篇一看标题就很邪教的文章, 尽管拥有了 LSP 加持的 Neovim 处理大部分其他主流语言的项目都是游刃有余, 可是很难想象会有人愿意在 Java EE 开发(国内可以认为等价于 Spring 生态)中使用它, 尤其是项目本身极有可能具有沉重历史包袱而不得不使用的古老的 Java8..
本文将说明我在基于 Neovim 开发 Spring 项目时的一些粗浅尝试与配置技巧.
为啥不直接用 Intellij IDEA
前阵子正好看到 <obsidian.nvim: Obsidian 🤝 Neovim> 中有一段话深得我心:
Built for people who love the concept of Obsidian – a simple, markdown-based notes app – but love Neovim too much to stand typing characters into anything else.
对我来说, 本质看重的只是一套统一的文本编辑体验, 我可以随心使用那些烂熟于心的 Vim 功能以及各种自定义的 Lua 脚本, 而无需考虑这是 Vscode 还是 JB 家的套件还是别的什么东西; 至于编译、调试、打包等等其余的操作, 完全可以通过其他的工具来补全, Just do one thing and do it well
另一个原因是, 我是重度 cli 用户: shell + Chrome 可以满足我的绝大部分需求; 因此如果要将开发环境配置文件从我个人的 Linux 电脑上迁移到 MacOS 上, 仅仅需要对 Alacritty + Chrome 做键位的适配即可(将某些 Command 组合键映射到 Ctrl 上), 跨系统的迁移也变得方便起来; 不然针对 MacOS 上不同的软件, 都要适配一遍不同的快捷键… 丑陋且麻烦
最后的一个客观原因是, 无论是各种 Vscode Vim 模式还是 IDEA Vim 模式, 以及各种 Web 在线代码编辑器最喜欢适配的 Vim 模式(甚至不适配 Ctrl + w 删除前一个单词的功能, 每次写题都不敢用, 怕把网页直接关了 😓).. 本质上都是粗劣的表面的模仿, 它们没有 Vim 的精髓. 在尝试强迫自己使用了一段时间的 IDEA 的 Neovim 插件后, 感觉这边的插件和 VSC 的一样也是不太好用, 也不知道怎么样能够配置用上我之前自己写的一些小 Lua 脚本.. 有点像 Vim 原始人类的感觉..
技术限制
-
手里这份代码是 Java8 的, 更高版本的编译直接会挂, 具体原理尚不明晰
Spring 带来的各种动态注入导致仅仅是一个编译打包过程都变得有点复杂, 留坑后续研究一下
-
Neovim 的 LSP 目前有俩, 一个是 <mfussenegger/nvim-jdtls>, 另一个是 <georgewfraser/java-language-server>; 各自有各自的问题:
-
jdtls 大概是基于 eclipse LSP 搞的, 效果确实不错, 但是必须要 >= Java 17
- 看到了一些讨论.. 但是最低也只到 Java11了 <How to run for Java 8? · Issue #44 · mfussenegger/nvim-jdtls>
-
java-language-server 对 Java 的版本没有要求, 貌似是直接基于 Javac API 做的, 但是印象里很弱, 简单的符号跳转都做不到; 而且与 Nvim dev 分支的适配性感觉不太好, 启用后就报错.. 也没高兴切换到稳定分支去尝试了
-
使用方式
在 Neovim LSP 那边折腾了半天, 但是还是没有太好的解决方案; 因为项目要求低版本的 Java, 不能直接通过编译的方式来自动生成对应的辅助文件, 导致最好使的 nvim-jdtls 无法直接启用, 并且也不知道为啥很多时候照着教程配置了还是找不到符号或者咋样..
最终目前的使用方案为: Neovim(代码编写) + IDEA(重构, 打包, 调试)
并且在尝试后发现, 通过一些歪门邪道可以让 Neovim 的 nvim-jdtls
LSP 基本工作, 做到implement跳转、定义跳转、usage 查询等刚需功能,以及一些自动import缺失的包这些
方案如下:
-
shell 的环境变量中配置
JAVA_PATH
为 jdk22 版本, 该 jdk 仅用作 lsp 功能, 并不参与 mvn 打包等实际编译工作- 另经过测试, jdk17 无法成功通过 nvim-jdtls 搜索到符号; 切换至 jdk-22 就可以
-
IDEA 中配置 JDK 为实际参与编译的版本(即1.8), 后续通过该版本进行打包、调试等工作
-
生成
.classpath
文件; 这一步应该是最为重要的一步, 其关乎到符号跳转等功能能否正常工作. 通常来说, 有以下几个生成方式:-
jdtls 自动生成; 这也是最友好最便利的方式, 但我不确定是不是因为我的 jdk22 版本无法实际参与编译, 导致其自动生成的
.classpath
内容并不正确, 包的搜索以及索引感觉存在问题, 查询不到符号 -
直接将 Intellij 项目转换成 eclipse 兼容的形态, IDE 会为我们生成
.classpath
等内容-
我几乎一直用的这个方案, 效果相对来说还可以;
-
但是该方案可能会破坏原有 Java 代码库形态.. 我是 Java 苦手还不是特别会修, 出现该问题后, 整个包的配置都会出现问题,
.idea
也默认被 gitignore 了, 看不到 diff; 我翻了半天 setting 界面也没找到是哪里出的问题, 只能直接删.idea
重新生成一次
-
-
使用 eclipse 插件, 需要编辑
pom.xml
:1<build> 2 <plugins> 3 <plugin> 4 <groupId>org.apache.maven.plugins</groupId> 5 <artifactId>maven-eclipse-plugin</artifactId> 6 <version>2.10</version> 7 <configuration> 8 <classpathContainers> 9 <classpathContainer>org.eclipse.jdt.launching.JRE_CONTAINER</classpathContainer> 10 </classpathContainers> 11 <downloadSources>true</downloadSources> 12 <downloadJavadocs>false</downloadJavadocs> 13 </configuration> 14 </plugin> 15 </plugins> 16</build>
该方案立刻可以产出很好的 classpath -
不过事实上, 该文件生成一次即可, 后续几乎不用变. 里面直接指定的是
src
根目录; 并手动指定一些项目依赖的配置, 不过目前我尝试了半天, 依赖的 Jar 包天生也无法通过 Nvim 看到里面的东西(需要依靠 IDEA 的反编译器可以)
-
-
加一些古朴的黑科技:
ctags -R .
; 这也是今天才偶然意识到的, 因为发现 jdtls 能通过变量的类型跳转到对应的 class 里, 却没法通过 import 的包名跳转到同样的 class 里.. 实在太蠢了, gpt-4o 给出了使用ctags
这样如此惊人的建议, 那么就加上古朴的 ctags 吧!-
ctags 最大的问题在于其对于 symbol 匹配的精准度不够, 经常会把莫名奇妙的两个不相关的变量(比如两个分属不同类的、仅有大小写区别的同名成员变量)匹配到一起.. 之前看 Linux 内核就遇到了同样的问题, 今天还没怎么用就已经又出现该问题了 =_=||
-
但是优点在于启动快, 无需等待 lsp 启动即可工作; 匹配唯一标识符的内容也比较稳定
-
-
添加
lombok
支持; 最开始感觉 lombok 只是一个一般地位的辅助包而已, 但是没想到连 jdtls 甚至都默认集成了唯一一个这个包, 感觉在 java 开发中还是相当基础的一个三方包. -
一些自定义的 Lua 脚本, 比如
snake_case
和camelCase
的相互转换, 处理 myBatis 的 sql 时比较好用:1local function switch_case() 2 local line, col = unpack(vim.api.nvim_win_get_cursor(0)) 3 local word = vim.fn.expand('<cword>') 4 local word_start = vim.fn.matchstrpos(vim.fn.getline('.'), '\\k*\\%' .. (col+1) .. 'c\\k*')[2] 5 6 -- Detect camelCase 7 if word:find('[a-z][A-Z]') then 8 -- Convert camelCase to snake_case 9 local snake_case_word = word:gsub('([a-z])([A-Z])', '%1_%2'):lower() 10 vim.api.nvim_buf_set_text(0, line - 1, word_start, line - 1, word_start + #word, {snake_case_word}) 11 -- Detect snake_case 12 elseif word:find('_[a-z]') then 13 -- Convert snake_case to camelCase 14 local camel_case_word = word:gsub('(_)([a-z])', function(_, l) return l:upper() end) 15 vim.api.nvim_buf_set_text(0, line - 1, word_start, line - 1, word_start + #word, {camel_case_word}) 16 else 17 print("Not a snake_case or camelCase word") 18 end 19end 20vim.keymap.set('n', '<Leader>sc', switch_case)
总的来说这么一套搞下来体验不算很差, 结合 IDEA 也就跟写算法题一样本地写代码提交上去测试再实际跑起来差不多(大雾
nvim-lspconfig 配置代码
<dotfiles/config/.config/nvim/lua/plugins/lsp.lua>
-
不过该 branch 可能不会存活太久, dotfiles 仓库重构预订中..
-
直接看各种的文档也是不错的:
1local HOME = vim.fn.expand('$HOME')
2lspconfig.jdtls.setup{
3 cmd = {
4 "jdtls",
5 "-configuration", HOME .. "/.cache/jdtls/config",
6 "--jvm-arg=-javaagent:" .. HOME .. "/.local/share/nvim/mason/packages/jdtls/lombok.jar",
7 "-data", HOME .. "/.cache/jdtls/workspace",
8 },
9 settings = {
10 java = {
11 format = {
12 settings = {
13 url = "https://raw.githubusercontent.com/google/styleguide/gh-pages/eclipse-java-google-style.xml",
14 profile = "GoogleStyle"
15 }
16 },
17 -- configuration = {
18 -- -- 使 jdtls 了解你的 Maven 配置,
19 -- -- 这里指定 settings.xml 路径
20 -- updateBuildConfiguration = "interactive",
21 -- maven = {
22 -- userSettings = "/Users/rqdmap/Applications/apache-maven-3.9.7/conf/settings.xml"
23 -- }
24 -- },
25 }
26 }
27}
28-- lspconfig.java_language_server.setup{}
仍然存在的问题
-
即使 LSP 高亮解析都已经显示出来了, Tag 的解析还是看不到; 不确定是啥原因, 有时候挂后台放一会就好了, 但有时候 git commit 一轮就坏了(并未进行 reset 等任何删除已有文件的操作), 甚至高亮都直接没了, 这也是我会频繁去动
.classpath
的原因, 只能说不太稳定搜索不到 tags:
1Error detected while processing : 2E426: Tag not found: selectByViewId
权宜方案为: ctags + telescope(grep) 大法好!