实战落地:基于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. 高扩展:存储层、前端展示层均可按需替换,适配不同业务场景。

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

相关推荐
青云计划7 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿7 小时前
Jsoniter(java版本)使用介绍
java·开发语言
Victor3567 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor3567 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
探路者继续奋斗8 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
消失的旧时光-19439 小时前
第十九课:为什么要引入消息队列?——异步系统设计思想
java·开发语言
yeyeye1119 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
A懿轩A9 小时前
【Java 基础编程】Java 面向对象入门:类与对象、构造器、this 关键字,小白也能写 OOP
java·开发语言
Tony Bai9 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
乐观勇敢坚强的老彭9 小时前
c++寒假营day03
java·开发语言·c++