
接手的那天暴雨把办公室窗户砸得闷响。老项目在IDEA里启动要花两分钟控制台刷出上千行XML配置日志最后抛出一个ClassNotFoundException——没人知道少了哪个jar因为lib目录里有两百多个来路不明的依赖包。这项目跑了八年上线时Spring还是3.2连Java 8都没用上。我盯着那堆.jsp页面和BaseDaoImpl意识到自己面对的不是技术债而是一座用胶带和侥幸心理垒起来的危楼。先别急着写代码先搞清楚“它在干什么”我接手后的第一件事不是打开IDE而是把所有业务文档翻出来。结果发现唯一完整的文档是八年前的需求说明书而且里面的功能名和代码里的Action类对不上。于是我开始跑流程、抓日志、问运维把每一个接口的调用链画在纸上。三天后我得出一个残酷的结论这个项目里真正被使用的功能只有35%剩下的要么是当年给客户演示的demo要么是某个离职同事留下的半成品。重构的第一原则不是“重写”而是“删代码”。大多数老旧项目让你崩溃的并非架构陈旧而是大量死亡代码在稀释你的注意力。当你不知道某段逻辑为何存在时它就会成为你重构路上最大的陷阱。我用了整整一周把git log翻了个底朝天对照生产环境访问日志给每个方法标注了“活”或“死”的状态然后才敢动手。用“防腐层”把旧世界和新世界隔开很多人重构老项目喜欢从换框架开始。把web.xml改成SpringBootApplication把applicationContext.xml里的bean迁移成注解。结果呢启动快了但业务逻辑依旧是一团乱麻换个框架只是给烂房子刷了新漆。我的做法截然相反先建一个legacy包把老代码原封不动地挪进去然后在新代码和旧代码之间加一层防腐层。这层防腐层不是简单的Facade而是用接口定义业务意图用适配器屏蔽老实现的具体细节。比如老项目里有个订单查询逻辑散落在OrderAction、OrderService、OrderUtil三个类里还夹杂着SQL拼接。我不会去重构这三个类而是新写一个OrderQueryPort接口再写一个LegacyOrderQueryAdapter去适配老逻辑。新业务代码只面向OrderQueryPort编程。当旧的泥潭无法被清空时你至少可以在泥潭周围铺上木板让自己不再陷进去。这一步的价值是巨大的所有新功能都跑在干净的路上老代码的修改被严格限制在适配器内部。等有一天适配器积累到足够多你就能把老代码整体铲掉而不影响上层业务。从一坨Spring XML中解放出来但别迷信注解老项目里最恐怖的文件是applicationContext.xml足足1500行。里面有一半是bean idxxx class...另一半是property name... ref.../。我一开始也想全部改成Autowired但现实很快打脸有些bean在XML里被配置成了单例但代码里又用new出来还有些bean的初始化顺序依赖XML的声明顺序。盲目迁移注解等于把隐式依赖变成显式错误。我的策略是保留XML作为“根配置”但把业务bean迁移到Service和Component同时用DependsOn来显式声明顺序。最关键的是我利用Spring Boot的ConfigurationProperties把那些散落在.properties文件里的配置项集中管理。配置不应该散落在十个文件里更不应该靠注释来交代它是干什么的。我们把每个配置项都变成了有类型、有校验、有默认值的Java类启动时如果配置缺了或错了直接报错而不是等到运行时才神秘地灭掉。数据访问层从JdbcTemplate到JPA但保留SQL的尊严老项目的数据层是自研的BaseDao里面封装了JdbcTemplate但是写满了String sql select ...并且用ListMapString, Object返回结果。每一条业务SQL都是独立的没有复用没有命名参数只有几千个?占位符和手动setObject。重构数据层我选择的是Spring Data JPA但不代表我要用它的“自动查询方法”。在遗留系统里SQL是最宝贵的资产之一它承载了多年业务规则的演化你不能因为框架漂亮就把它扔掉。所以我的方案是实体类用JPA注解映射但复杂的查询继续写在Query里甚至保留原生SQL。JPA解决的是CRUD和事务管理SQL解决的是复杂聚合。两者并行不悖。更重要的是我把所有SQL从DAO类里抽出来放进repository层并用NamedQuery统一管理。这样一来你终于能回答老板那个经典问题“这个系统最慢的十条SQL是什么”重构前答案是“不知道可能都慢”重构后我们可以用Spring Boot的DataSource监控和Query的命名快速定位瓶颈。依赖注入的边界别让Spring成为你的设计借口老项目的Service层极度膨胀一个OrderService里有四十多个方法包括短信验证、库存扣减、物流查询。有人为了偷懒直接把ApplicationContext注入进去在业务代码里写SpringUtil.getBean(xxx)。这种写法让依赖关系彻底失控你看不出一个类到底需要什么因为它随时可以掏出一个“万能工具包”。重构中我严格执行“构造器注入”并控制每个Service的职责数量。如果一个Service的方法超过八九个说明它该拆了。我们拆出了OrderService、OrderStateMachine、OrderShippingService等独立类。Spring Boot的本质是帮你管理依赖而不是帮你掩盖依赖混乱。如果你把Autowired当作万能胶那框架再好也白搭。这里有个细节老代码里有个OrderUtil的静态类里面全是static方法比如getOrderStatusDesc。重构后我们把它改成OrderStatusDescriber注入到需要它的Service里。虽然看起来只是多了几个字但这意味着你不再需要为了测试一个方法而去祈祷静态变量没有副作用。老测试代码写的是OrderUtil.getOrderStatusDesc(1)但我们现在可以在测试里mock这个describer返回任意描述而不用关心其内部实现。启动速度从两分钟到十秒是顺其自然的结果很多人把Spring Boot的好处归结为“内置Tomcat不用打war包”其实这是最肤浅的好处。Spring Boot真正带来的不是技术栈而是一种“可运维性”。老项目部署时需要一套复杂的脚本设置JAVA_OPTS、CATALINA_HOME、-Dspring.profiles.active等等任何一个环节错了应用就起不来。我们重构后用mvn spring-boot:run就能本地起服务用java -jar app.jar --spring.profiles.activeprod就能部署生产。这不是魔法而是Spring Boot的约定优于配置在起作用。旧项目的bean初始化链里有几个可怕的PostConstruct方法里面要做数据库连接检查还要跑一遍数据字典加载如果数据库连不上应用直接崩。重构时我们把这类“启动期外部依赖检查”移到了ApplicationRunner里并且用Order来控制顺序。这样即使某个外部依赖挂了应用也能启动只是检查失败记录下来方便排查。这种“宁可启动慢一点也不让错误隐藏”的哲学是我从Spring Boot的失败分析器中学到的。测试不是重构的装饰品而是重构的救生圈老项目几乎没有测试唯一一个Test类还是生成项目时自带的跑的也是System.out.println。重构最怕的是什么是改了代码不知道哪里坏了。所以我在重构起始就引入了JUnit 5和Spring Boot Test但没天真地指望一次性覆盖全量。我的策略是为防腐层写测试为业务规则写测试为数据迁移写测试但绝不为那些要删除的死代码写测试。比如订单超时关单逻辑以前散落在一个Job类里用的Thread.sleep模拟触发重构后变成Scheduled方法测试时用Clock注入时间把“超时”做到可控。写测试的最高境界不是测试代码本身而是测试你对业务的理解。当你把OrderStatus枚举从String改成枚举类后所有switch都能变成策略模式测试用例自然就有了。我还做了一件大胆的事用SpringBootTest写了一个“冒烟测试”它会启动整个Spring上下文然后调一遍所有外部接口的mock确保没有bean缺失。这个测试在老项目里根本跑不起来因为ClassLoader都会出错。但重构到一半时这个测试成了我们的安全网。每次提交代码前跑一遍二十分钟后如果变绿就可以安心合并。那种“改了三行代码然后担心半天”的恐惧感终于消失了。边重构边交付而不是憋一个大版本最忌讳的重构思路是我闭关三个月把所有代码重写一遍然后一次性替换。这样的结局往往是死路一条。重构的最高策略是“绞杀者模式”——用新代码慢慢绞杀旧代码每次只杀一小块但每次都有业务价值。我们按照这个策略制定了三个月的重构路线图。第一个月把系统外层统一成RESTful API内部调用老Service但通过防腐层隔离。第二个月将核心业务表对应的数据访问迁到JPA并完成对应的测试。第三个月把老Service拆除拆到哪个类就把那个类的依赖理清。每一步都能独立上线每一步都能回滚。正因如此我们在重构中途还接了三个新需求而这在老项目上是不可想象的——因为老项目每改一次代码就得面临三天联调。面对“改坏了怎么办”的终极恐惧最后三个月里我经历了唯一一次线上事故一个老报表查询原本返回String类型的日期被我重构后改成了LocalDate结果前端s:date标签解析失败导致页面报500。这个事故让我深刻明白了一个道理重构不是替换而是对齐。对齐数据格式对齐错误码对齐前端预期。很多时候代码里的“糟糕”恰恰是它在保护某个隐含契约。从此我给所有对外接口的返回对象加上了version字段并且写了一套兼容旧格式的序列化器。与其抱怨以前的代码烂不如承认那些烂代码里藏着你没发现的边界条件。只有当你理解了所有异常和特判背后的故事你才有资格去重构否则就是把老坑填上又挖出新坑。最终项目从war包变成了可执行的jar从1500行XML变成了几百行配置从BeanFactoryServiceLocator的混乱依赖变成了清晰的构造器注入。但这个过程中最大的收获不是看着启动日志从红变绿而是我终于能在一个没有Thread.sleep、没有new Date()满天飞、没有if(orderType 1)魔法数字的世界里写代码了。重构结束的那天我删掉了legacy包里最后一个类。仓库里再也没有BaseDaoImpl没有SpringUtil也没有那个让我一开始崩溃的ClassNotFoundException。我打开新项目的bootstrap.yml看了一眼干净得像初雪的配置然后提交了最后一笔commitdelete legacy. the old world is gone.那一刻窗外的雨停了。