跳到主要内容
Spring AI + IDEA 插件:定制化 Java 代码助手实战 | 极客日志
Java VScode AI java
Spring AI + IDEA 插件:定制化 Java 代码助手实战 基于 Spring AI 和 IDEA 插件实现了一套面向 Java 项目的定制化代码助手。后端结合 Spring Boot、JavaParser、Maven API 解析项目上下文并完成代码生成、语法校验和性能优化,前端通过 IDEA 插件提供对话窗口、结果展示与一键插入代码能力。文章同时展示了 Prompt 设计、代码补全联动、插件打包发布以及实际踩坑后的处理方式,核心结论是:AI 代码助手要真正可用,关键不在模型本身,而在上下文采集、约束和校验这三步。
前言
做 Java 开发的人,大多都逃不开重复写 CRUD、改包名、补 import、盯着编译错误一条条修。通用 AI 代码助手当然能提速,但一旦进到具体项目里,包结构、依赖版本、表结构这些上下文对不上,生成结果往往还是要手工重来一遍。
这次我做的是一套偏'项目内可用'的代码助手:后端用 Spring AI 负责模型调用和提示词编排,再配上 JavaParser、Maven API 做项目解析;前端放进 IDEA 插件里,提供对话窗口和一键插入代码。它主要解决三件事:按需求生成完整代码、对已有代码做优化、结合当前项目上下文给出补全建议。
一、项目背景与架构设计
1.1 项目定位与核心需求
这套工具的定位很明确:围绕 Java 项目本身做代码生成,而不是做一个泛用聊天框。前端是 IDEA 插件,后端是 Spring Boot 服务,插件负责采集上下文和交互,后端负责理解需求、拼 Prompt、调模型、做校验。
维度 核心需求 技术挑战 代码生成 输入需求描述,生成 Controller+Service+Mapper 完整代码 Spring AI 的 Prompt 设计、Java 语法合规性校验 代码优化 自动修复语法错误、优化性能(如 SQL 优化、循环优化) JavaParser 解析代码 AST、Spring AI 调用大模型分析 上下文感知 感知当前项目的包结构、依赖、数据库表结构 IDEA 插件获取项目上下文、后端存储上下文信息 交互体验 IDEA 内对话窗口、一键插入生成的代码 IDEA 插件 Swing 开发、前后端通信协议设计
1.2 整体架构设计
1.3 技术栈选型
选型基本是顺着 Java 生态往下走,没刻意追新。后端和插件都要稳定,能少踩一点兼容性坑就少踩一点。
技术领域 选型 选型理由 后端核心 Spring Boot 3.2 + Spring AI 0.8.1 Spring AI 原生适配 Spring 生态,支持多模型统一调用 代码解析 JavaParser 3.25.10 轻量、高效的 Java 代码 AST 解析库,支持代码生成 / 校验 Maven 交互 Maven API 3.9.6 解析项目 pom.xml,获取依赖与包结构 IDEA 插件 IntelliJ Platform SDK 2023.2 官方 SDK,支持 IDEA 插件全功能开发 前端交互 Swing + OkHttp 4.12.0 Swing 实现 IDEA 内窗口,OkHttp 实现前后端通信 大模型 GPT-4 + 通义千问(可选) GPT-4 代码生成质量高,通义千问支持私有化部署 存储 MySQL 8.0 + Redis 7.0 存储项目上下文、生成的代码片段
二、核心技术架构拆解
2.1 后端核心:Spring AI + 工具调用体系
2.1.1 Spring AI 核心配置 先把模型客户端接起来,后面的 Prompt、工具调用、结果校验才有落点。
@Configuration
public class SpringAiConfig {
@Bean
public OpenAiChatClient openAiChatClient () {
String apiKey = System.getenv("OPENAI_API_KEY" );
String baseUrl = "https://api.openai.com/v1" ;
OpenAiApi openAiApi = new OpenAiApi (baseUrl, apiKey);
OpenAiChatClient client = new OpenAiChatClient (openAiApi);
client.setTemperature(0.2 );
client.setModel("gpt-4" );
return client;
}
}
2.1.2 工具调用层:JavaParser + Maven API 真正让它和'普通聊天机器人'拉开差距的,是这层上下文解析。光靠模型猜,生成结果很容易飘;把 pom、包结构、源码目录这些信息喂进去,代码才更像是给当前项目现写的。
@Service
public class ProjectContextParser {
public ProjectContext parseMavenProject (String pomPath) throws Exception {
ProjectContext context = new ProjectContext ();
File pomFile = new File (pomPath);
MavenXpp3Reader reader = new MavenXpp3Reader ();
Model model = reader.read(new FileReader (pomFile));
context.setGroupId(model.getGroupId());
context.setArtifactId(model.getArtifactId());
context.setBasePackage(model.getGroupId() + "." + model.getArtifactId());
List<String> dependencies = new ArrayList <>();
for (Dependency dep : model.getDependencies()) {
dependencies.add(dep.getGroupId() + ":" + dep.getArtifactId() + ":" + dep.getVersion());
}
context.setDependencies(dependencies);
File srcDir = new File (pomFile.getParentFile(), "src/main/java" );
if (srcDir.exists()) {
context.setSrcRootPath(srcDir.getAbsolutePath());
List<String> packages = parsePackages(srcDir);
context.setPackages(packages);
}
return context;
}
private List<String> parsePackages (File srcDir) {
List<String> packages = new ArrayList <>();
File[] files = srcDir.listFiles();
if (files == null ) return packages;
for (File file : files) {
if (file.isDirectory()) {
String packageName = file.getAbsolutePath().replace(srcDir.getAbsolutePath(), "" )
.replace(File.separator, "." );
if (!packageName.isEmpty()) {
packages.add(packageName.substring(1 ));
}
packages.addAll(parsePackages(file));
}
}
return packages;
}
public boolean validateJavaCode (String code) {
try {
CompilationUnit cu = StaticJavaParser.parse(code);
List<Problem> problems = cu.getProblems();
return problems.isEmpty();
} catch (Exception e) {
return false ;
}
}
}
@Data
public class ProjectContext {
private String groupId;
private String artifactId;
private String basePackage;
private List<String> dependencies;
private List<String> packages;
private String srcRootPath;
private String projectId;
}
2.2 前端核心:IDEA 插件开发基础
2.2.1 IDEA 插件工程搭建 插件侧没有做太多花活,先把入口、窗口和快捷键跑通。IDEA 插件开发最容易浪费时间的地方,不是业务逻辑,而是各种生命周期、上下文和 UI 线程问题。
<idea-plugin >
<id > com.ai.code.assistant</id >
<name > AI Code Assistant</name >
<version > 1.0</version >
<vendor email ="[email protected] " url ="https://your.site" > Your Name</vendor >
<description >
基于 Spring AI 的 Java 代码助手,支持上下文感知的代码生成与优化
</description >
<actions >
<action text ="AI Code Assistant" description ="Open AI Code Assistant Dialog" >
<add-to-group group-id ="EditorPopupMenu" anchor ="first" />
<keyboard-shortcut keymap ="$default" first-keystroke ="ctrl alt A" />
</action >
</actions >
<extensions defaultExtensionNs ="com.intellij" >
<toolWindow anchor ="right" factoryClass ="com.ai.code.assistant.window.AiToolWindowFactory" />
</extensions >
</idea-plugin >
2.2.2 对话窗口开发(Swing) 窗口本身不复杂,重点是:别阻塞编辑器,别让插入代码时把当前光标位置和选区搞乱。
public class AiCodeDialog extends JDialog {
private JTextArea inputArea;
private JTextPane resultArea;
private JButton generateBtn;
private JButton insertBtn;
private Project currentProject;
public AiCodeDialog (Project project) {
super (WindowManager.getInstance().getFrame(project), "AI Code Assistant" , Dialog.ModalityType.MODELESS);
this .currentProject = project;
initUI();
setSize(800 , 600 );
setLocationRelativeTo(null );
}
private void initUI () {
inputArea = new JTextArea (5 , 50 );
inputArea.setPlaceholder("请输入代码生成需求,例如:生成用户管理的 Controller+Service+Mapper" );
JScrollPane inputScroll = new JScrollPane (inputArea);
resultArea = new JTextPane ();
resultArea.setContentType("text/java" );
JScrollPane resultScroll = new JScrollPane (resultArea);
generateBtn = new JButton ("生成代码" );
insertBtn = new JButton ("插入到编辑器" );
insertBtn.setEnabled(false );
JPanel panel = new JPanel (new BorderLayout ());
JPanel topPanel = new JPanel (new BorderLayout ());
topPanel.add(new JLabel ("需求描述:" ), BorderLayout.NORTH);
topPanel.add(inputScroll, BorderLayout.CENTER);
JPanel btnPanel = new JPanel ();
btnPanel.add(generateBtn);
btnPanel.add(insertBtn);
panel.add(topPanel, BorderLayout.NORTH);
panel.add(resultScroll, BorderLayout.CENTER);
panel.add(btnPanel, BorderLayout.SOUTH);
generateBtn.addActionListener(e -> generateCode());
insertBtn.addActionListener(e -> insertCodeToEditor());
add(panel);
}
private void generateCode () {
ProjectContext context = collectProjectContext();
CodeGenerateRequest request = new CodeGenerateRequest ();
request.setRequirement(inputArea.getText());
request.setProjectContext(context);
OkHttpClient client = new OkHttpClient ();
resultArea.setText(generatedCode);
insertBtn.setEnabled(true );
}
private ProjectContext collectProjectContext () {
ProjectContext context = new ProjectContext ();
String projectPath = currentProject.getBasePath();
VirtualFile pomFile = currentProject.getBaseDir().findChild("pom.xml" );
if (pomFile != null ) {
context.setPomPath(pomFile.getPath());
}
Editor editor = FileEditorManager.getInstance(currentProject).getSelectedTextEditor();
if (editor != null ) {
PsiFile psiFile = PsiDocumentManager.getInstance(currentProject).getPsiFile(editor.getDocument());
if (psiFile instanceof PsiJavaFile) {
PsiJavaFile javaFile = (PsiJavaFile) psiFile;
context.setCurrentPackage(javaFile.getPackageName());
}
}
context.setProjectId(currentProject.getName());
return context;
}
private void insertCodeToEditor () {
Editor editor = FileEditorManager.getInstance(currentProject).getSelectedTextEditor();
if (editor == null ) return ;
Document document = editor.getDocument();
SelectionModel selectionModel = editor.getSelectionModel();
int start = selectionModel.getSelectionStart();
int end = selectionModel.getSelectionEnd();
WriteCommandAction.runWriteCommandAction(currentProject, () -> {
document.replaceString(start, end, resultArea.getText());
});
selectionModel.removeSelection();
editor.getCaretModel().moveToOffset(start + resultArea.getText().length());
}
}
三、核心功能实现
3.1 代码生成:Controller+Service+Mapper 完整生成
3.1.1 Prompt 工程核心逻辑 Prompt 不只是'把需求塞给模型'这么简单。要让它知道项目基础包、已有包、依赖版本,还要把'只返回代码'这类约束写得足够硬,不然后面处理结果会很烦。
@Service
public class PromptEngineeringService {
public Prompt buildGeneratePrompt (String requirement, ProjectContext context) {
String systemPrompt = "你是一位资深 Java 后端开发工程师,精通 Spring Boot、MyBatis、MySQL。\n" +
"请根据以下需求和项目上下文,生成符合规范的 Java 代码:\n" +
"1. 包结构必须符合项目基础包:%s\n" +
"2. 代码必须兼容项目依赖版本,优先使用项目已引入的依赖\n" +
"3. 生成完整的 Controller+Service+Mapper 层,包含必要的注释、异常处理\n" +
"4. 代码风格符合阿里巴巴 Java 开发手册\n" +
"5. 只返回代码,不返回多余解释\n" +
"项目上下文:\n" +
" - 基础包名:%s\n" +
" - 已存在的包:%s\n" +
" - 项目依赖:%s" ;
String formattedSystemPrompt = String.format(systemPrompt,
context.getBasePackage(),
context.getBasePackage(),
String.join("," , context.getPackages()),
String.join("," , context.getDependencies()));
String userPrompt = "需求:" + requirement;
return new Prompt (List.of(
new SystemMessage (formattedSystemPrompt),
new UserMessage (userPrompt)
));
}
}
3.1.2 代码生成核心接口
@RestController
@RequestMapping("/api/code")
public class CodeGenerateController {
@Autowired
private OpenAiChatClient openAiChatClient;
@Autowired
private PromptEngineeringService promptService;
@Autowired
private ProjectContextParser contextParser;
@Autowired
private ProjectContextRepository contextRepository;
@PostMapping("/generate")
public Result<String> generateCode (@RequestBody CodeGenerateRequest request) {
try {
ProjectContext context;
if (request.getProjectContext().getProjectId() != null ) {
context = contextRepository.findByProjectId(request.getProjectContext().getProjectId());
if (context == null ) {
context = contextParser.parseMavenProject(request.getProjectContext().getPomPath());
context.setProjectId(request.getProjectContext().getProjectId());
contextRepository.save(context);
}
} else {
context = request.getProjectContext();
}
Prompt prompt = promptService.buildGeneratePrompt(request.getRequirement(), context);
AiResponse response = openAiChatClient.generate(prompt);
String generatedCode = response.getGeneration().getText();
boolean isValid = contextParser.validateJavaCode(generatedCode);
if (!isValid) {
generatedCode = regenerateCode(prompt);
}
return Result.success(generatedCode);
} catch (Exception e) {
log.error("生成代码失败" , e);
return Result.error("生成代码失败:" + e.getMessage());
}
}
private String regenerateCode (Prompt prompt) {
Prompt newPrompt = new Prompt (
prompt.getMessages().stream()
.collect(Collectors.toList())
.add(new UserMessage ("以上代码存在语法错误,请修复后重新生成,只返回修复后的代码" ))
);
return openAiChatClient.generate(newPrompt).getGeneration().getText();
}
}
3.1.3 效果示例 用户输入需求:生成用户管理的 Controller+Service+Mapper,包含查询、新增、修改、删除接口。
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> listAll () {
return userService.listAll();
}
@GetMapping("/{id}")
public User getById (@PathVariable Long id) {
return userService.getById(id);
}
@PostMapping
public boolean save (@RequestBody User user) {
return userService.save(user);
}
@PutMapping("/{id}")
public boolean update (@PathVariable Long id, @RequestBody User user) {
user.setId(id);
return userService.update(user);
}
@DeleteMapping("/{id}")
public boolean delete (@PathVariable Long id) {
return userService.delete(id);
}
}
3.2 代码优化:自动修复语法错误与性能问题
3.2.1 代码优化 Prompt 设计 优化这块的重点不是'写得更漂亮',而是先把最常见的问题解决掉:能编译、逻辑不变、性能有实质改善。
public Prompt buildOptimizePrompt (String code, String optimizeType) {
String systemPrompt = "你是一位资深 Java 性能优化工程师,精通 Java 语法、性能调优。\n" +
"请根据指定类型优化以下代码:\n" +
"优化类型:%s\n" +
"优化规则:\n" +
"1. 修复语法错误,保证代码可编译\n" +
"2. 性能优化需给出具体的优化点(如循环优化、SQL 优化、集合使用优化)\n" +
"3. 保留原有业务逻辑,只优化语法和性能\n" +
"4. 输出优化后的代码 + 优化说明(分开展示)" ;
String formattedSystemPrompt = String.format(systemPrompt, optimizeType);
return new Prompt (List.of(
new SystemMessage (formattedSystemPrompt),
new UserMessage ("需要优化的代码:\n" + code)
));
}
3.2.2 代码优化接口实现
@PostMapping("/optimize")
public Result<CodeOptimizeResponse> optimizeCode (@RequestBody CodeOptimizeRequest request) {
try {
Prompt prompt = promptService.buildOptimizePrompt(request.getCode(), request.getOptimizeType());
AiResponse response = openAiChatClient.generate(prompt);
String result = response.getGeneration().getText();
CodeOptimizeResponse responseVO = parseOptimizeResult(result);
return Result.success(responseVO);
} catch (Exception e) {
log.error("优化代码失败" , e);
return Result.error("优化代码失败:" + e.getMessage());
}
}
private CodeOptimizeResponse parseOptimizeResult (String result) {
CodeOptimizeResponse response = new CodeOptimizeResponse ();
String[] parts = result.split("===优化说明===" );
if (parts.length >= 1 ) {
response.setOptimizedCode(parts[0 ].replace("===优化后代码===" , "" ).trim());
}
if (parts.length >= 2 ) {
response.setOptimizeDesc(parts[1 ].trim());
}
return response;
}
3.2.3 优化效果示例 原始代码的毛病很典型:把数据库查询放在循环里,数据一多,IO 次数就直接上去了。
public List<User> listUsers (List<Long> ids) {
List<User> users = new ArrayList <>();
for (Long id : ids) {
User user = userMapper.getById(id);
users.add(user);
}
return users;
}
public List<User> listUsers (List<Long> ids) {
if (CollectionUtils.isEmpty(ids)) {
return Collections.emptyList();
}
return userMapper.listByIds(ids);
}
性能问题:循环遍历 ID 列表,每次查询数据库,导致多次 IO 操作,性能低下;
优化方案:使用 MyBatis 的批量查询方法 listByIds,一次 SQL 查询获取所有数据;
额外优化:增加空值判断,避免空指针异常。
3.3 知识注入:项目上下文感知的代码补全
3.3.1 上下文感知核心逻辑 补全场景比生成完整代码更挑上下文。当前包名、已导入类、光标前的片段,缺一个都容易跑偏。所以这里的策略是尽量把编辑器里能拿到的信息都收进来,再让模型去补。
@Service
public class CodeCompletionService {
public List<String> completeCode (CodeCompletionRequest request) {
String systemPrompt = "请根据当前 Java 文件的上下文,生成代码补全建议:\n" +
"1. 补全建议必须符合当前包结构:%s\n" +
"2. 优先使用已导入的类:%s\n" +
"3. 补全建议简洁,每条不超过 50 个字符\n" +
"4. 只返回补全建议列表,每行一个" ;
String formattedSystemPrompt = String.format(systemPrompt,
request.getCurrentPackage(),
String.join("," , request.getImportedClasses()));
String userPrompt = "需要补全的代码片段:\n" + request.getCodeSnippet();
Prompt prompt = new Prompt (List.of(
new SystemMessage (formattedSystemPrompt),
new UserMessage (userPrompt)
));
AiResponse response = openAiChatClient.generate(prompt);
String result = response.getGeneration().getText();
return Arrays.stream(result.split("\n" ))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}
3.3.2 IDEA 插件端补全联动
public class CodeCompletionListener extends TypedActionHandlerBase {
@Override
public void execute (@NotNull Editor editor, char c, @NotNull DataContext dataContext) {
CaretModel caretModel = editor.getCaretModel();
int offset = caretModel.getOffset();
Document document = editor.getDocument();
String codeSnippet = document.getText(new TextRange (Math.max(0 , offset - 100 ), offset));
Project project = CommonDataKeys.PROJECT.getData(dataContext);
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
CodeCompletionRequest request = new CodeCompletionRequest ();
request.setCodeSnippet(codeSnippet);
if (psiFile instanceof PsiJavaFile) {
PsiJavaFile javaFile = (PsiJavaFile) psiFile;
request.setCurrentPackage(javaFile.getPackageName());
List<String> importedClasses = javaFile.getImportList().getAllImports().stream()
.map(ImportStatement::getQualifiedName)
.collect(Collectors.toList());
request.setImportedClasses(importedClasses);
}
CompletableFuture.runAsync(() -> {
List<String> completions = callCompletionApi(request);
showCompletionSuggestions(editor, completions);
});
}
}
四、实战部署:插件打包与私有仓库发布
4.1 IDEA 插件打包 Gradle 这部分没什么特别的,关键是版本别乱跳,IDEA SDK 和插件依赖一变,后面兼容问题会很烦。
plugins {
id 'java'
id 'org.jetbrains.intellij' version '1.17.3'
}
intellij {
version = '2023.2'
type = 'IC'
plugins = ['java']
}
sourceCompatibility = 17
targetCompatibility = 17
// 打包配置
tasks.buildPlugin {
archiveBaseName = 'ai-code-assistant'
archiveVersion = '1.0.0'
destinationDirectory = file("$projectDir/dist")
}
打完包后,会在 dist 目录里生成 ai-code-assistant-1.0.0.zip。
4.2 私有仓库发布 如果是团队内用,放到私有仓库比手工发包省事得多。Nexus 或 JetBrains Plugin Repository 都能用,流程差不多。
搭建私有插件仓库;
上传插件包;
在 IDEA 里添加仓库地址:http://your-nexus-url/repository/idea-plugins/。
4.3 后端服务部署 mvn clean package -DskipTests
nohup java -jar ai-code-assistant-1.0.0.jar --spring.profiles.active=prod > app.log 2>&1 &
线上服务还是走常规套路:打包、部署、Nginx 反代,没什么新鲜的,但足够稳。
五、实战踩坑与优化方案 问题分类 具体问题 根因 最终解决方案 IDEA 插件 插件启动时获取不到项目上下文 插件加载时机过早,项目未完全初始化 在 projectOpened 事件中初始化上下文采集逻辑 代码生成 AI 生成的代码包名错误 Prompt 中上下文拼接不完整 优化 Prompt,强制 AI 使用项目基础包名,增加校验逻辑 性能问题 代码生成响应慢(>5s) AI 调用 + 上下文解析耗时 1. 缓存项目上下文(Redis);2. 异步生成代码,返回任务 ID 轮询结果 语法校验 JavaParser 校验误判 JavaParser 版本与 IDEA SDK 不兼容 统一使用 IDEA 内置的 Java 解析器(PSI API)替代 JavaParser 插件交互 插入代码时格式错乱 换行符 / 缩进不一致 插入前格式化代码(CodeStyleManager.getInstance(project).reformat(psiElement))
六、总结与进阶规划 这套东西做下来,最直接的感受是:生成能力本身不是难点,难的是让模型输出的东西真的能贴到项目里用。上下文采集、Prompt 约束、语法校验、插入编辑器这几步,哪一步松一点,最后都要靠人工兜底。
后面如果继续做,我会优先补这几块:私有化模型接入、本地知识库、按表结构批量生成模块代码,还有团队内共享 Prompt 的能力。多 IDE 适配也能做,但那是后面的事了,先把 IDEA 这条链路打稳更实际。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online