在代码生成类系统中,用户修改已生成代码时,直接覆盖原代码易导致误操作。本文将分享一套可落地、高扩展性的代码修改预览确认方案------用户修改代码后先展示差异,确认无误后再合并到最终代码,核心基于Spring Boot + java-diff-utils实现,兼顾易用性与企业级开发规范。
一、需求背景与核心目标
1.1 业务痛点
- 直接修改并覆盖生成的代码,无确认环节,易产生误操作;
- 用户无法直观感知修改内容,排查问题成本高;
- 缺乏标准化的修改确认流程,系统稳定性难以保障。
1.2 核心目标
- 支持代码修改后的差异可视化展示(增/删/改行标注);
- 实现"预览→确认→合并"的闭环流程,用户确认后才更新最终代码;
- 方案具备可扩展性,适配不同存储方式、前端展示形式。
二、技术选型与整体架构
2.1 技术栈选型
| 组件/框架 | 选型 | 选型理由 |
|---|---|---|
| 后端框架 | Spring Boot 2.7.x | 企业级主流Web框架,简化配置、内置Tomcat,适配生产环境部署 |
| 差异计算库 | java-diff-utils 4.12 | 轻量级、高性能的Java差异对比库,支持行级/字符级差异计算,API友好 |
| 前端交互 | 原生HTML/JS + CSS | 极简交互逻辑,可无缝替换为Vue/React等框架 |
| 数据存储 | 内存HashMap(演示)/Redis/MySQL | 演示用内存存储,生产可无缝切换为持久化存储 |
| 模板引擎 | Thymeleaf | Spring Boot官方推荐,天然支持HTML模板渲染,适配服务端渲染场景 |
2.2 整体架构流程
否
是
用户访问代码修改页面
加载当前最终代码
用户编辑代码并提交预览
后端计算代码差异
前端格式化展示差异(增绿/删红)
用户确认?
后端合并修改到最终代码
更新存储并返回成功结果
前端刷新最终代码状态
三、落地实现步骤(完整可运行)
3.1 环境准备
- JDK:1.8+(兼容主流生产环境)
- 构建工具:Maven 3.6+
- 开发工具:IntelliJ IDEA/Eclipse
- 依赖配置(pom.xml):
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.code.preview</groupId>
<artifactId>code-preview-service</artifactId>
<version>1.0.0</version>
<name>CodePreviewService</name>
<dependencies>
<!-- Spring Boot Web核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 差异计算核心库 -->
<dependency>
<groupId>io.github.java-diff-utils</groupId>
<artifactId>java-diff-utils</artifactId>
<version>4.12</version>
</dependency>
<!-- 可选:参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 可选:Lombok简化实体类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2 核心数据模型设计
定义代码存储实体类,区分"原始代码"(初始化后不变)和"最终代码"(用户确认后更新),适配多版本管理需求:
java
package com.code.preview.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 代码存储实体
* @author 技术实战派
* @date 2026-02
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CodeEntity {
/** 原始代码(初始化后不修改) */
private String originalCode;
/** 最终代码(用户确认合并后更新) */
private String finalCode;
}
3.3 核心业务逻辑实现
3.3.1 全局存储管理(模拟生产存储)
通过静态HashMap实现内存存储,生产环境可替换为Redis/MySQL:
java
package com.code.preview.constant;
import com.code.preview.entity.CodeEntity;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 代码存储常量类(模拟生产存储)
* 生产环境可替换为RedisTemplate/MyBatis Mapper
*/
public class CodeStorageConstant {
/** 内存存储:key=代码ID,value=代码实体 */
public static final Map<String, CodeEntity> CODE_STORAGE = new HashMap<>();
/** 初始化示例代码 */
public static final String INIT_DEMO_CODE = """
public class CalculateDemo {
// 计算两数之和
public static int sum(int a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(sum(1, 2));
}
}
""";
/** 初始化默认代码ID */
public static final String DEFAULT_CODE_ID = UUID.randomUUID().toString();
// 静态初始化:加载默认代码
static {
CODE_STORAGE.put(DEFAULT_CODE_ID, new CodeEntity(INIT_DEMO_CODE, INIT_DEMO_CODE));
}
}
3.3.2 核心控制器(处理预览/合并逻辑)
封装差异计算、合并核心逻辑,暴露REST接口供前端调用:
java
package com.code.preview.controller;
import com.code.preview.constant.CodeStorageConstant;
import com.code.preview.entity.CodeEntity;
import difflib.Delta;
import difflib.DiffUtils;
import difflib.Patch;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 代码预览合并核心控制器
*/
@Slf4j
@Controller
@RequestMapping("/code")
public class CodePreviewController {
/**
* 首页:加载代码修改页面
*/
@GetMapping("/edit")
public String codeEditPage(Model model) {
// 加载默认代码ID和最终代码
String defaultCodeId = CodeStorageConstant.DEFAULT_CODE_ID;
CodeEntity codeEntity = CodeStorageConstant.CODE_STORAGE.get(defaultCodeId);
model.addAttribute("codeId", defaultCodeId);
model.addAttribute("currentCode", codeEntity.getFinalCode());
return "code-edit"; // 对应templates/code-edit.html
}
/**
* 预览代码差异
* @param requestData 请求参数:codeId(代码ID)、modifiedCode(修改后代码)
* @return 差异结果 + 修改后代码
*/
@PostMapping("/preview-diff")
@ResponseBody
public Map<String, Object> previewCodeDiff(@RequestBody Map<String, String> requestData) {
Map<String, Object> result = new HashMap<>(4);
String codeId = requestData.get("codeId");
String modifiedCode = requestData.get("modifiedCode");
// 1. 参数校验
if (Objects.isNull(codeId) || codeId.isEmpty()) {
result.put("success", false);
result.put("errorMsg", "代码ID不能为空");
return result;
}
if (!CodeStorageConstant.CODE_STORAGE.containsKey(codeId)) {
result.put("success", false);
result.put("errorMsg", "代码ID不存在");
return result;
}
// 2. 获取当前最终代码(对比基准)
CodeEntity codeEntity = CodeStorageConstant.CODE_STORAGE.get(codeId);
String currentFinalCode = codeEntity.getFinalCode();
// 3. 按行拆分代码,计算差异(行级对比)
List<String> currentLines = Arrays.asList(currentFinalCode.split("\\r?\\n"));
List<String> modifiedLines = Arrays.asList(modifiedCode.split("\\r?\\n"));
Patch<String> diffPatch = DiffUtils.diff(currentLines, modifiedLines);
// 4. 格式化差异结果(适配前端展示)
StringBuilder diffResult = new StringBuilder();
diffResult.append("【当前代码】 vs 【修改后代码】\n");
diffResult.append("=============================\n");
for (Delta<String> delta : diffPatch.getDeltas()) {
switch (delta.getType()) {
case INSERT: // 新增行
diffResult.append("+ ");
diffResult.append(String.join("\n+ ", delta.getTarget().getLines()));
break;
case DELETE: // 删除行
diffResult.append("- ");
diffResult.append(String.join("\n- ", delta.getSource().getLines()));
break;
case CHANGE: // 修改行(先删后增)
diffResult.append("- ");
diffResult.append(String.join("\n- ", delta.getSource().getLines()));
diffResult.append("\n+ ");
diffResult.append(String.join("\n+ ", delta.getTarget().getLines()));
break;
default:
break;
}
diffResult.append("\n");
}
// 无差异处理
if (diffPatch.getDeltas().isEmpty()) {
diffResult.append("无修改内容");
}
// 5. 返回结果
result.put("success", true);
result.put("diffContent", diffResult.toString());
result.put("modifiedCode", modifiedCode);
log.info("代码差异计算完成,codeId:{}", codeId);
return result;
}
/**
* 确认合并修改后的代码
* @param requestData 请求参数:codeId、modifiedCode
* @return 合并结果
*/
@PostMapping("/confirm-merge")
@ResponseBody
public Map<String, Object> confirmCodeMerge(@RequestBody Map<String, String> requestData) {
Map<String, Object> result = new HashMap<>(4);
String codeId = requestData.get("codeId");
String modifiedCode = requestData.get("modifiedCode");
// 1. 参数校验
if (Objects.isNull(codeId) || codeId.isEmpty()) {
result.put("success", false);
result.put("errorMsg", "代码ID不能为空");
return result;
}
if (!CodeStorageConstant.CODE_STORAGE.containsKey(codeId)) {
result.put("success", false);
result.put("errorMsg", "代码ID不存在");
return result;
}
// 2. 更新最终代码
CodeEntity codeEntity = CodeStorageConstant.CODE_STORAGE.get(codeId);
codeEntity.setFinalCode(modifiedCode);
log.info("代码合并完成,codeId:{}", codeId);
// 3. 返回结果
result.put("success", true);
result.put("finalCode", modifiedCode);
result.put("msg", "代码合并成功");
return result;
}
}
3.4 前端页面实现(交互+可视化)
实现代码编辑、差异预览、确认合并的交互逻辑,重点优化差异展示的可视化效果:
html
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>代码修改预览确认 | Spring Boot实战</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: "Microsoft YaHei", sans-serif; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.title { margin-bottom: 20px; color: #333; }
.code-editor { width: 100%; height: 300px; padding: 15px; border: 1px solid #e6e6e6;
font-family: Consolas, monospace; font-size: 14px; line-height: 1.5;
resize: vertical; margin-bottom: 15px; }
.btn-group { margin-bottom: 15px; }
.btn { padding: 8px 20px; border: none; border-radius: 4px; cursor: pointer;
margin-right: 10px; font-size: 14px; }
.btn-preview { background-color: #4299e1; color: #fff; }
.btn-confirm { background-color: #48bb78; color: #fff; }
.btn:disabled { background-color: #ccc; cursor: not-allowed; }
.diff-title { margin: 15px 0 10px; color: #333; font-size: 16px; }
.diff-container { width: 100%; min-height: 200px; padding: 15px; border: 1px solid #e6e6e6;
background-color: #f8f9fa; font-family: Consolas, monospace;
font-size: 14px; white-space: pre-wrap; line-height: 1.5; }
.diff-add { color: #10b981; } /* 新增行绿色 */
.diff-del { color: #ef4444; } /* 删除行红色 */
.success-msg { color: #10b981; font-size: 14px; margin-top: 10px; display: none; }
.error-msg { color: #ef4444; font-size: 14px; margin-top: 10px; display: none; }
</style>
</head>
<body>
<div class="container">
<h1 class="title">代码修改预览确认</h1>
<!-- 隐藏域存储代码ID -->
<input type="hidden" id="codeId" th:value="${codeId}">
<!-- 代码编辑区域 -->
<textarea class="code-editor" id="codeEditor" th:text="${currentCode}"></textarea>
<!-- 操作按钮 -->
<div class="btn-group">
<button class="btn btn-preview" onclick="previewDiff()">预览修改差异</button>
<button class="btn btn-confirm" onclick="confirmMerge()" disabled id="confirmBtn">确认合并</button>
</div>
<!-- 差异展示区域 -->
<h3 class="diff-title">修改差异预览</h3>
<div class="diff-container" id="diffContainer">未预览修改,请点击「预览修改差异」查看</div>
<!-- 结果提示 -->
<div class="success-msg" id="successMsg">✅ 代码合并成功!</div>
<div class="error-msg" id="errorMsg">❌ 操作失败,请重试!</div>
</div>
<script>
// 暂存修改后的代码
let modifiedCode = '';
/**
* 预览差异
*/
function previewDiff() {
const codeId = document.getElementById('codeId').value;
modifiedCode = document.getElementById('codeEditor').value;
// 清空之前的提示
hideAllMsg();
fetch('/code/preview-diff', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
codeId: codeId,
modifiedCode: modifiedCode
})
}).then(res => res.json())
.then(data => {
if (data.success) {
// 格式化差异内容(给增/删行加样式)
let diffHtml = data.diffContent
.replace(/^\+/gm, '<span class="diff-add">+</span>')
.replace(/^\-/gm, '<span class="diff-del">-</span>');
document.getElementById('diffContainer').innerHTML = diffHtml;
// 启用确认按钮
document.getElementById('confirmBtn').disabled = false;
} else {
document.getElementById('diffContainer').textContent = data.errorMsg;
showErrorMsg();
}
}).catch(err => {
console.error('预览差异失败:', err);
showErrorMsg();
});
}
/**
* 确认合并代码
*/
function confirmMerge() {
const codeId = document.getElementById('codeId').value;
// 清空之前的提示
hideAllMsg();
fetch('/code/confirm-merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
codeId: codeId,
modifiedCode: modifiedCode
})
}).then(res => res.json())
.then(data => {
if (data.success) {
showSuccessMsg();
// 禁用确认按钮
document.getElementById('confirmBtn').disabled = true;
} else {
showErrorMsg(data.errorMsg || '合并失败');
}
}).catch(err => {
console.error('合并代码失败:', err);
showErrorMsg();
});
}
/**
* 显示成功提示
*/
function showSuccessMsg() {
document.getElementById('successMsg').style.display = 'block';
document.getElementById('errorMsg').style.display = 'none';
}
/**
* 显示错误提示
* @param msg 错误信息
*/
function showErrorMsg(msg) {
const errorDom = document.getElementById('errorMsg');
errorDom.textContent = `❌ ${msg || '操作失败,请重试!'}`;
errorDom.style.display = 'block';
document.getElementById('successMsg').style.display = 'none';
}
/**
* 隐藏所有提示
*/
function hideAllMsg() {
document.getElementById('successMsg').style.display = 'none';
document.getElementById('errorMsg').style.display = 'none';
}
</script>
</body>
</html>
3.5 启动类
java
package com.code.preview;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用启动类
*/
@SpringBootApplication
public class CodePreviewApplication {
public static void main(String[] args) {
SpringApplication.run(CodePreviewApplication.class, args);
}
}
四、测试验证
4.1 启动应用
运行CodePreviewApplication,访问http://localhost:8080/code/edit,进入代码编辑页面。
4.2 核心流程验证
- 修改代码 :将示例代码中的
return a + b改为return a * b; - 预览差异:点击「预览修改差异」,可看到红色删除行(原加法逻辑)、绿色新增行(乘法逻辑);
- 确认合并:点击「确认合并」,提示"代码合并成功",此时最终代码已更新;
- 二次预览:再次修改代码,预览差异的基准变为合并后的最新代码。
五、生产环境适配优化
5.1 存储层优化
将内存HashMap替换为Redis/MySQL,支持分布式部署和数据持久化:
- Redis:适合高频访问场景,用
String类型存储codeId -> 序列化CodeEntity; - MySQL:适合需要长期存储、版本回溯的场景,设计表结构如下:
sql
CREATE TABLE `code_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`code_id` varchar(64) NOT NULL COMMENT '代码唯一标识',
`original_code` text NOT NULL COMMENT '原始代码',
`final_code` text NOT NULL COMMENT '最终代码',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code_id` (`code_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码信息表';
5.2 异常处理优化
添加全局异常处理器,统一返回格式:
java
package com.code.preview.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Map<String, Object> handleException(Exception e) {
log.error("系统异常", e);
Map<String, Object> result = new HashMap<>(2);
result.put("success", false);
result.put("errorMsg", "系统异常,请联系管理员");
return result;
}
}
5.3 性能与安全优化
- 大代码段处理 :差异计算改为异步(
@Async),避免阻塞请求; - 参数校验增强 :使用
@Valid+jakarta.validation校验请求参数; - XSS防护:对用户输入的代码进行XSS过滤,避免前端注入;
- 接口鉴权:集成Spring Security/Spring Cloud Gateway,添加接口访问权限控制。
5.4 可视化增强
替换原生差异展示,集成专业前端差异库:
- diff2html:支持HTML格式的差异展示,自带语法高亮;
- CodeMirror:支持代码语法高亮、行号、代码折叠,提升编辑体验。
六、总结
本文基于Spring Boot实现的代码修改预览确认方案,核心亮点如下:
- 流程闭环:完整实现"编辑→预览→确认→合并"的代码修改流程,避免误操作;
- 易落地:代码结构清晰,依赖少,可直接复用核心逻辑到生产系统;
- 高扩展:存储层、前端展示层均可按需替换,适配不同业务场景。
该方案不仅适用于代码生成系统,还可扩展到配置文件修改、文本内容编辑等需要"预览确认"的场景,是企业级应用中提升用户体验、降低操作风险的通用解决方案。