一个半天需求干了三天:代码腐化的五个信号与自查命令

代码腐化的五个信号:我从这个项目里看到的

老炮踩坑录 · V03 · 老炮视野系列

基于「企业融合评估平台」真实源码,跟着一个"半天能做完"的小需求走完全程,看五个腐化信号如何在改动现场挨个露面

关键词:额外修改成本 · 重复方法 · 超大文件 · 废弃的注释代码 · 模板占位符

👋 欢迎阅读

🏠个人主页: 知守观

📘我的专栏: 老炮踩坑录

💻当前内容:代码腐化

引子

2022 年春天有个新需求,我到现在还记得自己当时的工作量预估:只要半天时间。

产品要的东西不复杂,几个申报列表上加一个时间提示,用来显示这条申报距离今天过了多久。列表查询本来就有,日期字段本来就有,取出来算一下,展示,完事。我给产品回的话是"明天给你上"。

实际上我花了三天才完成。

这三天里我没有解决任何技术难题,没有设计任何新模式,大部分时间在做两件事:找代码,以及判断找到的代码还作不作数。后来我复盘这三天的时间去向,发现五个熟悉的信号全在现场。这篇就按那天的真实路径走一遍,带你看看"一个半天的需求怎么变成三天的"。

先交代一句,当时的对话和具体钟点我凭记忆还原,但代码、行号、数字全部来自现在的代码仓库,2026-10 实跑可查。这个仓库快照里 git 历史只有二十几个提交,都是近年的,原始开发记录不在里面,所以没法按提交时间复原当年的过程,靠的是代码里留着的版本标记,下面会讲到。

先说清楚,这篇不是吐槽。这些代码里,有不少是我自己经手留下的。写它是因为那三天让我想明白了一件事:腐化不是某个人某次不负责任的产物,是一连串"当下合理"的决定长期累积的结果。

信号一:还没动手,先要在一千五百行里找到位置

接到需求,我先找项目申报列表在哪。

IDE 里打开 EnterpriseRegistController.java,1600 行。跟它配套的 ApplyInfoServiceImpl.java,1796 行,34 个方法。整个项目里 800 行以上的文件有 9 个:

yaml 复制代码
1796  ApplyInfoServiceImpl       34 个方法
1545  EnterpriseRegistController 25 个方法
1488  ApplyElecInfoServiceImpl
1048  EnterpriseRegistServiceImpl
 903  ApplyInfoDeptServiceImpl
 871  FileController
 866  HuaweiyunFileServiceImpl
 866  ApplyTypeInfoServiceImpl
 812  HttpUtil

EnterpriseRegistController 名义上管企业注册,25 个方法里躺着四个报表导出。exportPlustekDiagnosis 从 737 行铺到 1109 行,373 行的一个方法,清洗数据、算表头、渲染样式、写输出全在里面。我要找的"申报列表"埋在这些东西中间,滚动条拖了几个来回才定位到。

文件长本身不会直接产生 bug,它先产生的是成本:你读代码的每一分钟,都在为它的体积付费。那天上午的头一个小时就这么没了,一行代码还没改。

信号二:找到一个列表,发现它有三个兄弟

定位到方法,准备动手,发现事情不对。同一个区县申报列表,项目里有三个方法:

yaml 复制代码
countyApplyInfoList                1276 行
countyApplyInfoListForQuxianOnly   1427 行
countyApplyInfoListZhuanjia        1605 行

三个方法从 1276 行排到 1716 行,骨架一模一样:分页、查列表、拼字典、算诊断状态、组装返回。差异在查询条件和少数字段,占不到十分之一篇幅,但"加时间提示"这件事,我得在三个地方各做一遍。

文件里这种带编号的重复方法还有更直白的。FileController 里紧挨着:

java 复制代码
public Result uploadDiagnosis(MultipartFile[] files, ...)    // 60 行
public Result uploadDiagnosis1(MultipartFile[] files, ...)   // 51 行
​
public Result deleteDiagnosis(...)
public Result deleteDiagnosis1(...)

"1"这个后缀讲了个很短的故事:旧方法线上有页面在调,不敢动,新需求压下来,复制一份改改最快。

重复方法真正的问题在以后。哪天文件名校验要改,你搜到两个上传方法,没法确定哪个还在使用------实际情况是两个都有页面在调。改一个漏一个,一个月后另一个页面出问题,排查的人完全想不到两者同源。那天我对着三个方法逐行比对差异,又是半天。

信号三:改完一测,页面不对劲,但什么错都没报

三个方法都加上时间计算,本地跑,页面显示不对劲。没有异常弹窗,没有红色日志,就是某一列数据不对。

追到头,问题在时间解析的兜底上。项目里这种写法遍布各处,以 ApplyInfoServiceImpl 1295 行为例:

ini 复制代码
try {
    date = simpleDateFormat.parse(createTime);
} catch (ParseException e1) {
    e1.printStackTrace();
}
String dateDep = DateUtils.formatFromTodayDep(date);

parse 失败,堆栈拍向控制台,date 保持 null,下一行照走,页面拿到一个奇怪的结果。整个项目里 printStackTrace 有 117 处,26 个文件,HttpUtil 一家 27 处。我搜了一下空 catch,12 处,HttpUtil 第 29 行那个 static 初始化块,异常连控制台都不上:

php 复制代码
static {
    try {
        trustAllHttpsCertificates();
        HttpsURLConnection.setDefaultHostnameVerifier((urlHostName, session) -> true);
    } catch (Exception e) {
    }
}

测试时我的 parse 为什么失败,控制台其实打了,但那堆输出混在百来行历史堆栈里,眼睛直接跳过。异常处理烂到一定程度后,连真的异常也混在大量无效输出里。这一段调试耗掉我第二天的大半个下午。

信号四:翻参考实现,翻到一个完整的废弃方法

改的过程中我想确认下"时间提示"老版本有没有类似实现,顺着相关的类翻,在 WjxServiceImpl.java 第 48 行看到一大片注释:

ini 复制代码
//    @Override
//    public Result addApplicationCompany(JSONObject json) {
//        String companyName = json.getString("q1");
//        String companyLegal = json.getString("q2");
//        ...
//        String a24 = json.getString("q33");
//        ApplicationCompany applicationCompany = new ApplicationCompany();

76 行,一个完整的废弃方法。FileController 第 419 行还有 65 行,旧上传接口的完整实现,Javadoc 齐全。全项目疑似注释掉的代码行有 456 行,连续块超过 20 行的有一大批。

这段废弃代码差点坑了我。我第一眼以为那是某一版的参考逻辑,下意识读了下去,读到一半才反应过来它已经废弃------它引用的字段结构跟现在对不上。如果当时是个新人接手,照着它补新方法,半小时就白费,还可能引入已经作废的业务规则。

注释代码给每一个读文件的人增加一道额外工作:停下来,判断它是否还在使用,没有注释说明为什么保留,全靠猜。

信号五:想搜前人留的说明,135 条全是模板占位符

我没死心,想着这么关键的列表,万一有前人留过 TODO 说明。

bash 复制代码
grep -rn "TODO" src/main/java | wc -l
# 135,分布在 51 个文件

翻了二十几条,没有一条是真的:

kotlin 复制代码
/**
 * @Description: TODO(这里用一句话描述这个类的作用)
 */
public class SysIndustry {

代码生成器自带的模板,文件生成那天起就留在类头,再没被人碰过。真手写的待办混在里面,比如 ApplyElecController 里的 //TODO xmlu add,但我已经没有耐心从 135 条里把它们挑出来了。标记一旦失去区分度,跟没有标记没有区别。

占位符的同类是半成品。仓库里的 AbstractFileService 模板方法搭得不错,md5 去重都写了,全项目只有 HuaweiyunFileServiceImpl 一个子类接过去,其余上传入口照旧各写各的。重构进行到一半人被拉走,抽象留下,后来者还容易产生"这事好像有人做过了"的错觉。

五个信号如何叠加增加成本

单个信号看着都不致命,麻烦在它们互相影响。

复制粘贴攒出重复方法,重复方法多了没人敢动,文件体积失控;文件越长,找代码越依赖搜索,注释掉的废弃代码和模板 TODO 就混进搜索结果形成干扰;异常被 printStackTrace 淹没后,真问题排查更慢,大家更倾向"先复制一份保底"。一个需求三天,其中半天是干活,两天半是在这套循环里打转。

代码里留着一个特殊的记号能看清这件事。V3.5 是项目中期的一次大版本,我数了,"V3.5" 标记 36 处,分布在 13 个文件。我这次改的时间提示,就是 V3.5 的需求,它落地的痕迹现在还在三个列表方法里:

dart 复制代码
// V3.5 添加提示时间 start
String createTime = String.valueOf(e.get("sbsj"));
...
e.remove("sbsj");
// V3.5 添加提示时间 end

ApplyInfoServiceImpl 的 1291、1471 行各有一块,BackDeclareListServiceImpl 129 行还有一块------三个方法里各插了一份,连标记都复制了三份。这类 start/end 块当年是为了好定位改了哪,几年之后,没人知道这些 V3.5 兼容分支哪些数据还在走,哪些可以删。历史遗留代码就这么一层层累积起来。

在你的项目里重走这一遍

五个信号的统计命令,跟那天的排查路径同序:

信号 怎么搜 该警觉的量级
文件体积 按行数排序,再数单类方法数 单文件 800+、单方法 200+
重复方法 搜带数字编号的方法名,同名方法比对方法体 出现 1/2 编号 = 已经在复制
异常被吞 grep -r printStackTrace src/,再搜空 catch 健康项目趋近零,按文件密度看
废弃的注释代码 脚本扫连续 // 开头且像代码的行 连续超 20 行的块必须处理
占位符 grep -rn TODO src/,区分模板和手写 模板占位跨版本还在 = 没人管

补一句实话,单看某个信号可能冤枉人------printStackTrace 最多的也许恰好是没人动的工具类。五个体征在同一个模块里同时抬头,再下判断。

老炮点评

这篇文章里批评的东西,有不少是我自己经手留下的。那三个"V3.5 添加提示时间"块,时间提示这个功能,就是我做的。

当时没人觉得有问题。需求压着,三个重复方法是历史遗留,我做的是"按现有风格把功能加上",一天干完,产品满意,测试通过。我甚至还规规矩矩打了 V3.5 标记,方便以后定位。站在当时看,这活干得没毛病。

腐化最吊诡的地方就在这儿:它从来不是某个人某次不负责任的产物,是一连串"当下合理"的决定长期累积的结果。我写这篇不是站在旁观者的位置指责当年的开发者------这些代码里也有我的一份。

那三天教会我的东西也简单:判断一个项目健不健康,别看架构图,找一个真实的小需求,计时。改动顺畅、涉及面一目了然、异常有据可查,项目就还活着。一个半天的需求要三天,这个项目就在为过去的每一个"先这样吧"付出代价,而这些成本全部由现在改代码的人承担。

延伸阅读 :

如果你还没看过这个项目的重构方案,可以翻一下 《# 老项目重构实战》。


下期预告:《老炮的代码审查清单:我 Review 代码时看什么》

格式问题交给规约插件,我 review 时看的是异常流向、事务边界、空值路径、并发里共享了什么。下期把清单摊开,每一条配这个项目里的真实反例。

如果本文对你有帮助,欢迎:

👍点赞 | ⭐收藏 | 👤关注 | 💬留言

你的每一次互动都是我继续更新的动力,我们下一篇见!🚀

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

相关推荐
过客123452 小时前
从"发现"到"处置":一个无人值守 AI 闭环的完整拆解(85 天真实数据)
后端·agent·ai编程
黎燃2 小时前
我搭了个“大模型辩论赛“:让 DeepSeek、Qwen、GLM 在蓝耘上吵了一架
后端
墨家句子2 小时前
服务器被反复尝试登录之后:fail2ban 加密钥登录的五道加固
linux·后端
炸鸡叔2 小时前
我做了 BotBus:从手机续聊本地 Agent,查看文件和终端
前端·后端
漂不动2 小时前
告别上下文爆炸:基于 Milvus-Lite 与渐进式披露的动态 Agent 工具挂载实践
后端
代码山河2 小时前
Java学习路线图:2026年最新版,从入门到架构师
java·学习·架构·教程·面向对象·项目
anew___2 小时前
《从零手写操作系统 (30):环境变量与进程上下文——export/unset与继承语义》
java·开发语言·网络·jvm·算法
jason成都2 小时前
rtklib_java项目新增模块:rtklib-research与rtklib-stream功能详解
java·gnss·rtklib·rtklib_java
Gust of wind3 小时前
串与KMP模式匹配:存储结构、基本操作、next数组手算
c语言·数据结构·后端·算法