重构与代码生成:重构重复的样板代码
样板代码,说白了就是那些你写了无数遍、结构几乎一模一样、只是换了个业务字段的代码。比如 CRUD 的 Service 层、DTO 转换、配置类注册……我见过太多项目,一半以上的代码都是这种「复制粘贴改改字段」的活儿。
嗯,今天我们就聊聊怎么用代码生成来干掉这些重复劳动。这不是让你偷懒,而是让你把精力花在真正有挑战的地方。
样板代码到底有多「毒」?
先说说我自己的经历。几年前我接手过一个老项目,光是一个实体类的增删改查,就写了 2000 多行。四个模块加起来,类似的代码重复了 6 遍。每次加个字段,要改 6 个地方。有一次漏改了一个查询条件,线上数据直接查不全。
为什么会这样?因为人不是机器,重复的事情做多了,一定会出错。
样板代码的典型问题:
- 维护成本高:改一处逻辑,要同步 N 个地方
- 容易遗漏:复制粘贴时漏改一个变量名,就是 bug
- 代码膨胀:项目 70% 的代码都是「废话」,真正业务逻辑被淹没
- 新人上手慢:满屏的重复代码,根本分不清哪些是核心逻辑
我个人习惯,当一段代码出现第三次的时候,就该想想能不能用生成器解决了。
代码生成的三种常见模式
代码生成不是非得搞个高大上的脚手架。根据场景不同,我总结了三种模式,你可以按需选用。
| 模式 | 适用场景 | 典型工具 | 生成时机 |
|---|---|---|---|
| 模板引擎生成 | 固定结构的代码(如 DTO、Mapper) | FreeMarker、Velocity | 编译前 / 构建时 |
| 注解处理器生成 | 编译期自动生成(如 Lombok、MapStruct) | Annotation Processor | 编译时 |
| 运行时动态生成 | 需要动态扩展的场景(如规则引擎) | ByteBuddy、CGLIB | 运行时 |
我个人最常用的是第一种——模板引擎生成。它最直观,也最容易控制。你想想看,一个模板文件写好,输入不同的数据模型,就能输出不同的 Service、Controller、DTO。这不就是「一次编写,到处生成」吗?
实战:用模板引擎干掉重复的 CRUD
假设我们有一个典型的 Spring Boot 项目,每个实体都需要一个 Service 接口、一个 Service 实现、一个 Controller。这些代码的结构几乎一模一样,只是实体名和字段不同。
我们先定义一个 FreeMarker 模板,比如 Service.ftl:
package ${packageName}.service;
import ${packageName}.entity.${entityName};
import java.util.List;
public interface ${entityName}Service {
List<${entityName}> findAll();
${entityName} findById(Long id);
${entityName} save(${entityName} entity);
void deleteById(Long id);
}
然后写一个生成器类,读取实体信息,填充模板:
public class CodeGenerator {
public static void main(String[] args) throws Exception {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
cfg.setClassForTemplateLoading(CodeGenerator.class, "/templates");
cfg.setDefaultEncoding("UTF-8");
// 假设我们从数据库或配置文件读取了实体列表
List<EntityMeta> entities = loadEntities();
for (EntityMeta entity : entities) {
Map<String, Object> data = new HashMap<>();
data.put("packageName", "com.example.demo");
data.put("entityName", entity.getName());
Template template = cfg.getTemplate("Service.ftl");
File output = new File("output/" + entity.getName() + "Service.java");
template.process(data, new FileWriter(output));
}
}
}
你看,核心逻辑就这么几行。每次新增一个实体,只需要在配置里加一行,运行一下生成器,所有样板代码就自动生成了。
小技巧: 我建议把生成器做成 Maven 插件或 Gradle Task,这样每次构建时自动执行,保证生成的代码和实体定义始终同步。
重构视角:生成代码 vs 手写代码
有人可能会问:「生成的代码质量能保证吗?会不会比手写的差?」
我的回答是:生成的代码应该比你手写的更规范。因为模板是经过反复打磨的,所有边界情况都考虑到了。而手写的时候,人很容易偷懒——少写一个 null 判断、漏掉一个事务注解,这些都是常见问题。
我曾经在一个项目中,把所有 Controller 的异常处理都写进了模板。结果生成出来的代码,每个接口都有统一的 try-catch 和日志记录。后来审计代码的时候,发现所有接口的异常处理覆盖率是 100%。如果是手写,我敢说至少有一半的接口会漏掉。
注意: 生成的代码不要直接修改!如果你改了生成的文件,下次重新生成时修改就丢了。正确的做法是:把生成的代码当作「基础代码」,自定义逻辑通过继承、组合或扩展点来实现。
知识体系:样板代码重构的核心逻辑
下面这张图是我自己总结的「样板代码重构决策流程」,帮你快速判断什么时候该用代码生成。
这个流程的核心思想很简单:先判断重复次数,再判断结构是否固定。如果重复次数少,手动重构就够了;如果结构不固定,那就需要提取抽象层,而不是硬套模板。
避坑指南:我曾经踩过的三个坑
代码生成不是银弹,用不好反而会引入新问题。我把自己踩过的坑分享给你,希望能帮你少走弯路。
- 坑一:模板过于灵活。我曾经设计了一个「万能模板」,支持各种条件分支、循环嵌套。结果模板本身比手写代码还难维护。后来我学乖了:一个模板只做一件事,宁可多写几个模板,也不搞大而全。
- 坑二:生成代码和手写代码混在一起。有一次我生成了 Service 层,但手写了一些自定义方法。结果重新生成时,手写代码被覆盖了。解决方案:生成代码放在单独的包或模块中,手写代码通过继承或委托来扩展。
- 坑三:忽略测试。生成的代码也是代码,同样需要测试。我建议为生成器本身写单元测试,确保模板输出符合预期。另外,生成后的代码也要跑一遍集成测试,防止模板更新引入回归。
核心原则: 代码生成的目标是消除重复,而不是制造新的重复。如果生成器本身需要大量维护,那说明你选错了方向。
总结
样板代码重构,说白了就是「把重复交给机器,把创造留给自己」。我个人习惯在项目初期就搭建好代码生成的基础设施,这样后续新增功能时,只需要关注业务逻辑,而不是在复制粘贴中消耗精力。
记住:好的代码生成器,应该像一把锋利的刀——切掉冗余,留下精华。