2022年Java老项目环境重建实录:数据库脚本补全,三天跑通全流程

12 阅读19分钟

老炮踩坑录 · F01续篇 · 翻车现场系列

· 基于「企业融合评估系统」真实源码

· 关键词:多源交叉验证 · 证据链 · 差集检查 · 动态闭环

这是第一篇的续篇。

在第一篇里讲了老项目运行不了的原因,本篇讲如何把它运行起来:在没有数据库脚本的情况下,把一个停更多年的系统完整跑起来,并且成功登录。

我折腾了三天,踩了 7 个大坑,5个小坑。每一个大坑我都附上了真实报错和解决办法,建议您收藏。如果以后接收老项目的时候可以照着排雷。

但这篇文章真正想送给你的,不是 12 个坑的答案,而是一套面对 "缺文档、缺环境、缺人"的烂摊子时,如何用代码本身当作唯一可信的证据源,把系统重建出来的方法论——多源交叉验证、证据链、差集检查、动态闭环,文末有此完整的总结。任何接手遗留系统的人都能用得上。

事情的起因

上篇我们分析了这个项目现有情况:

  • 有完整源码,能编译;
  • 没有建表 SQL,没有数据库文档,没有 README;
  • 配置文件里指向的测试环境域名,DNS 早就解析不到了。

一个 Spring Boot 项目,没有数据库,就是一堆没法启动的类文件。

我们的目标:搞出一份能建库的 SQL,把项目启动起来,能用页面登录进去。

先摸底:这个项目到底用了什么技术

写SQL之前,我先把 pom.xml 翻了一遍,主要内容如下:

依赖版本作用
Spring Boot2.1.0.RELEASE主框架
JDK1.8编译目标
打包方式war可外部容器部署
mybatis-spring-boot-starter2.1.1MyBatis 整合
mybatis-plus-boot-starter3.3.0MyBatis-Plus(和原生 MyBatis 混用)
druid-spring-boot-starter1.1.13数据库连接池
mysql-connector-java随 Boot 版本管理MySQL 驱动
fastjson1.2.37JSON 序列化
Lombok随 Boot 版本管理注解生成实体代码

关键信息有两个:

第一,JDK 必须是 8。 这个后面救了我一命。

第二,MyBatis 和 MyBatis-Plus 是混用的。 这意味着一部分表在 XML mapper 里写 SQL,另一部分表直接继承 BaseMapper,一行 XML 都没有。那么逆向工程的时候,后者就会成为"漏网之鱼"。这个坑我们后面会看到。

再看 src/main/resources/mapping/ 目录:25 个 Mapper XML 文件,0 个 .sql 文件。

因此,数据库结构只能自己从 XML 里 "抠" 出来了。

第一天:从 25 个 Mapper XML 里逆向出建表脚本

逆向的基本思路

MyBatis 的 mapper XML 看起来只是 SQL 语句,但只要系统里所有的 CRUD 都经过它,表结构信息就藏不住。一张表的字段信息,可以从四个地方交叉验证:

  1. <resultMap> —— 最完整,列名、jdbcType、Java 字段名一一对应;
  2. Base_Column_List 这类 <sql> 片段 —— SELECT 的列清单;
  3. <insert> 的列清单 —— 写入时用到的列;
  4. <update>、<where> 里的动态条件 —— 补充字段。

我们拿 SysAccountMapper.xml 映射文件来举例,一个 resultMap 就是一张表的骨架:

<resultMap id="BaseResultMap" type="com.xiaomayi.jser.sys.pojo.SysAccount">
    <id column="id" jdbcType="VARCHAR" property="id" />
    <result column="account" jdbcType="VARCHAR" property="account" />
    <result column="password" jdbcType="VARCHAR" property="password" />
    <result column="accountFlag" jdbcType="INTEGER" property="accountflag" />
    <result column="createTime" jdbcType="TIMESTAMP" property="createTime" />
</resultMap>

翻译成 DDL 语句就是:

CREATE TABLE `sys_account` (
  `id` VARCHAR(64) NOT NULL,
  `account` VARCHAR(255) NULL,
  `password` VARCHAR(255) NULL,
  `accountFlag` INT NULL,
  `createTime` DATETIME NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

jdbcType 到 MySQL类型的映射规则

为了 把25 个文件统一口径,我先定了一张映射表,全程按它来约束:

jdbcTypeMySQL 类型
VARCHAR / CHARVARCHAR(30)
LONGVARCHAR / CLOBTEXT
INTEGERINT
BIGINTBIGINT
TINYINTTINYINT
BOOLEAN / BITTINYINT(1)
DECIMAL / NUMERICDECIMAL(19,2)
DOUBLE / FLOATDOUBLE
DATEDATE
TIMESTAMPDATETIME
未标注 jdbcType默认 VARCHAR(50)

注意,表中约束规则的最后一行——这是最容易埋雷的地方。mapper 里没写 jdbcType,不代表这列真的是字符串,只能说明原作者没标。逆向出来的类型只能当"占位",后面一定要和真实库做比对。

快照表:藏在 INSERT...SELECT 里

系统里有两张 _snapshot 结尾的快照表(企业基本信息快照、联系人快照),没有任何独立的字段声明,只在一条语句里出现:

INSERT INTO apply_enterprise_baseinfo_snapshot (列清单...)
SELECT 列清单...
FROM apply_enterprise_baseinfo
WHERE ...

这种就按 INSERT 后面的列清单来建,类型参照它 SELECT 的源表。

5张表 XML 里查不到,去 POJO 里找

逆向到一半卡住了。有 5 张表只在 JOIN 语句里写过名字,没有任何字段信息:

  • apply_elec_type
  • apply_evaluation_service
  • confirm_info
  • report_diagnosis
  • report_diagnosis_result

XML 这条路断了,但 MyBatis 项目有个特点:Java 实体类(POJO)和表是一一对应的。字段、类型、表名,全在注解里。

以 ReportDiagnosis 为例:

@Data
@TableName(value = "report_diagnosis")
public class ReportDiagnosis implements Serializable {

    @TableId(value = "rapplyid")
    private String rapplyid;

    @TableField(value = "enterpriseId")
    private String enterpriseId;

    @TableField(value = "diagnosisId")
    private String diagnosisId;

    @TableField(value = "createTime")
    private Date createTime;

    @TableField(value = "status")
    private Integer status;

    @TableField(exist = false)
    private String tempFlag;   // 注意这个属性
}

从 POJO 补 schema 有三条铁律:

  1. @TableName 给表名,@TableId 给主键,@TableField(value=...) 给列名;
  2. @TableField(exist = false) 标注的字段数据库里根本没有,必须排除——比如上面的 tempFlag,它只是个查询时的临时载体;
  3. Java 类型按规则映射:String → VARCHAR、Integer → INT、Date → DATETIME、BigDecimal → DECIMAL、Boolean→TINYINT(1)。

第一天的产出

折腾一天下来,产出一份 reverse_engineered_schema.sql:

  • 28 张表,869 行 DDL;
  • 按前缀分了五类:sys_ 字典参照表、apply_ 业务表、企业信息表、_snapshot 快照表、report_ 报告表。

文件开头我特意留了一段声明,这句话现在依然有效:

-- ============================================================================
-- 逆向工程生成的 MySQL DDL 脚本
-- 来源: src/main/resources/mapping/ 目录下的 25 个 MyBatis Mapper XML 文件
-- 说明: 约束、索引、默认值等均为近似值,不代表生产环境真实 schema。
--      列类型根据 jdbcType 属性映射,未标注 jdbcType 的列默认使用 VARCHAR(50)。
-- ============================================================================

SET NAMES utf8mb4;

CREATE DATABASE IF NOT EXISTS `ent_register`
  DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `ent_register`;

注意:

逆向脚本能让系统"跑起来",但不能当生产 schema 用。 索引、默认值、NOT NULL 约束、字段真实长度,全都需要跟生产库的 SHOW CREATE TABLE 逐一比对。

第二天:建库建表,报错一个接一个

我以为抠完 SQL 就结束了,没想到真正的折磨才刚开始。脚本往本地 MySQL(8.0)里一执行,连环报错。

坑 1:主键列不能为 NULL

报错:

ERROR 1171 (42000): All parts of a PRIMARY KEY must be NOT NULL;
if you need NULL in a key, use UNIQUE instead

逆向时我为了图省事,除了主键标记外所有列都写的 NULL,包括主键列本身。但 MySQL 规定主键列必须 NOT NULL(5.7 和 8.0 版本都一样),它不会像某些数据库那样隐式帮你改。

修复:所有 PRIMARY KEY 涉及的列,逐个改成 NOT NULL。28 张表批量处理:

-- 修复前
`id` VARCHAR(64) NULL,
PRIMARY KEY (`id`)

-- 修复后
`id` VARCHAR(64) NOT NULL,
PRIMARY KEY (`id`)

坑 2:表名引号被写成了双引号

执行脚本时又报语法错误,仔细一看,表名外面是双引号:

CREATE TABLE "sys_account" ( ... )

MySQL 的标识符引用符是反引号 `,不是双引号。 双引号在默认 sql_mode 下是字符串,不是标识符(只有开了 ANSI_QUOTES 才当标识符用)。

于是执行批量替换:

"sys_account"  →  `sys_account`

这个错误虽然低级,但是高发——如果从别处复制 SQL、或者某些工具导出时,引号风格会被悄悄换掉。

坑 3:一张宽表把行大小撑爆了

最棘手的一个报错来自 report_maturity:

ERROR 1118 (42000): Row size too large.
The maximum row size for the used table type, not counting BLOBs, is 65535.
This includes storage overhead, check the manual.
You have to change some columns to TEXT or BLOBs.

原因很直观:这张表有 70 多个问卷题目字段,全部被我映射成了 VARCHAR(255)。

我们可以算一笔账:utf8mb4 字符集下,一个 VARCHAR(255) 最坏要占 255 × 4 = 1020 字节。70 个字段就是 7 万多字节,直接超过 InnoDB 单行 65535 字节的硬上限。而问卷答案在业务上是 "选项/短文本",根本不需要 255 的长度保证。

修复方式:把这些题目字段从 VARCHAR(255) 改成 TEXT。 TEXT 的数据存在溢出页,不计入 65535 的行内限制。

-- 修复前:70 多个这样的列
`a1` VARCHAR(255) NULL,
`a2` VARCHAR(255) NULL,
...

-- 修复后
`a1` TEXT NULL,
`a2` TEXT NULL,
...

整个脚本里最终有 71 处用了 TEXT,绝大多数都是这次改的。

经验:

逆向时如遇到"一张表几十上百个字段"的宽表,先算最坏行大小,别等执行报错。公式:SUM(字符列长度 × 字符集最大字节数),utf8mb4 按 4 字节算。

第二天结束

28 张表终于全部建成。为了后面验证登录,我先往账号表里插了一条测试数据:

INSERT INTO `sys_account`
  (`id`, `account`, `password`, `accountname`, `accountFlag`, `telphone`)
VALUES
  ('test-admin-001', 'admin', '123456', '测试管理员', 1, '13800000000');

第三天:启动项目,把登录链路彻底跑通

JDK:不是越新越好

项目导入后第一次编译,直接失败:

java.lang.ExceptionInInitializerError:
Unable to make field private ...JavacProcessingEnvironment$DiscoveredProcessors
... accessible: module jdk.compiler does not "opens com.sun.tools.javac.processing"
to unnamed module

一看当前默认 JDK:24。

问题出在老版本 Lombok:它要通过反射访问 JDK 编译器内部类,而 JDK 9 以后的模块系统把这些内部包封死了。JDK 越新,封得越严。

这就是为什么第 1 节强调"JDK 必须是 8"。老项目不要赶潮流,pom.xml 文件里写的 java.version 是多少,就用对应版本的 JDK。我切到本机的 JDK 8(zulu-jdk8),编译立刻顺利通过。

启动命令:

$env:JAVA_HOME = "C:\JDK\zulu-jdk8.0.492"
mvn spring-boot:run "-Dspring-boot.run.profiles=test"

SSL 握手错误

启动后数据源连接又报错了,SSL 握手失败。本地 MySQL 本来就没配证书,没必要走 SSL。在 JDBC URL 上加两个参数:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ent_register
      ?useUnicode=true&characterEncoding=UTF-8
      &serverTimezone=Asia/Shanghai
      &useSSL=false
      &allowPublicKeyRetrieval=true
    username: admin
    password: 123456
  • useSSL=false:显式关闭 SSL;
  • allowPublicKeyRetrieval=true:允许从服务端获取公钥,解决 MySQL 8 驱动下的认证报错。

再次启动,看到下面的日志,我心里终于踏实了:

Tomcat started on port(s): 8080 (http) with context path ''
Started MainApplication in 16.999 seconds

登录链路长什么样子

浏览器访问: http://localhost:8080/views/login.html,登录页能正常渲染打开。

我们不着急登录,先搞清楚登录的完整链路:

浏览器 输入账号、密码
  → 前端用AES加密密码
  → POST /auth/login
  → 后端 AES 解密
  → 查 sys_account 表
  → 用户信息再 AES 加密返回
  → 前端解密、跳转

前后端共用同一套 AES 参数:

// 后端 AesEncodeUtil.java
private static final String AES_KEY = "beg0hcodeomegere";
public static final String AES_TYPE = "AES/ECB/PKCS5Padding";
// 前端 use-aces.js
const CRYPTOJSKEY = "beg0hcodeomegere";
// mode: ECB, padding: Pkcs7(PKCS7 和 PKCS5 在 AES 下等价)

顺带说一句:

密钥写死在前端代码里,只能防 "明文传输被肉眼看到",算不上真正的安全。

这是这个老年项目的历史局限性,心里有数即可,改起来要走 HTTPS + 后端下发密钥的方案,不在本篇范围内讨论。

坑 4:type 参数被 JS 硬编码覆盖

第一次点登录,后端日志出现这个:

java.net.UnknownHostException: tbepwney.xiaomyi.com

配置文件里写得明白:

baseUrl: http://tbepwney.xiaomyi.com/gateway   # 厂商测试环境,早已下线

原来登录有三种类型,由 type 参数决定,对应三套完全不同的逻辑:

type含义认证方式
0企业管理员入口调远程 SSO 网关(已下线的域名)
1职能部门入口查本地 sys_account
2后台入口查本地 sys_account

我明明把页面里的隐藏域改成了 value="2":

<input type="hidden" name="type" value="2"/>

可请求发出去还是 "type":0。顺着 JS 一查,在提交监听里抓到了元凶:

form.on("submit(accountSubmit)", function (data) {
    var loginData = {};
    loginData = data.field;
    loginData.type = 0;          // 就是这行!提交前把 type 强行覆盖回 0
    data.field.password = encrypt(data.field.password);
    $.ajax({ ... url: ajaxUrl + "auth/login", data: data.field ... });
});

隐藏域被 JS 硬编码覆盖了,改 HTML 根本没用。 把这行改成 2:

loginData.type = 2;   // 走后台入口,本地认证

这个坑的教训是:当你遇到**"我明明改了,为什么不生效?"时,要先怀疑有没有下游代码把你的值覆盖掉**,尤其是前端这种 "表单 + JS 二次组装" 的写法。

坑 5:后端返回成功,前端取用户名却取崩了

改完 type,后端正常返回 200,浏览器却报:

Uncaught TypeError: Cannot read properties of undefined (reading 'username')
    at Object.success (login.html:509)

查看前端代码:

success: function (res) {
    res.data = decrypt(res.data);
    if (res.code === 200) {
        sessionStorage.setItem("username",
            res.data.account.userInfo.username);   //509 行

问题是:两种登录入口返回的数据结构不一样:

  • type=0(企业入口):数据来自远程网关,account 是个嵌套对象,里面才有 userInfo.username;
  • type=2(后台入口):account 直接就是 sys_account 那条记录,结构是扁平的,用户名字段就是 account。

后端 backLogin 里写得很清楚:

data.put("account", account);   // account 是 SysAccount,扁平结构

按真实结构改:

res.data.account.account        // "admin"

坑 6:点申报类型,提示"登录过期"

能登录了,点进申报选项页,随便点一个申报类型,弹窗:

登录过期请您重新登录!

这是本次最隐蔽的一个坑,需要把登录和鉴权两边的代码对着看。

申报相关的 Controller 方法,几乎全部标注了 @AuthCompany:

@AuthCompany
@RequestMapping(value = "/declar/type", method = RequestMethod.GET)
public Result declarType() { ... }

而 AuthAspect 切面里,@AuthCompany 的放行条件是:

if (annoAuthCompany != null) {
    if (session.getAttribute(SysContants.SESSION_COMPANY_TYPE) == null) {
        resetSessionInfo(request);   // 尝试靠 cookie 恢复
    }
    if (session.getAttribute(SysContants.SESSION_COMPANY_TYPE) == null) {
        return new Result<>().fail(ResultEnum.NO_LOGIN);   // "登录过期"就是它
    }
}

再看后台登录 backLogin 往 session 里写了什么:

session.setAttribute(SysContants.SESSION_BACK_USER_INFO, ...);
session.setAttribute(SysContants.SESSION_BACK_TYPE, account.getAccountflag());
// 唯独没有 SESSION_COMPANY_TYPE!

真相大白:后台登录只设置了后台鉴权(@AuthBack)需要的属性,而申报接口要的是企业鉴权(@AuthCompany)属性。

登录本身是成功的,但两套鉴权对不上,切面一律按 "没登录" 处理。

修复思路:后台入口登录后,把企业鉴权的标记一并设上,让两类接口都放行。改 backLogin方法:

session.setAttribute(SysContants.SESSION_BACK_USER_INFO, JSON.toJSONString(account));
session.setAttribute(SysContants.SESSION_BACK_TYPE, account.getAccountflag());
// 后台入口同时放行 @AuthCompany 标注的企业申报接口
session.setAttribute(SysContants.SESSION_COMPANY_TYPE, 1);

切面里靠 BACK_TOKEN cookie 恢复 session 的逻辑(resetSessionInfo)也要同步补上,否则 session 过期重建后又会报"登录超时":

if ("BACK_TOKEN".equalsIgnoreCase(name)) {
    String uuid = cookie.getValue();
    JSONObject userInfo = (JSONObject) CACHES.getIfPresent(uuid);
    if (userInfo == null) continue;
    HttpSession s = request.getSession();
    s.setAttribute(SysContants.SESSION_BACK_USER_INFO, ...);
    s.setAttribute(SysContants.SESSION_BACK_TYPE, ...);
    s.setAttribute(SysContants.SESSION_COMPANY_TYPE, 1);
}

坑 7:sys_dict 表不存在——MyBatis-Plus 的"隐形表"

鉴权通过了,调用申报类型接口又返回 500错误码。查后端代码逻辑,这个接口是要查字典表:

QueryWrapper<SysDict> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("dict_pcode", "declar_type");
List<SysDict> sysDicts = sysDictMapper.selectList(queryWrapper);

去库里一看——sys_dict 表压根不存在。

回头才想明白第 1 节埋的伏笔:SysDictMapper 继承的是 MyBatis-Plus 的 BaseMapper,所有 SQL 都是框架运行时自动生成的:

public interface SysDictMapper extends BaseMapper<SysDict> { }

没有 XML 文件,逆向工程自然就漏掉了它。 这是"从 XML 逆向"这个方案的天然盲区。好在实体类上注解齐全,照着建一张:

CREATE TABLE `sys_dict` (
  `dict_code`  VARCHAR(64)  NOT NULL,
  `dict_value` VARCHAR(255) NULL,
  `dict_label` VARCHAR(255) NULL,
  `dict_seq`   INT          NULL,
  `dict_pcode` VARCHAR(255) NULL,
  PRIMARY KEY (`dict_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典';

INSERT INTO `sys_dict` (`dict_code`,`dict_value`,`dict_label`,`dict_seq`,`dict_pcode`) VALUES
  ('dt-001','0','申报试点示范',1,'declar_type'),
  ('dt-002','1','申报咨询诊断',2,'declar_type');

小技巧:

逆向脚本完成后,一定要全局搜一遍所有 BaseMapper 的子接口,收集对应实体上的 @TableName,和已生成的表清单做差集。这次差集就是 sys_dict。这个检查应该纳入标准流程,而不是等运行时 500 错误。

最终验证:全链路打通

重新登录,逐项验证:

验证项结果
Spring Boot 用 JDK 8 编译、启动✅ Tomcat 8080 正常监听
登录页 views/l-+ogin.html 渲染✅ 账号/密码框正常
POST /auth/login(admin / 123456,type=2)✅ code=200,返回加密用户信息
前端解密返回数据✅ 得到 admin 用户信息
GET /apply/type/info/declar/type(@AuthCompany)✅ 返回申报类型字典
进入申报页面✅ 不再提示登录过期

接口实测返回:

{
  "code": 200,
  "data": [
    {"dictValue": "0", "dictLabel": "申报试点示范"},
    {"dictValue": "1", "dictLabel": "申报咨询诊断"}
  ]
}

三天的目标全部达成:数据库有了,项目活了,登录通了。

注意:

项目能跑起来、能登录成功,并不等于项目已经被完整还原,更不等于可以直接打包上生产。这也是现在 AI 生成的项目不能直接部署上线的原因之一。

接老项目?这份排雷清单请收好

把这三天的经验沉淀成一张清单,下次接手或者遇到类似的项目,按顺序走:

启动前

  • 看 pom.xml,记下 Spring Boot / JDK 大版本,准备对应 JDK(别用最新版硬上)
  • 列出 mapping/ 下所有 XML,确认有没有现成的 .sql
  • 全局搜 extends BaseMapper,把 MyBatis-Plus 的"隐形表"先登记下来

逆向建表

  • 从 resultMap / Base_Column_List / INSERT 列清单提取字段,多源交叉
  • 统一定一张 jdbcType → MySQL 映射表
  • XML 里缺失的表,从 Java POJO 补,排除 @TableField(exist=false)
  • 主键列一律 NOT NULL;标识符一律用反引号
  • 宽表先算最坏行大小(字符列长度 × 4),超 65535 就把非检索字段改 TEXT
  • 建表后用 BaseMapper 清单和表清单做差集,补掉隐形表

环境

  • 确认本机 MySQL版本
  • JDBC URL 显式加 useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
  • 命令行连接显式指定 host/port,避免连错实例

登录联调

  • 搞清楚登录类型参数(本项目是 0/1/2),确认哪一种走本地库
  • 搜 JS 里有没有提交前强行覆盖表单字段的代码
  • 核对前后端数据结构是否一致(嵌套 vs 扁平)
  • 顺着鉴权切面,核对"登录写入的 session 属性"和"各鉴权注解要求的属性"是否对得上

这套打法是通用的吗?

有读者可能会问:你这是运气好,源码齐全才能这么玩。换个项目还成立吗?

我的答案是:方法论层面普适是成立,操作层面半普适。 说清楚边界,你才能放心用。

能推广的四条原则

1. 多源交叉验证——单一信息源不可信,多方印证才可信

mapper 里没标 jdbcType,不代表这列是字符串;但 resultMap、INSERT 列清单、Java POJO 三方一致的结论,可信度极高。

这是情报分析的基本功,不限技术领域。

2. 证据链原则——不猜,让产物可证伪

逆向出的每个字段都能指出处;推不出来的(索引、默认值、NOT NULL)就明说"近似值,需与生产比对",而不是编一个看起来合理的。

可证伪,是逆向工程和瞎编的分界线。

3. 差集检查——"做完"不等于"做全"

逆向完成后,拿"代码里引用的全部实体"和"已生成的表"做差集,sys_dict 就是这么抓出来的。主动找缺口,而不是等运行时报错。

4. 动态闭环——静态重建必须用运行来验证

静态分析和动态验证交替进行:每一个运行时错误,反过来指回一个静态分析的盲区。修复无文档的项目,这是唯一可靠的路径。

不能过度推广的边界

诚实地说,这套打法有前提:

  1. 前提是有完整源码。 只有 war 包没有源码,得换反编译路线,难度完全不同。
  2. 单体架构适用,微服务要打折扣。 分布式老项目还得先查服务拓扑、注册中心、网关路由等,工作量要翻几倍。
  3. 逆向产物只是"能跑",不是"恢复真相"。 字段长度是拍的、索引全丢了、外键无从考证——上线前必须拿生产库 DDL 逐表比对。
  4. 运行时验证依赖"能触发的路径"。 关键路径依赖外部服务时,只能改代码绕行,而你改的每一处都是在"替原作者做决定",风险要自己承担。

一句话总结:这篇文章最值钱的不是"我怎么建出了 28 张表",而是"面对信息残缺的系统,如何用代码本身当作唯一可信的证据源,重建出可运行的系统"——这个思路对任何接手烂摊子的人都有效。

写在最后

三天下来最大的感受是:老项目跑不起来,难的从来不是技术,而是信息数据断层。

原作者把约定写在代码里、把环境留在自己机器上、把测试数据留在内网,人一走,这些信息全部清零。接手者的工作,本质上是一次"考古"——从残留的 XML、注解、配置里,把当年的设计一点点还原出来。

也有几点提醒我们在做项目的同行:

  1. 建表脚本一定要进代码仓库。 一份 schema.sql 成本几乎为零,缺了它,后人要花三天去猜。
  2. README 里至少写清:JDK 版本、中间件版本、如何初始化数据库、默认账号。
  3. 逆向工程永远只是"能跑"的起点。 这次生成的脚本没有索引、没有默认值、字段长度是拍脑袋的——但上线之前,必须拿到生产库的真实 DDL 逐表比对。
  4. 写完每一个"自动处理"的脚本(包括前端表单),想想别人改了上游之后,它会不会偷偷把值改回去。

这个系列文章后面还会继续,后面打算讲讲在这个老框架上开发新功能时,又遇到了哪些"历史的遗产"。

如果这篇文章帮你少踩一个坑,就是它最大的价值体现,也不枉我忙活了三天。

下期预告:《AI三分钟重构1600行Controller,方案看起来很专业,但我一条都没敢用》

我是老炮,18 年 Java 老兵,仍在一线。关注「Java老炮踩坑录」,不错过每一篇真实案例,少踩坑。