ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

javac插件接入DeepSeek:AST驱动的编译期代码重构

javac插件接入DeepSeek:AST驱动的编译期代码重构 com.sun.source.util.Plugin这个接口你在普通 Java 开发里基本不会碰到但只要你写过一次自定义的 javac 插件就会意识到它其实是 JDK 编译器的「后门」。它让你在编译过程中拿到所有.java文件的抽象语法树AST在字节码生成之前插一手。而把 DeepSeek 这类大模型接进去就是把这扇后门拓宽成了一条 AI 辅助开发的专用通道——编译器不再只是报错还能告诉你「这段代码为什么复杂、应该怎么改」。我在试用这个方案之前以为最麻烦的是写 AST 遍历逻辑真正动手之后才发现难点集中在这几件事怎么在编译阶段优雅地调用外部 API、怎么从 AST 里提取出大模型能理解的上下文、怎么避免编译线程被网络请求卡死。这篇文章我会把整套思路和踩过的坑都写出来适合已经会写 Java、想试试编译器插件但还没真正上手过javac -Xplugin的开发者。1. 为什么要在 javac 里塞进一个 DeepSeek——问题与机会1.1 编译器插件到底能做什么JDK 从 1.6 开始就有 JSR 269 注解处理但从 JDK 1.8 起com.sun.source.util.Plugin这个接口才算真正给编译器插件开了绿灯。它跟注解处理器的区别是注解处理器主要盯着注解而Plugin能把整个 javac 的分析流程接过来从词法、语法到语义分析中间每个阶段你都能挂回调。换句话说你能在编译的时候执行任意代码直接接触 AST、符号表、诊断信息。这听起来很吓人但它能做的实事也很明确自定义编译器诊断。比如发现方法循环嵌套太深、字段命名不符合规范、某个工具类被错误地实例化了直接输出警告甚至报错。代码结构分析。在 CI 里编译的同时跑一遍自定义规则比事后用 IDE 扫描要早一步。高危代码拦截。某些敏感 API 调用可以在编译期就拒绝通过。传统做法里这些规则要么靠 Checkstyle、PMD、SonarQube 这类外部工具在编译后扫描源码要么靠 IDE 的实时检查。编译器插件的价值在于它发生在编译生命周期内能访问编译器内部的语义信息比如类型解析结果这是纯源码扫描工具拿不到的。1.2 我设想的应用场景智能诊断与代码建议大模型本质上是另一个「外部工具」但它跟 Checkstyle 这类规则引擎完全不一样。规则引擎只能告诉你违反了第几条规则大模型可以根据上下文自然语言地解释问题还能给出重构后的代码。我当时的目标场景很具体项目里总有那么几个方法写得又长又绕维护成本极高我想在mvn compile的时候自动找出这些方法把方法体摘出来发给 DeepSeek然后拿到返回的重构建议甚至直接生成一个优化版本。这个需求用传统插件做只能做「圈复杂度检测」和「行数检测」但做不到「为什么复杂」和「怎么改才不复杂」。接入 DeepSeek 之后编译输出里就能出现一段人话警告: 方法 processOrder() 圈复杂度达到 24建议拆分出 validateOrder()、calculateDiscount() 两个子方法。 DeepSeek 建议: 当前方法内嵌套了 6 层 if-else其中前 12 行处理的是订单状态校验可以整体提取 第 40-56 行的折扣计算逻辑里三个分支条件互斥建议用表驱动替换。这就是我想要的效果编译期的静态分析与生成式 AI 的解释能力合流。顺着这个想法往下走就要先解决工程问题——怎么把一个编译器插件正确加载起来。2. 动手前的工程准备JDK 版本选择、模块依赖和插件加载机制2.1 先选对 JDK 版本否则接口都找不到com.sun.source.util.Plugin从 JDK 8 就有了但不同版本的行为和兼容性差异很大。我先说结论然后解释为什么。JDK 版本我的建议主要问题JDK 8/11能用但别选javac源码模块化还没彻底很多内部 API 通过tools.jar暴露打包和 IDE 调试都比较别扭JDK 17入门首选长期支持版本模块系统成熟jdk.compiler模块导出规则清晰网上资料也多JDK 21进阶试用新增语言特性比如虚拟线程会带来 AST 新节点类型插件代码要考虑兼容如果编译目标代码包含 record、sealed 类需要测试我自己用的是 JDK 17 来写插件编译目标代码也是 JDK 17。这样做的好处是语言层面的新特性相对稳定AST 节点类型变化不大。如果你需要给一个旧项目做插件但那个项目本身用的是 JDK 8建议还是用 JDK 17 编译插件 jar再用--release 8设编译目标这样插件能在老版本编译器上跑但同时又避开了tools.jar那套老式依赖方式。2.2 打包插件并通过 javac -Xplugin 加载插件加载入口非常简单。javac在启动时会查找-Xplugin参数后指定的插件名然后从类路径或插件路径里实例化实现类。为了让编译器知道这个插件叫什么名字标准的做法是在 jar 包里的META-INF/services/com.sun.source.util.Plugin这个文件中写入实现类的全限定名。举个例子我的插件实现类叫com.example.DeepSeekCompilerPlugin那么 jar 的目录结构是myplugin.jar └── META-INF └── services └── com.sun.source.util.Plugin # 内容是 com.example.DeepSeekCompilerPlugin └── com/example/DeepSeekCompilerPlugin.class然后执行javac -Xplugin:DeepSeekPlugin -processorpath myplugin.jar \ -cp myplugin.jar \ -d target/classes \ src/main/java/com/example/*.java这里有个容易踩的地方插件类本身既要在-processorpath里也要在-cp里。因为Plugin接口是从jdk.compiler模块加载的但实现类需要通过类路径加载而且它在编译过程中还会创建自己的辅助类这些辅助类默认用的是编译任务的类路径。我一开始只在-processorpath里放了 jar结果插件能初始化但一访问辅助类就抛ClassNotFoundException。2.3 模块封装--add-exports 到底在导出什么JDK 17 对 JDK 内部 API 做了强封装。com.sun.source.tree和com.sun.source.util这两个包被定义在jdk.compiler模块里但没有对外部模块完全导出。正常情况下你的插件代码直接import com.sun.source.util.TreePathScanner会在编译期就报错package com.sun.source.util is not visible解决办法是给 javac 加上模块导出参数。注意这个参数不止编译插件时要加插件实际运行的时候同样要加。比如javac --add-exports jdk.compiler/com.sun.source.utilALL-UNNAMED \ -Xplugin:DeepSeekPlugin ...很多人只在javac命令行里加了结果 IDE 里跑插件调试时忘了在运行配置里加导致明明能编译运行时却报模块访问错误。我的经验是直接把--add-exports jdk.compiler/com.sun.source.utilALL-UNNAMED同时写到 Maven 编译插件的compilerArgs和 IDE 运行配置的 VM options 里两边保持一致就不会间歇性出问题。还有一个隐藏点com.sun.source.tree和com.sun.source.util需要分别导出不能只导出util包。因为TreePathScanner的声明里大量引用了com.sun.source.tree下的类型你 import util 包时类型解析会连带访问 tree 包。我建议干脆三个包一起导出--add-exports jdk.compiler/com.sun.source.treeALL-UNNAMED --add-exports jdk.compiler/com.sun.source.utilALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.apiALL-UNNAMED最后一个com.sun.tools.javac.api是给JavacTask和Trees用的后面写代码时你会需要它。这一步做完插件才算真正跑起来。3.com.sun.source.util.Plugin接口核心机制拆解init 方法、JavacTask 与 Trees3.1 Plugin 接口的注意点名字与优先级Plugin接口实际上非常小核心方法只有两个public interface Plugin { String getName(); void init(JavacTask task, String... args); }JavacTask是编译器任务的主入口它既可以用javacAPI 的方式在外部创建也可以内部通过插件注入获取。args是你在-Xplugin:DeepSeekPlugin后面追加的参数比如javac -Xplugin:DeepSeekPlugin:modeldeepseek-chat:threshold20 ...那么init收到的args[0]可能是modeldeepseek-chatargs[1]是threshold20具体解析方式完全由你自己定义。这个设计让我很满意因为它意味着同一个插件 jar 可以按项目配置阈值、模型名、API key而不用重新编译。有一点必须注意多个插件同时注册时javac会按getName()的结果排序后依次初始化所以如果你的插件逻辑依赖另一个插件的输出尽量不要做这种假设编译器插件的执行顺序本质上就是未完全约定的。3.2 JavacTask 与 Trees编译器公开 AST 的两把钥匙JavacTask里有两个方法是插件访问 AST 的钥匙Iterable? extends CompilationUnitTree parse(); Iterable? extends Element analyze();parse()触发词法和语法分析返回编译单元的根节点列表。analyze()触发语义分析返回顶层元素类、接口、枚举等。但光有根节点还不够你需要在编译过程中进行「阶段化钩子」以便拿到语义信息。比较好的做法是在init方法里注册监听器task.addTaskListener(new TaskListener() { Override public void started(TaskEvent e) { // 可感知阶段开始 } Override public void finished(TaskEvent e) { if (e.getKind() TaskEvent.Kind.ANALYZE) { // 语义分析完成此时可以安全地使用 TreePathScanner } } });Trees类则负责把编译单元树和符号表关联起来。比如你想知道某个MethodInvocationTree到底调用了哪个被解析过的方法Trees能给你答案Trees trees Trees.instance(task); TreePath path TreePath.getPath(compilationUnit, tree); Element element trees.getElement(path);这个能力是把 AI 建议落到真实代码逻辑的关键——只有当你知道一个方法调用对应的是项目里的哪个类、哪个方法你给大模型的信息才不只是「字符串」而是「结构化的语义」。3.3 遍历 AST 的两种惯用法及我的选择拿到CompilationUnitTree后遍历 AST 有两种主流方式继承TreePathScannerObject, Object重写visitMethod、visitClass、visitIf等方法。利用TreeScanner或TreeVisitor手动派发。我推荐第一种。TreePathScanner的好处是它维护了一个TreePath栈你在扫描到任意节点时都能拿到从根到当前节点的完整路径。这意味着当你定位到一个IfTree时可以立刻向上追溯到它所属的类和方法而不需要手动记录上下文。下面是一段非常常见的骨架代码public class ComplexityScanner extends TreePathScannerObject, Integer { private int ifDepth 0; private int maxDepth 0; private MethodTree currentMethod null; Override public Object visitMethod(MethodTree node, Integer unused) { currentMethod node; ifDepth 0; maxDepth 0; Object result super.visitMethod(node, unused); if (maxDepth 3) { String code node.toString(); // 把 currentMethod 的源码片段写到一个收集器里等待后续给 DeepSeek } return result; } Override public Object visitIf(IfTree node, Integer unused) { ifDepth; maxDepth Math.max(maxDepth, ifDepth); Object result super.visitIf(node, unused); ifDepth--; return result; } }这里我故意用了maxDepth来记录 if 嵌套层数因为圈复杂度的完整算法需要CyclomaticComplexity这种外部库而简单场景下嵌套深度已经能反映 80% 的问题。把不满足阈值的方法源码丢给 DeepSeek就足够生成有价值的重构建议。4. 把 AST 片段送去 DeepSeek上下文构造、提示词设计与结果解析4.1 从 code 到 prompt控制 token 量的第一个要诀AST 节点能直接toString()但不意味着你应该把整个源码文件倒给大模型。我犯过的最大错误就是把一个 500 行的类完整发给 DeepSeek然后让它分析其中一个方法。这种行为有两个问题token 迅速膨胀、返回质量反而下降。大模型的注意力会被无关代码分散尤其是字段定义、注解、导入这些噪声。我的原则是「只喂局部上下文但给足关键信息」。一个方法的提示词建议包括方法签名包括泛型、返回类型、参数名。方法体源码控制在 200 到 300 行以内。该方法涉及的类名、字段名以及调用到的其它方法名不用带方法体。一个明确的分析任务例如「找出圈复杂度高于阈值的路径并给出重构后的完整方法代码」。控制 token 的另一个办法是先扫描 AST把方法体截断到只保留前 N 行。大部分重构建议关注控制流结构而不是每一行赋值语句。我用过 50 行、100 行、200 行三种截断策略100 行是最平衡的。4.2 我使用的提示词结构与分级配置DeepSeek 的 API 走的是 OpenAI 兼容格式所以你既可以用官方 SDK也可以用任何兼容 OpenAI 的 HTTP 客户端。我的提示词结构分三层系统指令设定角色和输出格式。参考资料抽取出的方法源码、类名、相关字段。用户问题要求输出诊断报告和重构代码。一个实际可用的 prompt 模板你是一名经验丰富的 Java 架构师。请分析下面的方法指出 1. 圈复杂度高的具体原因哪些 if、for、while 分支导致 2. 哪些逻辑可以提取为独立方法 3. 给出重构后的完整 Java 方法代码不要省略任何逻辑。 方法源码: java public void processOrder(Order order) { ... }类上下文:类名: OrderService字段: orderRepository, discountCalculator相关方法: validateOrder(Order), sendNotification(Order)注意 java 这个标记在 prompt 里其实是可以用的DeepSeek 能识别。但如果你用的是别的模型最好改成纯文本格式避免某些模型在代码块解析上不稳定。 输出解析上我要求 DeepSeek 返回一段固定结构的回答比如先给「问题定位」再给「重构代码」代码放在 java 代码块里。然后我在插件里用正则提取代码块内容。如果模型给的不是固定格式你就得做容错解析这个非常容易翻车。 ### 4.3 调用 DeepSeek API 的封装超时、重试与同步阻塞问题 这是整个方案里最容易被低估的一环。编译器插件运行在 javac 的分析线程上如果你直接同步调用 HTTP那么一次编译可能因为网络延迟多出十几秒甚至几分钟。CI 里的 mvn compile 会直接超时。 我给出的建议是分两步走 第一在插件本地做语法分析和知识提取把要请求的数据准备成轻量 JSON。这一步必须在编译线程里完成因为需要访问 AST。 第二调用 DeepSeek API 放到异步执行。我有一个比较笨但可靠的实现插件扫描阶段把「待分析任务」放进一个线程安全的队列然后由一个专门的线程池去消费队列调用 API最后把生成的建议通过 Messager 打印到编译输出。但由于 javac 的编译任务可能在主线程等待你必须考虑编译结束时机。最简单的做法是在编译结束钩子里 shutdown() 线程池并 awaitTermination给 API 请求一个有限的窗口期比如 20 秒超时就把未完成的任务丢进日志。 一个关键的 API 参数是 timeout。DeepSeek API 默认可能 60 秒没有返回你可等不起。我把超时设置为 15 秒重试 1 次重试退避 2 秒。如果仍失败就在编译输出里打一个警告但不阻断编译。 下面是调用部分的骨架用 Java 11 HttpClient不引额外依赖 java HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://api.deepseek.com/chat/completions)) .header(Content-Type, application/json) .header(Authorization, Bearer apiKey) .timeout(Duration.ofSeconds(15)) .POST(BodyPublishers.ofString(payload)) .build();这里我特别提醒一个很容易犯的错误不要在源码里写死 API key。你可以从-Xplugin参数传也可以从环境变量读甚至从~/.config/deepseek读取。把 key 打到 jar 里然后推到 Git 仓库基本等于把密钥公开了。这一点在协作项目里尤其重要。5. 一个完整的插件示例扫描方法复杂度并让 DeepSeek 给出重构建议5.1 插件主类与注册下面这个示例我尽量精简但保持可用。它实现的能力是扫描每个方法如果 if 嵌套深度超过指定阈值就把方法源码发给 DeepSeek并在编译结束后把建议打印出来。public class DeepSeekCompilerPlugin implements Plugin { private static final String PLUGIN_NAME DeepSeekPlugin; Override public String getName() { return PLUGIN_NAME; } Override public void init(JavacTask task, String... args) { PluginConfig config PluginConfig.parse(args); Trees trees Trees.instance(task); task.addTaskListener(new TaskListener() { Override public void started(TaskEvent e) { } Override public void finished(TaskEvent e) { if (e.getKind() TaskEvent.Kind.ANALYZE) { CompilationUnitTree unit e.getCompilationUnit(); ComplexityScanner scanner new ComplexityScanner(config, trees); scanner.scan(unit, null); } } }); task.addTaskListener(new TaskListener() { Override public void finished(TaskEvent e) { if (e.getKind() TaskEvent.Kind.COMPILATION) { // 结束前等待异步任务完成 } } }); } }注册文件META-INF/services/com.sun.source.util.Plugin里写com.example.DeepSeekCompilerPlugin5.2 扫描逻辑TreePathScanner 的实际代码ComplexityScanner负责找嵌套过深的方法。和前面骨架相比这里我在visitMethod中使用了Trees获取方法元素这个元素信息会一起加入 promptpublic class ComplexityScanner extends TreePathScannerObject, Integer { private final PluginConfig config; private final Trees trees; private int ifDepth 0; private int maxDepth 0; private MethodTree currentMethod; private CompilationUnitTree currentUnit; public ComplexityScanner(PluginConfig config, Trees trees) { this.config config; this.trees trees; } Override public Object visitCompilationUnit(CompilationUnitTree node, Integer unused) { currentUnit node; return super.visitCompilationUnit(node, unused); } Override public Object visitMethod(MethodTree node, Integer unused) { currentMethod node; ifDepth 0; maxDepth 0; Object result super.visitMethod(node, unused); if (maxDepth config.getMaxNestedIfDepth()) { MethodElementInfo info extractMethodInfo(node); // 交给异步分析器 AsyncAnalyzerQueue.submit(info); } return result; } Override public Object visitIf(IfTree node, Integer unused) { ifDepth; maxDepth Math.max(maxDepth, ifDepth); Object result super.visitIf(node, unused); ifDepth--; return result; } private MethodElementInfo extractMethodInfo(MethodTree node) { TreePath path TreePath.getPath(currentUnit, node); Element el trees.getElement(path); String enclosingClass ; if (el ! null) { enclosingClass el.getEnclosingElement().getSimpleName().toString(); } return new MethodElementInfo(enclosingClass, node.getName().toString(), node.toString(), maxDepth); } }这里有一个体验上的细节node.toString()能拿到方法的原始源码但它在CompilationUnitTree上的格式化结果和源码基本一致足以在 prompt 中作为输入。如果你的项目使用了 Lombok 这类注解处理器可能 AST 中会出现一些由处理器生成的代码插件遍历时要做好过滤——我一般只处理用户源码文件不处理GENERATED代码通过检查CompilationUnitTree对应的 URI 后缀来过滤。5.3 DeepSeek 客户端封装以及如何把建议写回编译器诊断异步队列的消费者其实就是上一节提到的 HttpClient 调用。我在调用成功之后通过 javac 的Messager往编译输出打消息public class DeepSeekAnalyzer { private static final Pattern CODE_BLOCK Pattern.compile( (?s)java\\s*(.*?)); static void analyze(MethodElementInfo info, PluginConfig config) { String prompt PromptBuilder.build(info); String response DeepSeekClient.chat(prompt, config); String suggestion extractJavaCode(response); if (suggestion.isEmpty()) { suggestion DeepSeek 未返回可解析的代码块请查看 API 响应日志。; } // 把结果交给编译诊断输出 DiagnosticOutput.printWarning( 方法 info.getMethodName() 嵌套深度过高( info.getMaxDepth() ) DeepSeek 建议重构\n suggestion); } }DiagnosticOutput底层用什么最直接的是通过JavacTask拿到com.sun.tools.javac.util.Log或者更通用一点直接用System.err.println配合-Xplugin的日志开关。严格来说走Messager才能跟javac的-Xdiags格式统一但在一个简化插件里重定向到编译日志更省事。这个示例完整跑通之后你会看到像这样的编译输出警告: 方法 processOrder 嵌套深度过高(5)DeepSeek 建议重构 - 问题定位: 前 20 行是订单状态校验多个 if 分支可提取为 validateOrder - 重构代码: private void validateOrder(Order order) { ... }注意-Xplugin的输出默认不会带文件名和行号因为这已经是编译诊断之外的自定义输出。如果你需要精确位置可以在MethodTree上调用getStartPosition()再通过JavacTask的SourcePositions算出行号。这一步补上后输出会专业很多。6. 实测中的坑与规避javac 阶段、并发编译、模块导出和 IDE 兼容性6.1 阶段问题为什么必须在 ANALYZE 之后才能拿到完整语义信息我在最初版本中犯过一个错误在finished(e.getKind() PARSE)阶段就尝试用Trees#getElement查符号结果大量返回 null。原因是Trees实例在语义分析阶段才会填充符号表。编译器分析阶段之前AST 里只有语法结构没有类型绑定。所以正确做法是只在TaskEvent.Kind.ANALYZE的finished事件里启动扫描这时候类型信息、方法符号、字段符号基本都已经确定。如果你还需要在GENERATE阶段之前做修改ANALYZE到GENERATE之间还有一小段窗口足以读取符号表。这和注解处理器的行为不同。注解处理器是在专门的 round 里拿到 Element 的而 Plugin 直接接触 javac 生命周期所以阶段判断必须自己来。6.2 并发编译下 API 调用会卡死编译线程Gradle 和 Maven 3 默认都会在多模块项目里并发编译多个模块。如果你让每个 javac 进程都在finished(ANALYZE)阶段同步调用 DeepSeek API那么并发编译会被网络 IO 大量阻塞整个构建体验会非常差。我建议三个层次的缓解在插件内部做请求合并。比如 50 毫秒内收集到的方法分析任务合并成一个请求一次发给大模型。DeepSeek 支持多轮对话也支持一次 prompt 里包含多个代码片段。使用异步线程池并且把线程数限制为 2。不要起 10 个线程同时请求因为编译结束时要awaitTermination起得多反而都在等网络响应。设置开关。在本地开发时默认关闭 AI 调用只在 CI 环境变量里开启DEEPSEEK_ANALYZE_ENABLEDtrue避免开发人员每次编译都烧 token。实测数据供你参考一次全量编译 80 个模块如果每个模块都触发 10 个 API 请求总共 800 个请求假设每个 2 秒同步模式下耗时 1600 秒。异步线程池 2 线程 请求合并后实际等待时间能控制在 60 秒左右。因此构建工具的超时设置一定要留足空间。6.3 IDE 与构建工具兼容性Maven/Gradle 里的最终配置样例直接把插件接到 Maven 编译插件时需要在pom.xml里配置下面这些参数。我经历过的坑是Maven 编译用了 fork--add-exports参数如果不传给 fork 的 javac 进程插件运行时照样会缺少模块访问权限。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration release17/release compilerArgs arg-Xplugin:DeepSeekPlugin:threshold3/arg arg--add-exports/arg argjdk.compiler/com.sun.source.utilALL-UNNAMED/arg arg--add-exports/arg argjdk.compiler/com.sun.source.treeALL-UNNAMED/arg arg--add-exports/arg argjdk.compiler/com.sun.tools.javac.apiALL-UNNAMED/arg /compilerArgs annotationProcessorPaths !-- 如果项目有其它注解处理器要一并声明 -- /annotationProcessorPaths /configuration /plugin如果你用 Gradletasks.withType(JavaCompile).configureEach { options.compilerArgs [ -Xplugin:DeepSeekPlugin:threshold3, --add-exports, jdk.compiler/com.sun.source.utilALL-UNNAMED, --add-exports, jdk.compiler/com.sun.source.treeALL-UNNAMED, --add-exports, jdk.compiler/com.sun.tools.javac.apiALL-UNNAMED, ] }IDE 兼容性是我最后唠叨的重点。Eclipse 自带的编译器是 ECJ不是 javac所以-Xplugin在 Eclipse 里根本不会被执行。IntelliJ IDEA 在非默认情况下可能用自己的编译器做构建也需要在Settings - Build Tools - Maven - Runner里把「Delegate IDE build/run actions to Maven」打开确保走 Maven 的 javac 流程。否则你会陷入「IDE 里不生效、命令行又没问题」的困惑。7. 我的一些经验与扩展思路这个项目最开始只是我想在 CI 上做实验后来发现它真正能落地的方向比想象中多。除了方法级重构建议你还可以把com.sun.source.util.Plugin这套机制扩展成团队代码规范的 AI 审查器比如禁止使用某些java.util.Date的过期方法、发现System.out.println残留、检测循环依赖导致的问题。传统规则引擎处理这些要写一堆正则和结构匹配而大模型只需要一段语义化的 prompt 就能理解规则并且能定位到具体代码。我把这个插件在内部项目里跑了大半年最大的体会是AI 编译期插件不适合做「分钟级」的交互它更适合做「编译完成后的增量审查」。你想在编程时实时获得 AI 建议IDE 插件是更好的选择但你想让所有开发者提交代码时无感获得 AI 级诊断编译器插件几乎是唯一的路。另外有一个经验要分享不要把大模型的 API key、项目代码片段、编译输出混在一起打到同一个日志文件。代码本身对企业是敏感资产而 API key 泄漏会带来直接的经济损失。我给插件加了-Xplugin:DeepSeekPlugin:redactKeytrue选项在日志里把 key 替换成sk-***并默认开启。这点看起来很小但配合回归测试时看日志特别有用不会不小心把密钥打印到 CI 日志里。如果你也想试建议从最小用例开始先不接 DeepSeek只写一个能打印出方法名的插件然后加 AST 扫描再加网络调用。每一步都单独验证最后再拼起来。这样遇到问题的时候你能分清到底是 javac 阶段的问题还是大模型 prompt 的问题而不是两头一起崩。最后再多说一句插件的启动参数args一定要认真解析别图省事用全局变量。因为同一个 javac 进程在 Gradle daemon 里可能被复用不同模块的编译参数会不同如果全局变量被上一个模块污染下一个模块就全错了。我当时在这个小细节上 DEBUG 了一个晚上排查到最后才发现是静态字段没重置。
返回列表