ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

老项目重构怎么做?没有测试、没有停机窗口、没有专项人力下的分期手术方案

老项目重构怎么做?没有测试、没有停机窗口、没有专项人力下的分期手术方案 老炮踩坑录 · V02 · 老炮视野系列基于「企业融合评估平台」真实源码给出一份带约束、带顺序、带放弃清单的重构方案关键词重构 ≠ 重写 · 安全网先行 · 导出剥离 · 三兄弟合并 · 不动清单 欢迎阅读个人主页知守观我的专栏老炮踩坑录当前内容项目重构文章目录引子先把重构两个字说清楚诊断病灶地图第 0 期先织安全网再动刀第 1 期把导出从 Controller 里剜出来第 2 期合并区县列表三兄弟第 3 期收口基础设施这份不动清单和动刀清单一样重要落地这笔预算怎么要决策清单老炮点评引子在我之前写的文章底下经常收到一类私信老炮别光吐槽要是让你回去重构这个项目你会怎么动手呀我一般会先反问他三个问题给多少人给多久系统能不能停很多人听到这三问就不说话了。他们脑子里的重构是一个理想化的周末拉个新分支推倒重写DDD 分层微服务新技术栈周一上线。这种方案我写不出来写出来也没人敢批。这篇我换个写法假设我真的回去了手里是 224 个 Java 文件、3.5 万行代码、一个不能停的在跑系统、三两个能挤出来的人力我按什么顺序下刀以及哪些东西我非常清楚是不能碰的。先把重构两个字说清楚我用的是老 Fowler 的定义行为保持小步推进每一步做完都能随时停下来。推倒重写在我这儿叫重写归另一个项目走另一套预算承担另一份风险。这篇只谈在原系统上做手术。手术有三个绕不开的约束没有测试兜底、没有停机窗口、没有专项人力。后面的所有方案都在这三个约束前提下产生的所以你看不到先把代码重写一遍这种建议。诊断病灶地图先看我在仓库里实际数出来的数字2026-10 跑的命令都在欢迎复核224个 Java 文件35,256 行800行以上的文件9个1796ApplyInfoServiceImpl34个方法1600EnterpriseRegistController25个方法1488ApplyElecInfoServiceImpl1048EnterpriseRegistServiceImpl903ApplyInfoDeptServiceImpl871FileController21个方法866HuaweiyunFileServiceImpl866ApplyTypeInfoServiceImpl812HttpUtil光看行数会误判我把几个大文件单独挨个翻开看他们的功能、职责。EnterpriseRegistController名义上管企业注册里面塞着四个报表导出方法。exportPlustekDiagnosis从 737 行铺到 1109 行373 行一个方法干了数据清洗、表头并集、样式渲染、填充输出四件事。加上另外三个导出方法这个 Controller 里近一半篇幅跟注册没有关系。ApplyInfoServiceImpl的 34 个方法里混着问卷加载、推荐列表、区县列表、Excel 打印、云端文件上传、评估结果修改——至少六件事。区县列表还存着三个孪生方法countyApplyInfoList、countyApplyInfoListForQuxianOnly、countyApplyInfoListZhuanjia从 1276 行排到 1716 行骨架几乎一样差在查询条件和少数字段。FileController里能看到uploadDiagnosis和uploadDiagnosis1、deleteDiagnosis和deleteDiagnosis1这种编号并存的方法两段带进度监听的下载代码各自内联了一个匿名类复制粘贴的痕迹明显。工具层同样热闹四个 Excel 工具类ExcelUtil、ExcelReaderUtils、ExportExcelUtils、ElecExcelExportUtil三个华为云 OBS 相关的类散在两个包下。有意思的是重构的头前人已经起过仓库里有个AbstractFileService模板方法写得像模像样但只有HwFileServiceImpl一个子类接了过去FileController该多大还是多大。典型的重构进行到一半人又去忙别的了。我们把病灶摸清楚了下面是手术方案分期来。第 0 期先织安全网再动刀直接在 373 行的方法上动结构跟蒙眼拆炸弹差不多。第一步得给这些方法兜住底。我的做法是给导出接口写特征测试不评判业务正确性只做一件事把旧代码的输出钉在那里。// 同样的输入新旧代码产出的字节必须一致TestpublicvoidexportPlustekDiagnosis_sameBytesAsBefore()throwsException{ListMapString,ObjectqueryDatafixture(plustek-3-enterprises.json);byte[]legacyrunLegacyExport(queryData);// 旧方法原样保留byte[]refactoredrunNewExporter(queryData);// 新结构同一批数据assertArrayEquals(legacy,refactored);}项目里本来就有一个ExportPlustekDiagnosisTestJUnit4 的测试数据构造得挺用心能直接当底座。这一期不碰生产代码只产出一批输出快照四份导出各喂两到三批数据单企业、多企业维度不齐、含异常返回把生成的文件存成基准文件。后面每动一刀跑一遍字节比对一致才算过。办法不高级在没有覆盖率的项目里这是唯一能让我晚上睡着的东西。第 1 期把导出从 Controller 里剜出来安全网就位动第一刀四个导出方法整体外迁。先定一个极简接口四个模型各一个实现publicinterfaceReportExporter{Integerpaperid();voidexport(ListMapString,Objectdata,HttpServletResponseresponse);}ComponentclassPlustekDiagnosisExporterimplementsReportExporter{publicIntegerpaperid(){return0;}publicvoidexport(ListMapString,Objectdata,HttpServletResponseresponse){ListMapString,ObjectcleanednewDiagnosisDataCleaner().clean(data);DiagnosisHeaderheadernewDiagnosisHeaderBuilder().build(cleaned);newDiagnosisSheetRenderer(header,cleaned).write(response);}}那个 373 行的方法顺势拆成三段清洗、表头构建、渲染每段一个类每类只管一件事。Controller 里原来按 paperid 分支的入口backExportModel709 行那个换成一张注册表AutowiredprivateListReportExporterexporters;// Spring 自动收集所有实现PostMapping(backExportModel)publicResultbackExportModel(HttpServletResponseresponse,RequestParamIntegerpaperid){exporters.stream().filter(e-e.paperid().equals(paperid)).findFirst().orElseThrow(()-newSystemException(不支持的模型: paperid)).export(applyInfoService.selectApplyInfoExport(paperid),response);returnnewResult().success();}ReportController.searchPaperInfo里那个case 1/2/3的 switch用同一套注册表思路收编。以后产品说加第五个模型写个新实现丢进 Spring 容器老代码一行不用动。这一刀做完EnterpriseRegistController从 1600 行掉到 700 行上下回到企业注册该有的体量。第 2 期合并区县列表三兄弟三兄弟的差异我逐行比过集中在查询条件区县限定、专家视角和返回字段的增减。这种差异不配各占一个 150 行的方法。抽一个查询参数对象差异收进显式开关publicclassCountyListQuery{privateIntegerpageNum;privateIntegerpageSize;privatebooleanquxianOnly;// 只看本区县privatebooleanexpertView;// 专家视角字段集不同// 其余查询条件...}合成一个方法公共骨架只写一遍差异点用两三个私有小方法隔出来。Mapper XML 里对应的三段 SQL 同样处理公共部分抽sql片段where条件按开关拼。这一期比第一期便宜风险却更碎。三个方法在生产环境各自有页面在调返回字段差一个前端表格就空一列。顺序上我会让新方法和旧方法并存先切一个页面观察一周再切下一个三个页面都切完再删旧方法。第 3 期收口基础设施结构松快之后处理那些不致命但天天恶心人的东西。文件上传收编。AbstractFileService已经在仓库里躺着把FileController里uploadDiagnosis、companyUpload这些重复的上传方法逐个接到模板方法上编号带 1 的孪生方法确认没有调用后直接删。两段重复的进度监听下载抽一个DownloadProgressListener内联匿名类消失。异常处理统一。117 处printStackTrace是最扎眼的数字光HttpUtil一家就占 27 处。上一个RestControllerAdvice全局兜底工具类里 catch 到异常要么有正当理由吞掉并记录要么包装成业务异常往上抛堆栈交给日志框架不允许直接拍控制台。这件事没法一步到位按调用链分批改改一批回归一批。事务回滚的坑先填。Transactional 事务失效排查写过catch 里吞异常加 returnTransactional形同虚设。这属于数据正确性问题真要排期我会把它提到第 1 期之前跟着第一次导出回归一起验证。这份不动清单和动刀清单一样重要成熟的重构方案一半内容是不做什么。我明确不碰这几样鉴权体系不动。AuthAspect加三个注解虽然长得笨——切点大、每个请求查库、缓存容量写死 50——但它在生产环境跑了几年权限正确性有实战背书。鉴权代码的每一次改动都踩在安全红线上为优雅重写它收益和风险完全不成比例。真要优化查库频率加个一分钟短缓存足矣局部小改不碰骨架。Session 不换 JWT。这是项目级别的决策牵涉客户端存储、失效策略、三套入口的改造混在重构里做等于给自己挖坑。war 和外置 Tomcat 不动。之前的文章推演过换部署形态要运维一起动那是升级专项的预算重构期不沾。数据库表结构不动。重构里改 schema数据迁移和回滚预案会吃掉所有节奏。结构问题记账单独立项。包结构不做大规模搬家。按业务域重组包名看着很爽git 历史全断、merge 冲突遍地系统行为却没有任何改善。这种为整洁交的学费我不交。落地这笔预算怎么要方案再好没人批就是废纸。我工作十多年没见过哪个老板批重构专项批得简单痛快过但每个迭代里都塞有重构的需求。我的经验是把手术拆碎了混进去这个迭代做导出需求顺手外迁一个 Exporter下个迭代修区县列表的 bug顺手合并一个孪生方法。每刀都小每刀都有业务需求当掩护每刀合上去都可回滚。特征测试和基准文件提前一个迭代备好谁也看不出你在搞重构。完成的标准得定好旧方法删干净、调用方切完、基准测试全绿、对应页面回归过。四条缺一条这期手术不算完——仓库里那个只有一个子类的AbstractFileService就是算了完三个字的下场。决策清单判断项怎么看我的取舍先动哪里看调用频率和出错代价不看丑陋程度数据正确性事务先于结构没有测试能不能改能先补特征测试再动结构安全网先行没有例外大方法怎么拆按职责阶段拆拆完行为字节级一致清洗 / 构建 / 渲染分段孪生方法怎么并差异参数化先并存再逐个切流量页面切完再删旧方法什么不碰安全红线、跨团队成本、无行为收益鉴权、认证、部署、schema、包搬家怎么落地拆碎混进业务迭代不立专项小步、可停、可回滚老炮点评我对重构这件事的看法越做越保守。年轻时候觉得重构是审美活动看见长方法手就痒恨不得一夜之间把代码改成教科书。现在我知道重构是经济活动每一刀都有成本、有风险、有收益方案的高下取决于资源约束下的排序和取舍。一个能落地的笨方案强过一百张漂亮的目标架构图。这个项目真正的教训也在这儿大量时间花在纠正当年图省事留下的东西。AbstractFileService写到一半烂尾、孪生方法不断累加、导出逻辑在 Controller 里生根——每一笔单独看都情有可原合在一起就是 3.5 万行需要做手术的代码。重构能力决定一个系统能活多久。而把小重构坚持做在日常的团队永远不需要一篇这样的文章。下期预告《代码腐化的五个信号我从这个项目里看到的》printStackTrace 117 处、注释掉的代码块、编号孪生方法、没人敢删的基建——代码腐化是可以量化的。下期接着拿这个项目当标本数一数五个信号各出现多少次。如果本文对你有帮助欢迎 点赞 ⭐ 收藏 关注 留言你的每一次互动都是我继续更新的动力我们下一篇见我是老炮18 年 Java 老兵仍在一线。关注「Java老炮踩坑录」不错过每一篇真实案例少踩坑。
返回列表