共用代码处理从复制粘贴到公共模块的工程化治理指南

发布时间:2026/9/8 1:54:32
共用代码处理从复制粘贴到公共模块的工程化治理指南 如果你在一个后端团队里待过一两年大概率见过这样的场景A 服务里的统一返回体是自己写的B 服务也有一份C 服务从 B 复制过来又改了两行。三个服务的接口返回格式看起来差不多却又不完全一致。等到前端同学问“为什么有的接口返回 code0有的返回 code200”时整个后端组就要花掉一个下午去对齐这些差异。共用代码处理的问题几乎每个项目组都会遇到但很少有人把它当成一个正经的工程问题来看。很多人觉得代码复用嘛把公共逻辑抽出来复制到各个项目里不就行了真正做起来才发现难点根本不在“抽取”这一步。难点在于抽取之后的事情谁维护、怎么升级、怎么让团队成员愿意用、怎么避免公共模块膨胀成一个什么都有的大杂烩。这篇文章是“共用代码处理”系列的第一期。今天先不急着谈具体框架而是把这个问题完整地摊开共用代码到底有哪些形态常见的处理方式各自有什么优缺点从“复制粘贴各改各的”过渡到“独立公共模块按版本引用”应该怎么走如果你是后端开发、前端开发或技术负责人这篇文章会帮你判断团队当前处于哪个阶段并找到下一步可以落地的方向。1. 这篇文章真正要解决的问题先说现象。共用代码处理失控通常会有几个典型症状。第一重复代码散落在各个业务项目里每个项目都有自己的工具类。比如日期格式化三个服务三套写法有的用yyyy-MM-dd HH:mm:ss有的用DateTimeFormatter有的直接拼字符串。表面上看没什么问题一旦需求变化比如统一改成 ISO 8601 格式就要改三份甚至更多而且改的过程中很容易出现遗漏。第二公共代码放错了位置。有的团队把公共工具类放在某个业务模块下面另一个项目要复用只能通过 HTTP 接口调用或者直接复制源文件。这样做短期内省事长期会让模块之间的依赖关系变得混乱业务模块和基础组件高度耦合。第三公共包升级难。团队好不容易建了一个公共模块发布到了私有制品库但真正升级的人很少。问起来就是“升级怕有问题”“那个包改动太大”“我们项目用的版本比较老”。时间一长公共模块的新功能没人用旧版本没人维护公共代码又成了新的技术债。第四公共模块膨胀。也有一些团队走向另一个极端什么代码都往公共模块里塞工具类、配置类、业务常量、数据库访问对象全放一起。结果公共模块的依赖越来越重发布一个公共包带动十几个项目一起升级团队怨声载道。这篇文章要解决的核心问题就是把共用代码处理拆解成三个层面来看边界怎么划分、版本怎么管理、变更怎么通知。这三个层面互相影响任何一层没做好共用代码都会从“提效工具”变成“维护负担”。适合读这篇文章的读者有三类后端或前端开发经常要在多个业务项目中搬代码、改重复逻辑想知道怎么更优雅地抽公共模块。架构师或技术负责人正在从单体项目走向多项目协作需要给团队建立一套公共代码的治理规范。刚接触工程化不久的同学想理解为什么企业里一般不用“复制粘贴”来处理公共代码。先想清楚这些问题后面再讨论具体技术方案才有意义。2. 共用代码的基础概念与常见形态在展开技术方案之前先把“共用代码”这个概念说清楚。共用代码是指被两个或两个以上业务模块、项目或团队共同使用的代码。它的核心特征有两个一是“被复用”二是“与具体业务弱相关”。被复用意味着它不能只为一个业务场景服务与业务弱相关意味着它的变化频率应该比业务代码低。如果一个类虽然是公共的但几乎每个版本都在改动那它很可能不是真正的公共代码而是被放错位置的业务逻辑。判断一段代码是否适合作为共用代码有一个很实用的标准如果它包含特定业务的字段、规则或流程那它就不该放在公共模块里。比如返回体ResultT通常是公共的但里面如果加了一个业务专用的字段比如orderStatus那就变味了。这个标准和团队规模无关与代码的抽象层次有关。常见的共用代码形态可以分成几类形态典型例子通常的存放位置工具类日期格式化、字符串处理、加密摘要公共模块或各项目内部通用基础组件统一返回体、全局异常处理、通用日志切面公共模块客户端SDK封装Redis客户端、OSS存储封装、外部系统对接独立仓库 制品库配置模板application.yml 统一模板、Dockerfile 模板配置仓库或脚手架前端公共组件和工具函数请求封装、通用表单组件、权限指令npm 私服或 Monorepo这里要特别注意一组容易混淆的概念公共代码和业务公共代码。纯公共代码通常指与技术实现有关的通用逻辑比如日期工具、字符串工具、网络请求封装。这类代码在任何业务项目中都可以复用抽取起来的阻力最小。业务公共代码则是指多个业务模块共同使用但承载着业务规则的代码比如登录状态校验、订单号生成规则、用户权限判断。这类代码抽取时需要格外谨慎因为它不是单纯的“技术复用”而是把业务规则从一个地方挪到另一个地方。一旦多个团队共享就需要一个明确的业务负责人来维护否则很容易出现“这个公共类被改了结果 A 业务正常、B 业务出错”的问题。很多团队在公共代码治理上的失败根源就在于把这两类混为一谈。他们以为把所有重复的代码都抽到公共包里就完成了复用却没有为“业务公共代码”建立责任边界。效果上技术公共代码确实减少了重复但业务公共代码反而增加了协调成本。3. 处理共用代码的典型方式与演进路径共用代码的处理方式并不是从第一天就设计好的。绝大多数团队都走过一条从“复制粘贴”到“公共模块”的演进路径。3.1 复制粘贴这是项目初期最常见的方式。某个项目需要一段日期格式化代码另一个项目也有类似需求直接把代码复制过去。优点是见效快不需要任何额外设施缺点是维护成本随项目数量线性增长。只要公共逻辑发现 bug所有复制过这段代码的项目都要手动修改修改过程中还可能因为复制时改了部分代码而产生差异。3.2 项目内公共包当一个项目内部有多个模块时可以把公共代码抽到一个独立的模块比如 Maven 多模块工程里的 common-core。这种方式解决了同一个项目内部模块间的代码重复问题但没有解决跨项目复用的问题。不同项目之间如果都要用仍然要靠复制或引入源码。3.3 独立仓库 二进制制品库这是当前后端团队比较成熟的做法。公共代码放到独立的 Git 仓库通过 CI/CD 构建后发布到私有制品库比如 Nexus、Artifactory 或打包工具自带的私有仓库。业务项目只需要在构建配置里声明依赖和版本号比如 Maven 的 dependency、npm 的 dependencies。这样做的好处是版本清晰、升级可控、依赖关系明确团队可以针对公共模块单独做测试和评审。3.4 MonorepoMonorepo 是把多个项目放在同一个代码仓库中管理使用统一工具链进行构建和测试。这种模式适合组织规模较大、模块之间变更非常频繁的团队。优势是全局重构和变更跟踪很方便因为所有代码都在一个仓库里劣势是仓库体积会快速增长权限控制粒度变粗对 CI/CD 和代码评审的要求更高。四种方式的对比如下处理方式维护成本版本管理适合阶段主要风险复制粘贴最高无原型验证、小团队早期重复维护、改了一处漏一处项目内公共包中跟随项目版本单体应用多模块无法跨项目复用独立仓库 制品库低语义化版本多项目协作、微服务需要治理机制否则容易膨胀Monorepo中统一提交历史大规模团队、频繁跨模块变更仓库体量和权限管理压力大没有哪一种方式是绝对正确的。选择的标准取决于团队规模和项目数量。项目数量少、迭代速度快的时候项目内公共包已经够用当项目数量上来、跨项目协作成为常态独立仓库加制品库是更稳妥的演进方向。这里想强调一个判断不要为了追求“标准化”而一上来就搭建复杂的公共代码平台。如果团队的重复代码总共只有几十处花两周时间搭一套公共仓库和制品库流程投入产出比并不高。先盘点、再分层、最后工具化才是成本更低的路径。4. 核心流程拆解从复制粘贴到公共模块接下来用一套可落地的流程演示如何把分散在多个项目里的重复代码逐步收敛成一个公共模块。这里以后端项目为例使用 Maven 作为构建工具。前端项目流程类似只是把制品库从 Maven 换成 npm 私服。4.1 先盘点再动手不要一开始就建公共模块。第一步应该是盘点把各个项目里的工具类、通用组件列出来按“被复用的次数”和“是否包含业务规则”两个维度分组。可以用下面这个检查清单来判断一段代码是否适合抽成共用代码 1. 是否被两个以上模块或项目直接使用 2. 是否包含特定的业务字段、业务规则或业务流程 3. 是否有对外提供稳定接口的必要 4. 是否依赖了具体业务项目中的数据库表或外部服务 5. 是否可以在不修改业务代码的前提下独立测试如果问题的答案中第 1 条和第 5 条为“是”第 2 条和第 4 条为“否”那这段代码就适合放进公共模块。如果第 2 条或第 4 条为“是”就要谨慎先和业务方确认这段逻辑是否稳定再由业务团队指定负责人。上一次我们团队做代码整理时就是用这个清单过滤掉了十几个看似共但实际带业务规则的类避免公共包一开始就背上业务包袱。4.2 创建公共模块工程假设我们决定把统一返回体ResultT和日期工具类DateUtils收敛到公共模块中。先创建一个 Maven 多模块工程作为公共模块的载体。!-- 文件路径common-parent/pom.xml -- project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdcommon-parent/artifactId version1.0.0/version packagingpom/packaging modules modulecommon-core/module /modules /project再创建 common-core 模块它的 packaging 是默认的 jar。groupId 建议带上公司或组织的统一前缀artifactId 建议做到见名知意比如 common-core 代表核心通用代码common-web 可以代表和 Web MVC 相关的通用组件。这样后续按模块选择依赖时团队能快速判断该引什么。公共模块的源码放在哪里可以是 GitHub/Gitee 的私有仓库也可以是公司自建的 GitLab。关键是源码仓库和制品库要分开管理源码仓库管“改什么”制品库管“发布什么版本”。很多团队把公共代码放在某个业务仓库里这种做法会让代码评审和发布权限都变得混乱。4.3 编写一个真实的公共工具类下面是一个最简的ResultT统一返回体。实际项目里可以按自己的接口规范扩展但要注意一点不要在里面塞业务字段。// 文件路径common-core/src/main/java/com/example/common/core/result/Result.java package com.example.common.core.result; public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 0; result.message success; result.data data; return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } public int getCode() { return code; } public String getMessage() { return message; } public T getData() { return data; } }再写一个日期工具类。日期格式化看起来简单实际踩坑很多比如线程安全问题SimpleDateFormat在多线程环境下会有状态问题所以公共工具类可以直接用 Java 8 的DateTimeFormatter// 文件路径common-core/src/main/java/com/example/common/core/util/DateUtils.java package com.example.common.core.util; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public final class DateUtils { private static final DateTimeFormatter DEFAULT_FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); private DateUtils() { } public static String formatNow() { return LocalDateTime.now().format(DEFAULT_FORMATTER); } public static String format(LocalDateTime dateTime) { if (dateTime null) { return null; } return dateTime.format(DEFAULT_FORMATTER); } }这段代码看起来简单但已经包含了两个公共代码设计原则工具类构造方法私有化避免被实例化时间格式化使用线程安全的DateTimeFormatter避免并发问题。4.4 配置发布与依赖引用公共模块写完之后需要发布到私有制品库。以 Nexus 为例在 common-core 的 pom.xml 中配置仓库地址!-- 文件路径common-core/pom.xml 中新增 distributionManagement 配置 -- distributionManagement repository idnexus-releases/id urlhttp://nexus.example.com/repository/maven-releases//url /repository snapshotRepository idnexus-snapshots/id urlhttp://nexus.example.com/repository/maven-snapshots//url /snapshotRepository /distributionManagement注意这里的 URL 只是示例实际项目中要替换成团队自己的制品库地址。发布时执行mvn clean deploy -DskipTests发布成功后业务项目里就可以按版本引用了dependency groupIdcom.example/groupId artifactIdcommon-core/artifactId version1.0.0/version /dependency到这一步共用代码已经完成了从“复制粘贴”到“公共模块按版本引用”的转变。但这里只是解决了“怎么发布”的问题后面还有更重要的“怎么维护”和“怎么升级”。5. 共用代码的版本管理与升级策略很多团队接入公共模块后第一个遭遇战就是版本混乱。不同项目引用不同版本公共模块发了一个新版本有的项目跟进了有的项目不敢动最终又退回到“各自维护一份”的老路。为什么会出现这种情况核心原因是公共代码的版本管理比业务项目内部的依赖管理要求更高。业务项目只有一个版本在演进公共模块则可能同时被几十个项目引用每一次发布都相当于对下游做一次变更推送。如果这个变更没有节奏、没有兼容性保障下游项目自然不敢升级。5.1 采用语义化版本公共模块必须使用语义化版本格式是主版本号.次版本号.修订号。1.2.3 主版本号不兼容的 API 变更 次版本号向后兼容的功能新增 修订号向后兼容的问题修复这个规则的意义在于下游项目看到版本号就知道能不能安全升级。修订版本可以放心升次版本一般可以升主版本变更就要安排专门的工作量来适配。5.2 兼容性是一种承诺公共代码一旦发布方法签名、返回格式、异常行为都变成了一种对外承诺。如果公共方法要改名、返回值要从String改成LocalDateTime不能直接改完就发新版本而是要先在旧方法上标注Deprecated保留一个版本的过渡期再在下一个主版本移除。很多团队忽略这一点认为公共模块的代码是自己团队写的想改就改。但公共模块的下游不只是自己团队还可能是其他业务团队甚至是外部合作方。改一个公共方法影响范围是几何级扩散的。所以公共模块的评审门槛应该比业务代码更高任何对公有 API 的改动都要经过评审。5.3 维护一份变更记录公共模块一定要维护 CHANGELOG内容是每个版本做了什么、有没有破坏性变更、升级时需要注意什么。下面是一个示例# common-core 变更记录 ## [1.2.0] - 2025-03-10 ### 新增 - Result 增加 traceId 字段用于链路追踪 ### 升级注意 - 该字段默认为空不影响现有接口 ## [1.1.0] - 2025-01-20 ### 新增 - 新增 DateUtils 类统一日期格式化 ## [1.0.0] - 2024-12-01 ### 初始发布 - 统一返回体 Result这个文件要和代码一起评审、一起发布。下游项目升级前先看 CHANGELOG是减少升级恐惧最直接的办法。5.4 升级节奏要独立于业务迭代公共模块的升级节奏要和业务迭代分开。不要因为在某个项目中发现了公共代码的 bug就直接在那个项目里改改完再复制到公共模块。正确的顺序是在公共模块中修复发布新版本再由各个业务项目按自己的节奏升级。这个过程一开始会显得慢尤其是团队还在追业务进度时大家会觉得“直接在项目里改更快”。但只有当公共代码升级变成一条固定的、可预期的流水线团队才不至于在下一次需求变更时被反复拖进重复修改的泥潭。6. 完整示例一个多项目共用工具包的最小实践为了让你更直观地理解整套流程这里把上面的内容串成一个最小实践。假设有三个后端服务order-service、user-service、pay-service它们都需要使用统一的返回体和日期工具。目标是让这三个服务从各自的复制粘贴版本迁移到引用 common-core 公共包。完整目录结构如下common-parent/ ├── pom.xml └── common-core/ ├── pom.xml └── src/main/java/com/example/common/core/ ├── result/Result.java └── util/DateUtils.java接下来按顺序执行。第一步创建公共工程。把 4.2 和 4.3 中的 pom.xml 和 Java 类放到对应目录。第二步本地执行测试打包mvn clean install这里只用 install 验证能不能正常构建。验证通过后再执行 deploy 发布到私有制品库。第三步在 order-service 中替换原有返回体。如果原来的 Result 类在 order-service 内部删除本地类然后在 pom.xml 中添加 common-core 依赖。编译后检查所有引用了 Result 的代码是否报错。第四步运行回归测试mvn test预期结果是所有测试正常通过。如果有测试依赖了原 Result 的某个字段或方法说明公共包的 API 和业务项目需求不一致需要回到公共模块补全 API再发布新版本而不是在业务项目里临时改公共包源码。第五步在 user-service 和 pay-service 中重复第三步和第四步。关于验证结果有两点要说明。一是公共包引入的编译错误不可能完全避免重点是让错误集中在“API 不匹配”上而不是“代码逻辑被复制错了”。二是如果三个服务都验证通过说明公共包的 API 设计基本合理可以继续推广。这套流程并不复杂但很多团队倒在了第一步和第二步之间代码抽出来了却没有人愿意把它从本地仓库搬到共享制品库。实际上只要建立一次发布流水线把mvn clean deploy挂到 CI/CD 上后续发布就只是打一个 Git 标签、触发一次构建的事。7. 常见问题与排查思路公共模块在落地过程中会碰到很多细节问题。下表汇总了高频问题并给出排查方式。问题现象可能原因排查方式解决方案业务项目引不到公共包最新代码没有发布到制品库或引用版本号没有更新在制品库中搜索当前版本是否存在用新版本号发布避免覆盖旧版本多个业务项目的公共代码不一致缺少统一载体各项目各维护一份全局搜索重复类名和方法先盘点再分阶段抽取进公共模块升级公共包后业务项目编译失败公共包存在破坏性变更查看 CHANGELOG定位编译错误遵循语义化版本破坏性变更升主版本引入公共包后服务启动失败依赖传递导致版本冲突执行 mvn dependency:tree 查看依赖树在业务项目中排除冲突依赖或统一 BOM 版本公共包体积不断膨胀缺乏评审机制业务代码也塞了进来查看公共包依赖数量和类数量建立标准超过阈值拆分模块团队不愿意升级公共包公共包不稳定、文档缺失、兼容性差统计公共包发版频率和下游停留版本稳定 API补齐文档低侵入接入公共方法出现线程安全问题工具类持有可变状态查看是否有共享 SimpleDateFormat 等实例改用线程安全类或每次调用创建实例这里单独说一下“公共包体积膨胀”这个问题。很多团队的公共模块一开始很小后来因为大家一起往里加代码半年之后就变成了一个大杂烩。要避免这个问题从一开始就要定两条规则第一新增代码到公共模块必须经过评审评审重点不是代码风格而是“这段逻辑是否真的与业务无关”第二公共模块内部也要按包名划分职责result、util、exception、constant 各归各的不要让所有类都堆在一个包里。这个习惯能帮你更容易发现“业务逻辑混进来了”的信号。8. 最佳实践与工程建议从大量项目的经验来看共用代码处理做得好不好与团队是否有明确规范高度相关。下面这些实践不是理论而是可以在下周一就落地的动作。第一给公共模块命名定规则。groupId 统一用公司域名倒写和项目区分的组合artifactId 不要包含“common”就完事最好明确指出职责比如common-core、common-web、common-cache。这样做的好处是后续看依赖列表就能知道这个模块管什么。第二最小化依赖。公共模块是底层组件尽量不要依赖具体业务的框架。比如一个工具类模块就不应该依赖 Spring MVC一个返回体组件也不应该把项目里所有 JSON 库都引进来。依赖越多下游项目受到的间接影响就越大。第三公共模块要写文档而且文档至少要包含三类README、使用示例、CHANGELOG。README 说明这个模块解决什么问题使用示例告诉我们最快怎么用CHANGELOG 说明每个版本的差异。对于后端公共包可以额外提供一个“新版本升级指引”把需要手工调整的地方列出来。第四权限和评审。公共包的提交权限要收紧不能让所有人随便往 main 分支推代码。推荐的做法是公共包仓库走 MR/PR 评审发布权限只给维护者。评审的时候除了看代码是否正确还要重点看 API 是否合理、是否会造成破坏性变更、是否真的有必要加入公共包。第五渐进式迁移。存量项目不要把公共代码一次性全抽出来建议按模块分批迁移。每期只处理一个工具类或一个组件发布一个版本验证成功后再处理下一个。这样风险可控团队也有时间适应新的依赖管理方式。第六关注公共代码的消费数据。如果条件允许可以在制品库或 CI/CD 上统计公共包的下游依赖数量。下游依赖多、升级停滞的公共包往往就是团队协作出问题的地方需要优先治理。9. 总结与后续学习方向回到开头的问题共用代码处理到底难在哪不只是“抽取代码”这一步而是抽取之后的三层工程问题——边界划分、版本管理、变更协作。边界没划清楚公共包就会变成垃圾场版本没管理好下游项目就不敢升级变更协作没建立评审机制、通知机制、升级节奏都无从谈起。这篇文章给出了从复制粘贴到独立公共模块的一套落地路径先盘点重复代码再创建公共模块发布到私有制品库最后按语义化版本管理升级。这套路径不需要特别复杂的工具链关键在团队有没有把它当成一件需要持续投入的工程事项来对待。下一步你可以从两件事开始做起一是梳理当前项目里重复出现的工具类和组件列一个候选清单二是选一个最简单、最稳定的类按文章中的流程走一遍从抽取到发布的全过程。先让公共模块跑起来再谈优化。后面的系列我会继续展开更深入的话题比如多仓库和 Monorepo 的选型对比、私有制品库的搭建与权限管理、前端 npm 公共包的落地实践以及公共模块代码评审时的具体检查清单。如果你在共用代码处理上有自己的经验或踩坑经历也欢迎在评论区补充。这套文章可以作为你推动团队公共代码治理时的一张基础地图建议收藏备用。