实战落地:基于Spring Boot实现代码修改预览确认功能

在代码生成类系统中,用户修改已生成代码时,直接覆盖原代码易导致误操作。本文将分享一套可落地、高扩展性的代码修改预览确认方案------用户修改代码后先展示差异,确认无误后再合并到最终代码,核心基于Spring Boot + java-diff-utils实现,兼顾易用性与企业级开发规范。

一、需求背景与核心目标

1.1 业务痛点

  • 直接修改并覆盖生成的代码,无确认环节,易产生误操作;
  • 用户无法直观感知修改内容,排查问题成本高;
  • 缺乏标准化的修改确认流程,系统稳定性难以保障。

1.2 核心目标

  1. 支持代码修改后的差异可视化展示(增/删/改行标注);
  2. 实现"预览→确认→合并"的闭环流程,用户确认后才更新最终代码;
  3. 方案具备可扩展性,适配不同存储方式、前端展示形式。

二、技术选型与整体架构

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 核心流程验证

  1. 修改代码 :将示例代码中的return a + b改为return a * b
  2. 预览差异:点击「预览修改差异」,可看到红色删除行(原加法逻辑)、绿色新增行(乘法逻辑);
  3. 确认合并:点击「确认合并」,提示"代码合并成功",此时最终代码已更新;
  4. 二次预览:再次修改代码,预览差异的基准变为合并后的最新代码。

五、生产环境适配优化

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 性能与安全优化

  1. 大代码段处理 :差异计算改为异步(@Async),避免阻塞请求;
  2. 参数校验增强 :使用@Valid + jakarta.validation校验请求参数;
  3. XSS防护:对用户输入的代码进行XSS过滤,避免前端注入;
  4. 接口鉴权:集成Spring Security/Spring Cloud Gateway,添加接口访问权限控制。

5.4 可视化增强

替换原生差异展示,集成专业前端差异库:

  • diff2html:支持HTML格式的差异展示,自带语法高亮;
  • CodeMirror:支持代码语法高亮、行号、代码折叠,提升编辑体验。

六、总结

本文基于Spring Boot实现的代码修改预览确认方案,核心亮点如下:

  1. 流程闭环:完整实现"编辑→预览→确认→合并"的代码修改流程,避免误操作;
  2. 易落地:代码结构清晰,依赖少,可直接复用核心逻辑到生产系统;
  3. 高扩展:存储层、前端展示层均可按需替换,适配不同业务场景。

该方案不仅适用于代码生成系统,还可扩展到配置文件修改、文本内容编辑等需要"预览确认"的场景,是企业级应用中提升用户体验、降低操作风险的通用解决方案。

相关推荐
Hui Baby14 小时前
springAi+MCP三种
java
hsjcjh14 小时前
【MySQL】C# 连接MySQL
java
敖正炀14 小时前
LinkedBlockingDeque详解
java
wangyadong31714 小时前
datagrip 链接mysql 报错
java
untE EADO14 小时前
Tomcat的server.xml配置详解
xml·java·tomcat
ictI CABL14 小时前
Tomcat 乱码问题彻底解决
java·tomcat
敖正炀14 小时前
DelayQueue 详解
java
uzong14 小时前
最新:阿里正式发布首款AI开发工具Meoo(秒悟),0门槛、一键部署上线
人工智能·后端
用户83562907805115 小时前
Python 操作 PowerPoint:添加与设置文本框完整教程
后端·python
HuaidongLi15 小时前
三级缓存与循环依赖
后端