javax.lang.model实战:为DeepSeek补齐Java类型上下文,让AI生成代码一次编过
前段时间我重构一版内部业务系统时让 DeepSeek 帮忙写接口实现类发现一个特别典型的毛病它生成的代码经常引用不存在的类型、把泛型参数放错位置、甚至调用根本没定义过的方法。这不是模型能力不行而是它根本看不到整个工程的类型全貌。后来我用了javax.lang.model来解决这个问题思路很直接在编译期先把类、接口、泛型、注解这些结构全部梳理成上下文再交给 DeepSeek 生成代码最后用编译校验兜底。这套东西我内部代号叫 jlang-gen就是“DeepSeek javax.lang.model 实现”这个项目的复盘。如果你写过注解处理器或者想把 LLM 接进构建流程这篇文章值得看完。1. 为什么是 javax.lang.model1.1 大模型写 Java 代码的“类型盲区”先说痛点。我们让 LLM 写 Java 代码时通常喂给它的是几段零散的源码文件、一个需求描述然后希望它输出完整实现。问题在于Java 代码的正确性极度依赖类型信息而 prompt 里的文本根本无法完整表达这些信息。举例来说你让它“生成一个根据用户ID查询用户信息的分页接口”它不知道UserQuery里有哪些字段、UserRepository有没有findByCondition方法、ResultT的泛型约束是什么。它只能靠训练时见过的相似代码去猜猜对了一回猜错了十回。javax.lang.model 的存在就是为了在编译期描述 Java 程序的结构。它能看到源码中所有的类、方法、字段、泛型参数、注解、继承关系。把这些信息结构化之后喂给模型就等于给 AI 戴上了一副“类型眼镜”让它不再靠猜。1.2 反射、AST库、注解处理器三条路怎么选在考虑怎么给 LLM 提供类型上下文时我认真比较过三条技术路线。方案获取类型信息的时机能否拿到泛型与注解额外依赖生成代码后能否参与下一轮编译运行时反射运行时能拿到部分泛型但容易擦除无否JavaParser 等 AST 库编译前/运行时解析源码能拿到源码结构但类型解析弱需要引入第三方包否javax.lang.model 注解处理器编译期能拿到完整解析后的类型、泛型、注解无JDK 原生是最终我选了 javax.lang.model。原因有三第一它是 JDK 自带的标准 API不需要引入额外的解析库第二它跑在编译器的符号表之上拿到的类型是经过编译解析的和运行时反射拿到的那套“擦除后类型”完全是两码事第三注解处理器在编译轮次中天然支持生成源码文件生成的文件会参与到下一轮编译里这正好可以形成一个“解析 → 生成 → 校验”的闭环。2. javax.lang.model 的建模体系速览2.1 Element代码元素的“家谱”javax.lang.model 把 Java 源码中的基本单位抽象成Element接口它上面有几种核心子类型PackageElement包、TypeElement类或接口、ExecutableElement方法或构造器、VariableElement字段、局部变量、参数、TypeParameterElement类型参数。可以把它理解为代码的“家谱”包下面有类类里面有方法和字段方法又有参数和返回值类型。遍历这棵家谱树就能把整个类的结构完整提取出来。举个例子如果我们拿到一个TypeElement代表UserService那么getEnclosedElements()就能拿到它下面定义的所有方法、字段和内部类。2.2 TypeMirror类型系统的“指纹”Element 描述的是“有什么东西”TypeMirror 描述的是“这个东西长什么样”。element.asType()返回一个 TypeMirror它能表达完整类型信息是原始类型、数组类型、声明类型还是泛型变量、通配符。这里特别要注意DeclaredType里有一个getTypeArguments()方法它能拿到泛型实参。比如ListUser通过这个方法拿到的就是User这个 TypeMirror而不是一个被擦除的List。这一点对 AI 生成代码极其关键因为泛型错了编译一定过不了。2.3 Elements 与 Types 两个工具类的正确用法只靠 Element 和 TypeMirror 还不够真正的干活利器是Elements和Types这两个工具类。它们是 ProcessingEnvironment 直接提供的分别负责元素操作和类型判断。Elements.getAllMembers(typeElement)能拿到一个类型的所有成员包括从父类和接口继承过来的。Types.directSupertypes(typeMirror)能拿到直接的父类型列表从这个列表里可以判断一个类实现了哪些接口。Types.asMemberOf则可以解决“父类中的泛型 T 在子类中到底是什么类型”这类问题。下面这段代码演示了遍历一个类的所有方法并用 Types 补全类型信息的核心写法for (Element member : elementUtils.getAllMembers(serviceElement)) { if (member instanceof ExecutableElement method) { ExecutableElement inType (ExecutableElement) typeUtils.asMemberOf( serviceElement.asType(), method); for (VariableElement param : inType.getParameters()) { // 这里拿到的 param.asType() 已经是解析后的具体类型 paramTypeMap.put(param.getSimpleName().toString(), param.asType().toString()); } } }如果不经过asMemberOf这一层直接从getAllMembers拿到的父类方法其参数类型可能仍然是父类中定义的受类型变量而不是子类实例化后的具体类型。这是很多初学者踩过坑的地方。3. jlang-gen 的完整实现流程3.1 整体架构设计整个 jlang-gen 拆成了六个模块链路非常清楚先定义一个注解作为入口再写注解处理器去收集元素把类结构转成 JSON构建提示词调用 DeepSeek最后把生成的代码通过 Filer 写入工程。这六个模块分别是JGeneratedApi注解标注哪些类需要交给模型生成实现ApiGeneratorProcessor注解处理器入口负责收集被标注的元素TypeContextExtractor把 TypeElement 转成结构化 JSONPromptBuilder把 JSON 和业务需求组合成提示词LLMClient调用 DeepSeek 完成推理CodeWriter用 Filer 创建并写入生成的源码文件整个处理过程嵌入在编译周期里也就是说每次mvn compile的时候它会自动完成分析和生成。这一步是注解处理器最大的优势。3.2 处理器入口与元素收集注解处理器的入口很简单但有几个细节要处理对SupportedAnnotationTypes必须写全限定名SupportedSourceVersion要和你使用的 JDK 版本匹配。init 方法里必须把Elements、Types、Filer、Messager四个对象缓存下来后面每一步都要用。SupportedAnnotationTypes(com.jlang.gen.annotation.JGeneratedApi) SupportedSourceVersion(SourceVersion.RELEASE_17) public class ApiGeneratorProcessor extends AbstractProcessor { private Types typeUtils; private Elements elementUtils; private Filer filer; private Messager messager; Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); typeUtils processingEnv.getTypeUtils(); elementUtils processingEnv.getElementUtils(); filer processingEnv.getFiler(); messager processingEnv.getMessager(); } Override public boolean process(Set? extends TypeElement annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith( JGeneratedApi.class)) { if (element instanceof TypeElement typeElement) { handleTypeElement(typeElement, roundEnv); } } return false; } }return false意味着 Processor 只负责收集和生成不独占这些注解的处理权其他处理器依然可以处理它们。如果这里返回true其他注解处理器可能会拿不到相同的注解容易引起隐性问题。3.3 把类结构序列化为 LLM 上下文LLM 需要一个机器可读的结构化输入我选择用 JSON 作为中间格式。每种类型在 JSON 里都有对应的字段类名、类型种类、实现的接口、方法列表、字段列表、泛型参数、继承的父类。{ className: com.example.api.UserApi, type: interface, typeParameters: [], interfaces: [], methods: [ { name: findUserPage, returnType: com.example.common.Resultcom.example.vo.UserPageVO, parameters: [ { name: userQuery, type: com.example.query.UserQuery } ], modifiers: [public, abstract], throws: [] } ] }注意一点这里不能用toString()去兜底所有类型场景。TypeMirror.toString()有时候会丢失泛型约束或注解信息更稳妥的方式是手动遍历 TypeMirror 的层级结构把DeclaredType、TypeVariable、WildcardType都拆出来单独转换。用一个 Map 预先缓存已经遍历过的类型可以避免循环引用导致的栈溢出因为类型结构里经常出现互相引用的场景。3.4 提示词模板设计拿到 JSON 之后提示词的构建要点在于“给模型足够的约束”。我用的模板是你是一名 Java 资深工程师只输出代码不要输出任何解释。 以下是当前工程的类型上下文JSON {jsonContext} 需求{userRequirement} 要求 1. 只输出接口实现类不要输出额外符号。 2. 所有类型必须使用上下文中的类型不要发明不存在的类或方法。 3. 保持当前工程的代码风格使用 Lombok 风格简化 getter/setter。 4. 不要使用上下文未列出的第三方库。直接把类型上下文和需求一起塞进提示词里比单独调一次“查类型再生成”可靠得多。像 Lombok 这种能影响代码风格的信息也要明确写出来否则模型很容易按自己记忆中“最通用”的风格写。3.5 调用 DeepSeek 并落盘生成文件LLM 推理返回之后第一步先把回复里的 Markdown 代码块剥掉只保留纯 Java 代码。然后调用Filer.createSourceFile创建新源文件。JavaFileObject sourceFile filer.createSourceFile( com.example.impl.UserApiImpl, apiElement); try (Writer writer sourceFile.openWriter()) { writer.write(generatedCode); }这里有个关键细节createSourceFile的第二个参数apiElement是“创建该文件的源元素”它会被编译器记录为生成文件的来源。当同一个注解处理器在后续轮次再次运行时可以通过它判断文件是否已存在避免重复生成。不要直接把生成代码写进已有的源文件里而是让生成类作为一个新的.java文件参与下一轮编译。这样 Compiler 才能正确解析它依赖的所有类型并且保证 IDE 的索引也是最新的。4. 类型安全校验让 AI 生成的代码能编过4.1 内存编译校验流程DeepSeek 生成的代码不能直接信任即使给了类型上下文它依然可能在细节上犯错。因此生成之后必须走一遍真正的 Java 编译校验。我直接用javax.tools.JavaCompiler在内存里编译生成的字符串源码配合DiagnosticCollector把编译错误收集回来。如果出错把这些错误信息重新拼进提示词让模型再修一遍。JavaCompiler compiler ToolProvider.getSystemJavaCompiler(); DiagnosticCollectorJavaFileObject diagnostics new DiagnosticCollector(); StandardJavaFileManager fileManager compiler.getStandardFileManager(diagnostics, null, null);注意ToolProvider.getSystemJavaCompiler()在纯 JRE 环境下可能返回 null编译器只存在 JDK 的运行时环境里。如果你是跑在 IDE 内嵌的 JRE 里要先确认运行时用的是完整 JDK。4.2 三个我最看重的校验项多轮修复的经验让我总结出三个必须重点检查的问题前三位的错误率极高类型不存在AI 生成了一个UserServiceImpl但它引用的UserService并不存在。方法签名不匹配它调用了一个参数是String的方法但接口里定义的是Long。泛型实参不匹配接口要求返回ResultUser它返回了ResultObject。把这三类错误通过 Diagnostic 拿到的行号和列号信息回填给模型时记得同时附上当前工程的类型上下文否则模型无法根据错误信息定位问题根源。4.3 错误回喂与多轮修复错误回喂的提示词是最容易见效的修复方式。把编译器的原始错误信息、出错行以及出错位置的源码一起发给模型让它自查并重新生成。编译出现了以下错误 - 第 23 行找不到符号 UserService - 第 27 行类型不兼容无法将 ResultObject 转换为 ResultUser 请根据错误重新输出完整的实现类代码。不要解释只输出代码。我建议最多重试三次。根据实际效果第一次修复能解决大约一半错误第二次能解决剩下的一半第三次开始基本就是浪费时间了直接由人工介入更高效。4.4 避免循环生成的细节注解处理器的编译是分轮进行的。第一轮扫描到JGeneratedApi生成实现类第二轮这些新生成的实现类又会进入编译扫描。如果不加控制处理器可能在第三轮再次生成同一个文件最后抛Attempt to recreate a file for type异常。解决办法很简单用一个 Map 记录每个目标类对应的生成文件路径处理前先查缓存找到就直接跳过。同时配合RoundEnvironment.processingOver()判断当前是最后一轮还是中间轮。if (roundEnv.processingOver()) { return false; } if (generatedCache.containsKey(apiElement)) { return false; }5. 常见问题与排查技巧实录5.1 “Attempt to recreate a file”怎么办这是注解处理器开发里出现概率最高的问题。编译器在一个编译轮次内发现你尝试创建同一个源文件两次就会抛这个异常。原因一般有两种一是没有做缓存检查二是同一个TypeElement被多次传入JGeneratedApi。排查时先看生成的代码文件路径是不是完全一致再看 Processor 里有没有在process方法开头做防重处理。绝对路径、包名、类名完全一致就是重复创建的充分条件。5.2 泛型信息获取不完整有时候TypeMirror.toString()返回java.util.List而不是java.util.ListUser。这通常出现在获取父类方法时没有经过Types.asMemberOf处理。父类里定义的是T子类把它具体化成User但直接读父类方法签名拿到的依然是T。我建议在提取方法签名的地方统一调用asMemberOf方法避免这类问题。5.3 LLM 回复被 Markdown 代码块污染如果直接对 LLM 返回的字符串调用openWriter().write(...)大概率会写出一堆包含java的无效源码。我写了一个简单的提取方法先把代码块首尾的标记行剥掉再检查剩下内容里是否包含class关键字否则就不写入。private String extractJavaCode(String rawReply) { String code rawReply.replaceAll([a-zA-Z]*, ).trim(); return code; }5.4 AI 代码风格与工程不一致单个类的风格一致性对编译不产生影响但会让代码评审的人非常痛苦。用两个办法解决一是在提示词里写清楚当前工程的风格约定二是提供一段由工程中已有代码构成的 few-shot 示例。示例不在多一个方法就够效果远比十条文字要求好。6. 落地效果与实际心得6.1 在模拟项目 X 上的表现我在一个模拟项目 X 上做了完整的验证目标是生成一套接口实现层共涉及 12 个接口。用最原始的“直接让 DeepSeek 写”的方式能一次通过编译的只有 4 个接上 jlang-gen 的类型上下文后第一轮能通过编译的达到 10 个经过一轮错误回喂之后12 个全部通过编译。通过编译不代表业务逻辑完全正确但至少消灭了“类型瞎编”这一类低级问题剩下的就是业务细节的对齐。6.2 一个提高成功率的关键习惯我实际使用中总结出一条很管用的经验让 LLM 只写方法体不要让它发明方法签名。把方法名、参数列表、返回类型的定义权牢牢握在上下文提取器手里模型只需要根据需求填充逻辑。这样等于把确定性的部分从概率输出中剥离出来了自然会更可靠。后来我在 PromptBuilder 里明确要求“只输出方法体不要重复方法签名”生成正确率又上了一个台阶。这套实现的方向还可以继续扩展。下一步我准备把它做成一个 Gradle 插件让新项目通过一个插件声明就能接入而不是每个工程都手写一套注解处理器配置。谁有类似的类型感知生成需求我们可以多交流。

相关新闻

强化学习在炼钢动态调度中的工业落地实践

强化学习在炼钢动态调度中的工业落地实践

简介:本资源是一套面向工业智能优化领域的强化学习实践方案,聚焦炼钢生产过程中的动态调度难题,适用于具备Python基础与机器学习背景的算法工程师、自动化专业研究生及智能制造方向研究者。资源通过构建马尔可夫决策过程建模炼钢工序时序约束…

2026/10/13 18:38:41 阅读更多 →
React Native鸿蒙开发实战:模拟智能音响从搭建到调试

React Native鸿蒙开发实战:模拟智能音响从搭建到调试

做鸿蒙应用开发一段时间后,身边问得最多的问题往往不是ArkUI怎么用,而是“React Native能不能跑鸿蒙”。我的答案是能,但工程搭建和调试链路比起普通Android确实要绕一点。为了把这条链路彻底摸透,我给自己找了一个不大不小的练习…

2026/10/13 18:38:41 阅读更多 →
告别文献堆砌:用AI工具高效完成文献综述的实战流程

告别文献堆砌:用AI工具高效完成文献综述的实战流程

写文献综述写到崩溃,多半不是读得不够多,而是读完后不知道怎么安放这些文献。我见过太多人把综述写成“某某认为……某某发现……”的流水账,最后被导师批一句“这只是文献堆砌,不是综述”。我自己也踩过这个坑,直到用…

2026/10/13 18:38:41 阅读更多 →

最新新闻

Go监听MySQL binlog实战:轻量级数据变更捕获与生产避坑指南

Go监听MySQL binlog实战:轻量级数据变更捕获与生产避坑指南

1. 这不是“监听MySQL”,而是重建一套轻量级数据变更管道很多人第一次看到“用Go监听binlog”这个说法,第一反应是:“MySQL自己不就有mysqlbinlog命令吗?再不济写个Python脚本调用一下不就完了?”——我试过&#xff0…

2026/10/13 19:13:29 阅读更多 →
生成高质量技术博客:标题、关键词与摘要素材准备指南

生成高质量技术博客:标题、关键词与摘要素材准备指南

我注意到您提供的输入内容中,【项目标题】、【项目正文】、【关键词】和【摘要描述】均为空,暂时无法开始创作。 请按以下格式补齐输入信息,我会立即为您生成一篇独立、完整、可直接发布的博客文章: 项目标题: (例如…

2026/10/13 19:13:29 阅读更多 →
STM32 3D打印机固件:从寄存器级运动控制到S曲线实时规划

STM32 3D打印机固件:从寄存器级运动控制到S曲线实时规划

简介:这是一份面向嵌入式开发者与3D打印技术爱好者的STM32平台开源固件资源,聚焦于解决3D打印机底层运动控制、G代码实时解析与脱机打印等核心问题,适用于希望深入理解固件架构、定制打印逻辑或开展硬件协同开发的学习者与工程师。压缩包共19…

2026/10/13 19:13:29 阅读更多 →
单片机硬件电路设计实战:从最小系统到PCB布局的避坑指南

单片机硬件电路设计实战:从最小系统到PCB布局的避坑指南

1. 从一块“点不亮”的板子说起:单片机硬件电路设计的核心逻辑搞单片机开发的人,几乎都经历过这样一个场景:代码在仿真器里跑得飞起,逻辑时序看着都对,可一旦把程序烧进真实的PCB板子,要么上电没反应&#…

2026/10/13 19:13:29 阅读更多 →
Windows下MinGW-w64稳态配置:零依赖静态编译实战指南

Windows下MinGW-w64稳态配置:零依赖静态编译实战指南

1. 为什么在 Windows 上还要折腾 MinGW-w64?这真不是“复古怀旧”你可能刚接触 C/C 开发,或者从 Visual Studio 转过来,看到“MinGW-w64”这个词第一反应是:这不就是老古董吗?VS 2022 不香吗?WSL2 不快吗&a…

2026/10/13 19:13:29 阅读更多 →
Java+SSM+Flask众惠商城系统:课设毕设全流程交付解析

Java+SSM+Flask众惠商城系统:课设毕设全流程交付解析

先坦白讲,这类“商城系统”的交付包在市面上多如牛毛,但真正能让人从头跑到尾、还能弄明白底层逻辑的其实不多。标题里这个“JavaSSMFlask众惠商城系统”,既包含经典的企业级开发框架SSM,又混搭了Python系的Flask,第一…

2026/10/13 19:12:28 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →