ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java后端消除重复代码:工厂模式+模板方法+反射+Bean拷贝实践

2026/10/6 5:59:32 拓冰建站 浏览量
Java后端消除重复代码:工厂模式+模板方法+反射+Bean拷贝实践 在Java后端待久了天天和处理重复代码的人打交道。新渠道接入来了个和旧渠道几乎一模一样的流程新接口校验又是复制粘贴的那一套DO转VO80行getter/setter抄来抄去。针对这三类问题我的解法就是工厂模式模板方法固定流程注解反射收拢横切逻辑Bean拷贝接管对象属性搬运。这套组合拳在多个项目里跑过代码量下降明显新增渠道从两天压缩到半天。这篇博客就按这三招拆开讲最后附上一个支付渠道对接的重构实例和踩坑记录。适合谁看如果你是写业务逻辑的Java工程师被if-else和重复代码磨得没脾气如果你在面试前想梳理设计模式和反射的实际应用如果你接手老旧项目准备做一次低成本重构。这三招都能直接落地不需要引入重量级框架实测下来反而比某些“高级”方案更稳。1. 前言三个上线前夜的“紧急救火”先说三个我真实经历过的上线前夜。第一个凌晨一点产品跑过来说新渠道明天必须上线你打开代码一看新渠道和上个月做的渠道几乎一模一样只是签名算法、字段命名、回调地址不同。怎么办复制粘贴改吧。于是Controller、Service、DTO、Utils各多出一份“孪生代码”。第二个全项目每个Controller的入参都有一坨手写的非空校验、日志打印、参数转换横竖看不顺眼但没人敢动因为一动就牵连十几个类。第三个从数据库实体DO转VO整整80行的Getter/Setter复制过来复制过去一个字段新增忘改线上直接报空指针。这三个场景对应标题里的三招刚好是一一对应的工厂模式模板方法管“流程重复”注解反射管“横切逻辑重复”Bean拷贝管“属性搬运重复”。我最初是分开用的后来在一个支付渠道项目里把这套组合拳打完整效果比我预想中好很多。下面拆开讲。2. 绝招一工厂模式模板方法把“新增场景”变成“填空”2.1 先看模板方法把流程固定把变化留给子类先说原理。模板方法模式的核心是把一套业务流程的骨架固化在父类里把每一步具体做什么交给子类覆盖。生活里最典型的例子是煮泡面烧水、放面、加料包、等三分钟这是骨架加什么料包、放不放蛋、熟度多久这是子类。你不需要每次重新发明煮面流程只需要按“填空”的方式完成差异部分。在Java业务代码里最常见的模板场景是参数校验、业务处理、结果组装、日志记录。这四个步骤几乎每个接口都有。我见过不少代码把校验、日志、组装散落在每一个方法里第101个接口又来一遍。用模板方法改造后父类里定义一个public final的execute方法统一安排顺序子类只实现doValidate、doProcess、doAssemble这些钩子方法。顺序固定了重复逻辑自然没地方藏身。2.2 工厂模式用Map代替一长串if-else有了模板还需要知道“按什么条件选哪个实现”。工厂模式在这里的职责是把创建对象的逻辑收拢到一个类里调用方不再写switch/if-else判断类型只管通过一个key或枚举去取。很多Java项目里的渠道接入、优惠策略、通知类型都是典型的多分支场景。我最常用的是“Map初始化注册”的写法在工厂类里维护一个Mapkey是渠道编码或业务类型value是模板子类实例。Spring环境下把子类注入到List里启动时自动注册非Spring环境也可以用静态代码块手动注册。实际效果非常直观原来A渠道、B渠道、C渠道三套if-else现在new一个渠道实现类加一行注册就完事。业务逻辑不再有分支要加渠道就像在菜单上添一道菜。补充一下选型理由为什么不用策略模式策略模式解决的是“同一目标的不同算法”模板方法解决的是“同一流程的不同步骤”两者可以叠加使用但我更建议业务接入场景先上模板。因为大多数接入类需求的差异不仅仅是算法不同而是校验、组装、回写各环节都有差异单用策略模式还得在自己方法里重新编排一遍流程。2.3 一个能直接抄的示例通用渠道处理器用一个支付渠道的例子来说。先定义一个处理器父类public abstract class AbstractChannelHandlerT, R { // execute方法固定流程子类不要重写 public final R execute(T request) { logRequest(request); validate(request); R result doProcess(request); assembleResult(request, result); logResult(request, result); return result; } protected abstract void validate(T request); protected abstract R doProcess(T request); protected void assembleResult(T request, R result) { // 默认空实现子类按需覆盖 } private void logRequest(T request) { // 统一请求摘要日志 } private void logResult(T request, R result) { // 统一结果摘要日志 } }接着是工厂。为了兼容Spring和纯Java两种用法我用一个双重注册的设计Component public class ChannelHandlerFactory implements ApplicationContextAware { private static final MapString, AbstractChannelHandler HANDLERS new ConcurrentHashMap(); private static ApplicationContext context; // 提供静态方法供非Spring组件调用 public static AbstractChannelHandler getHandler(String channel) { return HANDLERS.get(channel); } // 子类在构造器/初始化时调用此方法注册 public static void register(String channel, AbstractChannelHandler handler) { HANDLERS.put(channel, handler); } PostConstruct public void init() { // 从Spring容器里取出所有AbstractChannelHandler类型实例 MapString, AbstractChannelHandler beans context.getBeansOfType(AbstractChannelHandler.class); beans.forEach((name, handler) - { // 这里约定子类上标注ChannelType注解 ChannelType type handler.getClass().getAnnotation(ChannelType.class); if (type ! null) { register(type.value(), handler); } }); } Override public void setApplicationContext(ApplicationContext applicationContext) { context applicationContext; } }这里有个我踩过的坑如果用getBeansOfType自动注册子类一旦实现接口被代理getAnnotation可能拿不到注解。解决办法是让注册逻辑和实现类解耦比如在实现类里实现一个getChannel()方法工厂直接调用这个方法做key。或者更稳妥一点放弃自动注册在工厂的init里显式注册所有实现。代码略长但排查问题非常顺畅。调用侧就干净了ChannelHandler handler ChannelHandlerFactory.getHandler(order.getChannel()); if (handler null) { throw new BizException(不支持的渠道编码); } handler.execute(new ChannelRequest(order));2.4 使用模板工厂的注意事项第一模板父类里execute方法必须设计为final否则子类一旦重写流程骨架就散了“固定流程”就成了摆设。第二不要在父类构造器里调用子类方法Java对象初始化顺序会导致子类字段尚未赋值就执行了钩子方法出现空指针的风险极高。第三工厂里的Map要控制并发初始化我用ConcurrentHashMap注册动作放在启动阶段完成避免运行期动态注册引发可见性问题。第四抽象出钩子方法时不要贪多五六个钩子以上的流程子类理解成本反而上升。还有一个容易被忽视的点模板方法会带来类数量增加。一个渠道一个子类十个渠道就是十个类。如果渠道差异极小只是参数不同直接用工厂返回配好参数的同一个子类反而更好。类数量不是越多越优秀合适的抽象边界才是关键。3. 绝招二注解反射一句注解干掉一坨重复逻辑3.1 注解的本质元数据不是魔法先讲清楚一个常见误解注解本身不包含任何执行逻辑它只是一段结构化元数据附着在类、方法、字段上。真正干活的是“读到这个注解并执行逻辑”的那个处理器。就像快递单上的“易碎”标签标签本身不会搬运箱子是搬运工看到了标签才轻拿轻放。所以用注解反射的第一步不是写注解而是想清楚谁来读读到了做什么什么时候执行我给团队定过一个选择题标准如果一份重复逻辑只是为了“标注某类字段/方法需要额外处理”那就用注解如果是运行时动态找实现类、动态操作对象那就用反射如果两者都要就用自定义注解搭配反射处理器。项目里落地最多的三类字段脱敏、参数非空校验、操作日志。下面拿字段脱敏讲透其他案例按同一套思路套就行。3.2 手写注解反射处理器从定义到执行定义注解很直接Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface Sensitive { SensitiveType type() default SensitiveType.MOBILE; }注意RetentionPolicy一定要选RUNTIME因为我们要在运行时用反射读取。CLASS级别在源码编译后仍存在于class文件但JVM运行时不保证加载反射拿不到。然后写一个处理器。以手机号脱敏为例public final class SensitiveFieldProcessor { private SensitiveFieldProcessor() {} public static void process(Object obj) { if (obj null) { return; } Class? clazz obj.getClass(); for (Field field : clazz.getDeclaredFields()) { Sensitive sensitive field.getAnnotation(Sensitive.class); if (sensitive null) { continue; } field.setAccessible(true); try { String value (String) field.get(obj); if (value null || value.length() 7) { continue; } field.set(obj, mask(value)); } catch (IllegalAccessException e) { // 记日志不要随便吞 } } } private static String mask(String value) { return value.replaceAll((\\d{3})\\d{4}(\\d{4}), $1****$2); } }写好之后在返回VO的字段上直接标注public class UserVO { Sensitive(type SensitiveType.MOBILE) private String mobile; private String email; }调用侧一行搞定UserVO vo userService.getById(id); SensitiveFieldProcessor.process(vo);这里有几个容易翻车的细节。第一getDeclaredFields只能拿到当前类声明的字段父类字段得递归或配合getFields注意可见性区别。第二字段类型不是String时直接强转会ClassCastException可以在注解里加expectedType或在处理器里统一校验。第三频繁反射同一个Class会有一定性能损耗但业务系统里一次出参处理几十个字段毫秒级以下基本无感不需要在这个场景里过度优化。3.3 与Spring AOP结合让零侵入变成常态光有反射处理器还不够因为每个方法都要手动调用process还是有重复。我的做法是把处理逻辑放AOP切面里彻底消灭调用点。举个例子写一个切面专门负责将Controller返回值做脱敏Aspect Component public class SensitiveAspect { Around(annotation(com.example.annotation.SensitiveResponse)) public Object process(ProceedingJoinPoint pjp) throws Throwable { Object result pjp.proceed(); if (result null) { return null; } if (result instanceof Collection?) { ((Collection?) result).forEach(SensitiveFieldProcessor::process); } else { SensitiveFieldProcessor.process(result); } return result; } }然后在Controller方法或类上标注SensitiveResponse返回的VO字段再标Sensitive一行注解都不用在业务代码里出现。这个方案在项目里跑了半年很稳。新的业务开发人员接入成本也低只需知道“出参对象要脱敏就加注解”这一条规则。这里要提醒一个实践教训AOP只对通过Spring代理调用的方法生效同类内部this调用不走代理注解不生效。解决方法是把逻辑拆到另外一个被Spring管理的Bean里或者用AopContext.currentProxy。我之前在基类里写了一个public方法然后子类内部直接调用注解一直不触发排查很久才发现是自调用问题。3.4 注解反射的边界与性能很多人一听到反射就觉得慢。我的体感是在业务系统中反射主要用在启动期的注解扫描、框架初始化和少量数据的对象操作真正高频的循环反射要让位给缓存和编译期方案。如果你确实有每秒上万次的反射调用需求建议用两种手段加速第一做Field/Class缓存把getDeclaredFields、getAnnotation的结果缓存到本地Map避免每次反射查询第二用MethodHandle或者直接上MapStruct这类编译期代码生成。还有一种特殊情况值得单独说非public字段。setAccessible(true)在Java 17以上模块系统里不再是无脑可用要追加--add-opens参数否则抛InaccessibleObjectException。如果项目是JDK 17建议设计时尽量用public方法代替直接改私有字段或者就把目标字段定义为包内可见减少模块限制的烦恼。关于注解丢失我经历过一个非常诡异的场景同一个注解类在Spring Boot 3.x Gradle增量编译时偶尔出现运行时reflect拿不到注解的情况。后来定位是增量编译把class文件覆盖得不够完整注解元数据丢了解决方案是clean后重新build并把构建进程关闭或使用稳定的增量编译配置。这个坑在很多讨论里都有提及实际就是编译期和运行期字节码不一致导致的建议遇到先clean verify再怀疑代码问题。4. 绝招三Bean拷贝请个“搬运工”来复制属性4.1 别再手写Getter/Setter搬运工先说一个最朴素的问题DO转VO、VO转DTO、第三方接口的Request转我们内部的Model这些场景里手写赋值代码到底好不好短期看IDE自动生成很爽长期看字段一旦增加所有拷贝点都要同步改漏改一个就是运行时数据缺失。更麻烦的是多个系统间字段名不同比如cmdno和commandNo纯手写代码就会变成一场找字段的捉迷藏。Bean拷贝工具的本质就是“按名字把源对象的属性值搬到目标对象”。你不需要关心每个字段的Getter/Setter怎么写只需要告诉它“把a拷到b”剩下的交给反射或编译期代码。用生活类比搬家的时候你不会自己把每个抽屉里的东西一个个搬上楼而是让搬运工根据贴好的标签把东西归位。这个搬运工就是Bean拷贝库。4.2 三个主流拷贝方案的对比与选型我在实际项目中用过三种方式简单列个对比方案实现原理性能适用场景风险点Spring BeanUtils.copyProperties运行时反射中等同名字段多、项目里已依赖spring-beans类型不同会忽略与否需仔细看版本source为null会NPEApache Commons BeanUtils.copyProperties运行时反射较慢老项目历史遗留有类型转换开销性能差避免新用MapStruct编译期生成Mapper实现很高字段多、性能敏感的大型项目需要额外插件不如反射“零配置”Spring的BeanUtils是我们日常最常用的因为它不需要引入额外依赖只要项目是Spring生态直接把org.springframework.beans.BeanUtils引进来就能用。Apache那个我建议新项目慎用它的copyProperties会把源Bean转成Map再设值性能差不少而且遇到不同类型的字段转换还会悄悄出错。用的时候有一个非常实用的点忽略不需要拷贝的字段例如从DO转VO时不想带出createTime。Spring版本提供了入参忽略字段的重载BeanUtils.copyProperties(source, target, createTime, updateTime);但要注意忽略逻辑按字段名来不是按类型。如果两个对象字段名相同但语义不同比如source里的amount单位是分target里的amount单位是元千万别用BeanUtils老老实实手写转换或在工具里做单位换算。4.3 MapStruct编译期生成的“最优解”如果项目字段流转非常多对性能也有要求我强烈建议考虑MapStruct。它的原理是在编译期自动生成一个Mapper实现类调用它的方法本质是直接赋值不经过反射所以性能接近手写。Mapper(componentModel spring) public interface UserConvertor { UserVO toVO(UserDO userDO); Mappings({ Mapping(source cmdno, target commandNo), Mapping(target createTime, ignore true) }) ChannelRequest toRequest(ChannelConfigDO configDO); }使用时的注意点第一需要在pom里引入mapstruct-processor并和lombok同时使用时注意annotationProcessorPaths的顺序否则会出现“找不到getter/setter”的诡异报错。第二多个源对象合成一个目标时Mapping参数要写完整对象名例如toVO(UserDO userDO, RoleDO roleDO)当多个源里有同名字段时需要显式指定。第三List拷贝同样是一行方法声明MapStruct自动生成遍历逻辑这个体验非常舒服。4.4 Bean拷贝容易踩的坑我整理四个高频坑。第一空指针与null策略。Spring BeanUtils拷贝时如果源对象某个字段为null默认会把null覆盖到目标字段目标里已有的值会被清空。做更新操作时我通常先查DB得到已有对象target再拷贝入参到target如果入参某个字段没传DB老数据就被覆盖成null了。解决办法是写一个工具方法用PropertyDescriptor遍历目标字段只拷贝源对象里非null的字段。第二字段类型不一致。source是Integertarget是LongSpring BeanUtils的默认行为是直接抛异常还是忽略不同版本不一致。我在Spring 5.3.x上实测过会尝试类型转换转换失败抛异常转换成功则正常赋值。所以涉及类型不一致时不要依赖默认行为建议显式转换或用MapStruct指定转换方法。第三集合与嵌套对象。BeanUtils只做浅拷贝target里的嵌套List或Map引用会直接指向source的同一份引用。一旦你改了target里的嵌套对象source也会被改。想深拷贝就得自己处理或者用序列化方式但序列化方案性能差一般业务场景不推荐。第四字段名别看错。doghouse和dogHouseidea和Id这类大小写差异会让“按名拷贝”直接失败。排查方法很简单打开两个类用IDE的Compare功能过一遍字段名或者干脆写单元测试把所有字段对齐关系断言一遍比上线后发现问题强得多。5. 三招的边界什么时候不适合用5.1 过度设计的危险信号没有银弹三招也各有代价。工厂模板会增加类层次注解反射会引入元编程的调试难度Bean拷贝会把字段映射关系“藏”到底层。用它们的最终目的只有一个用可维护性换掉重复代码。如果系统里只有两个固定渠道、满足十年不变量你和我说要用工厂和模板方法我会劝退你。类多、调用链深、新人理解成本高就是过度设计。我见过一个极端案例一个只有三个字段的内部状态枚举硬被套了策略工厂每个枚举值一个类还配了注解扫描。结果后续扩展没有发生维护的人每次都得翻七个类才能改一个数字。这类场景最合适的做法就是一组HashMap初始化或一个switch最多加上枚举的自身方法。5.2 适用范围速查表场景推荐技术理由多渠道接入、多类型处理工厂模板方法业务分支收敛、流程统一参数校验、脱敏、日志注解反射/AOP横切逻辑与业务解耦DO/VO/DTO转换、字段复制Bean拷贝减少样板代码、字段变化集中处理单一简单判断分支if-else不要为了模式而模式字段有业务语义转换分转元等手写转换器显式表达业务规则高频热点路径的拷贝MapStruct编译期生成、性能接近手写还有一个容易被忽略的维度团队习惯。如果团队里没有人熟悉反射和AOP强行上注解反射出现问题的时候可能没人能接手。我通常建议先在代码评审和小范围模块试点写出配套的《使用规范》再逐步推广而不是一把梭推全量。5.3 三招如何配合使用这三招不是孤立的。实战里最常见的组合是工厂负责拿到正确的处理器模板方法固定处理流程注解反射处理横切关注点脱敏、校验Bean拷贝在入参出参转换中消除样板代码。一个典型的接口链路长这样Controller接收外部请求。用Bean拷贝把外部DTO转成内部Command。工厂根据业务类型取到对应处理器。处理器走模板方法校验、业务处理、结果组装。返回前注解反射切面对结果做统一脱敏或字段补充。这样一来重复代码不是被“删掉”了而是被“收纳”到固定的结构里。对新人来说看代码的路径从“翻十个if分支”变成了“看一个工厂、看一个抽象类、看自己的子类”效率完全不一样。6. 实战复盘一次支付渠道对接的重构实录6.1 重构前的痛点标题里说的“彻底告别冗余代码”如果只是理论肯定没说服力。所以我复盘一个真实项目我们要新增一个支付渠道此时老代码里已经有四家渠道每个渠道都以“渠道名Service”类存在。随便打开一个WechatPayService里面是几百行的流程签名字段拼装、请求对象创建、回调验签、结果落库全部揉在一起。新增第五家时代码评审的人叹了口气说要复制改半小时。不只是类内重复类间更严重。wechatPayService和aliPayService里都有60%的逻辑长得一样请求摘要日志、异常重试、结果状态更新、回调幂等表写入。这些逻辑用复制粘贴实现导致改支付结果状态机时四份代码改了三个差点线上出事故。这个事件让我下决心做一次结构性重构。6.2 重构设计模板工厂落实支付流程我先抽出抽象父类AbstractPayHandler流程固定为构造请求参数、调用上游、解析响应、更新本地订单、写入回调流水。五个步骤里前三步由子类差异实现后两步在父类统一提供。这样一来状态机和幂等逻辑只出现在父类里不会再出现四份拷贝。工厂侧我用渠道编码作为key把五个子类注册到Map。注册后调用代码变成AbstractPayHandler handler PayHandlerFactory.get(channel); PayResponse resp handler.execute(new PayRequest(order));新增渠道时新写一个子类实现构造请求和解析响应注册渠道编码。最核心的业务参数处理从四份变成一份逻辑归属非常清楚。6.3 注解反射处理渠道参数差异渠道间的参数差异比想象中多有的渠道要商户号有的要子商户号有的要terminalId有的还要额外的扩展字段。我最初想把差异写进子类里结果每个子类构造请求时都有一大段if-else。后来优化成注解驱动定义一个渠道参数注解ChannelField标注在渠道专用DTO响应字段上处理器根据渠道号自动把对应字段填充进统一请求对象。实现思路不复杂渠道子类里维护一个Mapkey是字段名value是值处理器反射遍历统一请求对象遇到标了ChannelField的字段就从Map里取值并设置。这个方案真正起到了“一注解代替一坨if-else”的效果。而且接入新渠道时渠道对接人员只需要在DTO里标注解不用动处理器的调度逻辑。6.4 Bean拷贝在接入层的使用请求和响应的对象转换是另一个样板代码温床。每个渠道的回调请求参数都长得很像却各有特殊字段。我先用Spring BeanUtils做公共字段拷贝再用MapStruct生成一个Adapter把渠道特有字段显式映射到统一对象。这里特别强调一个细节我当时把统一对象的公共字段抽象成BaseCallback每个渠道回调DTO继承它。BeanUtils只负责拷贝BaseCallback的公共字段特殊字段走MapStruct的Mapping。改造后新增渠道回调接入只需写一个Adapter接口、标几个Mapping注解不需要手工拼接对象回调解析代码量下降了约40%。6.5 重构收益与验证重构完成后我统计了一下收益代码行数四家渠道支付核心流程整体减少约35%。新增渠道耗时从2天压缩到半天以内。状态机问题后续又调整过两次支付状态流转只改了父类测试覆盖一遍全渠道没有出现回归遗漏。事故率重构后半年内渠道接入相关线上故障为0。验证方式是每个渠道都跑了集成测试正常支付、超时重试、回调签名错误、重复回调四种场景。因为父类统一了状态机测试用例也收敛成一套基础用例加各渠道差异用例维护成本比原来大大降低。7. 常见问题与避坑清单7.1 高频QA问工厂模板方法会不会让类爆炸答会所以要先确认分支是否真的会持续增长。如果确定渠道数长期小于5可以只做模板工厂用Map简单注册就行。类爆不爆炸取决于你有没有扛住“每个渠道一个子类”的诱惑。差异极小的时候让一个子类持有参数配置远比多个子类更合适。问注解反射在微服务里怎么传递答注解是编译期附着在类上的服务间远程调用时不会传递只有本地反射才能读到。微服务场景通常是把需要脱敏的字段在出参DTO上标注解在网关或服务端处理后再返回不存在跨服务传注解的问题。问BeanUtils在JDK 17和Spring Boot 3.x下还能用吗答能用。Spring 6的BeanUtils底层已经适配模块系统通过构造器和属性描述符操作。但如果字段是private且模块未开放仍可能有权限问题建议配合--add-opens或者改用MapStruct。问三招适合在现有老项目里直接推广吗答不建议一把梭。老项目里优先找新增模块或变更频繁的渠道做试点把试用规范写好代码评审盯着用跑稳了再扩大范围。硬改已有稳定模块风险和收益不成正比。还有问反射缓存怎么做的我给一个简单公式用一个静态ConcurrentHashMapkey是Classvalue是Field数组或Method数组查一次class后缓存起来。注意缓存的是元数据不是对象实例避免内存泄漏。7.2 踩坑清单按严重程度排序模板父类execute不是final子类一改顺序全乱。子类构造器里初始化状态由于初始化顺序父类调用钩子时状态还没准备好。Spring AOP自调用导致注解不生效同一个类内部方法互调绕过了代理。反射拿到父类字段失败没有做递归遍历。BeanUtils更新操作覆盖null没有做非空判断老数据被清空。MapStruct与Lombok一起使用时注解处理器顺序混乱报错找不到getter。工厂Map用HashMap在并发环境下被并发写要用ConcurrentHashMap。注解Retention选错为SOURCE或CLASS运行时反射读不到。7.3 团队推广的小建议我建议把这三招写进团队的开发规范里但规则写得克制些优先推荐不强制。可以给三条硬性要求新增渠道/多类型分支必须用工厂或Map统一管理不允许新增if-else链超过三层对外返回对象涉及手机号、身份证等敏感字段必须用注解脱敏DO转VO禁止手写超过十个Setter。这三条可Review、可自动化扫描推行阻力小。8. 最后想说的我的体感这三招用下来我最大的体会不是“代码变少了”而是“改代码的胆子变大了”。以前改支付状态机要小心翼翼地把四个类打开对照怕漏改现在改父类一个方法再来一轮集成测试就能比较放心地发布。这种安全感才是消灭重复代码真正的红利。最后再分享一个小技巧重构时别一步到位。我通常先把复制粘贴最狠的一段代码抽成模板跑通测试再把分支案件收进工厂再跑通最后再引入注解或Bean拷贝。每一步都是独立的、可回滚的这样即使中途出问题也能快速退回到上一步不会把项目拖入“大规模重构翻车”的窘境。如果有机会你可以找一个渠道类型比较多的旧模块按这个顺序试一次。先用模板方法把重复流程固定下来再看哪些地方能收成注解最后把对象转换全部交给Bean拷贝。实践一次比读十篇设计模式文章都管用。