还重构?就你那代码只能铲了重写!CodeGuide 教你建立可维护、可扩展的 Java 代码质量体系
【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!项目地址: https://gitcode.com/gh_mirrors/code/CodeGuide
导读:绝大多数被需求"憋"出来的代码,最终都逃不过推倒重写的命运,因为重构烂代码的时间成本远超重写,而且重构还伴随着接口变更、调用方升级和线上事故的风险。本文以 CodeGuide 仓库中《还重构?就你那代码只能铲了重写!》为核心骨架,从约定规范、接口标准、库表设计、算法逻辑、职责分离、逻辑缜密、领域聚合、服务分层、并发优化、源码能力十个维度,系统讲解如何从源头写出"具备重构可能"的代码,并结合仓库内的数据库路由组件、DDD 落地案例、HashMap 源码分析等资料做纵深验证,帮你把每一次需求迭代都变成一次增量优化。
一、前言:为什么你的代码只能"铲了重写"
我们不一样,就你没对象!——对,你是面向过程编程的!
绝大多数码农没日没夜被需求憋着肝出来的代码,无论有多么吭哧瘪肚,都不可能有重构,只有重新写。为什么?因为重新写所花的时间成本,远比重构一份已经烂成团的代码要节省时间。但谁又不敢保证重写完的代码就比之前好多少,况且还要承担着重写后的代码事故风险和几乎体现不出来的业务价值。
虽然代码是给机器运行的,但同样也是给人看的。随着每次需求的迭代、变更、升级,都需要研发人员对同一份代码进行多次开发和上线,那么这里就会涉及到可维护、易扩展、好交接三个特点。
而那些不合理分层实现代码逻辑、不写代码注释、不按规范提交、不做格式化、命名随意——甚至把queryBatch写成queryBitch的代码,都会造成后续代码没法重构的问题。在 CodeGuide 仓库的 握草,你竟然在代码里下毒! 中,作者就整理过一批"有毒"代码,比如把批量查询写成queryBitchUserInfo(bitch意为"婊子"),接口是上午写的,人是下午走的。这类问题看似是个玩笑,背后却是代码可维护性坍塌的开始。
那么接下来,我们就分别介绍开发好"能重构的代码"都要怎么干。
二、约定规范:让提交、分支、注释都有据可依
代码重构的前提是"可追溯"——你能从提交记录里知道每一行代码为什么而改,能从注释里快速理解类的职责。约定规范是这一切的起点。
1. Commit message 规范
提交信息是代码历史的"目录",定义清晰的 type 前缀,能让git log/git blame直接变成可检索的变更索引:
# 提交:主要 type feat: 增加新功能 fix: 修复bug # 提交:特殊 type docs: 只改动了文档相关的内容 style: 不影响代码含义的改动,例如去掉空格、改变缩进、增删分号 build: 构造工具的或者外部依赖的改动,例如webpack,npm refactor: 代码重构时使用 revert: 执行git revert打印的message # 提交:暂不使用type test: 添加测试或者修改现有测试 perf: 提高性能的改动 ci: 与CI(持续集成服务)有关的改动 chore: 不修改src或者test的其余修改,例如构建过程或辅助工具的变动注意refactor这个 type:它专门留给重构场景。当你做的是不影响外部行为的内部结构调整时,用refactor提交,后续回溯就能把"重构"和"功能变更"清晰区分开。
2. 分支、提交、注释三件套
- 分支:开发前提前约定好拉分支的规范,比如
日期_用户_用途,如210905_xfg_updateRuleLogic,一眼就能看出谁在什么时间做了什么。 - 提交:
作者,type: desc,如小傅哥,fix:更新规则逻辑问题,参考上方 Commit message 规范。 - 注释:包括类注释、方法注释、属性注释。在 IDEA 中可以设置类注释的头信息
Editor -> File and Code Templates -> File Header,配置成统一模板:
/** * @description: * @author: ${USER} * @date: ${DATE} */3. 用 P3C 插件统一编码标准
规范光靠人记是不够的,还需要工具兜底。推荐下载安装 IDEA P3C 插件Alibaba Java Coding Guidelines,统一标准化编码方式。它能够按照阿里巴巴 Java 开发手册静态分析代码中出现的命名风格、常量定义、集合处理、并发处理、OOP、控制语句、注释、异常等各项潜在风险,并给出优化建议。CodeGuide 在 p3c 插件,是怎么检查出你那屎山的代码? 中拆解过它的原理:P3C 的规约部分基于 PMD 实现,PMD 通过 JavaCC 将 Java 源码转换成 AST 语法树,再通过 Visitor 遍历 AST 找出特定模式(即代码问题)。理解了这一点,你就知道所谓"编码规范"完全可以被工具化、被自动化——这也是高质量工程的第一步。
三、接口标准:返回结果必须有 Code 码和 Info 描述
在编写 RPC 接口的时候,返回的结果中一定要包含明确的Code码和Info描述,否则使用方很难知道这个接口是调用成功还是异常,以及是什么情况的异常。没有统一返回包装的接口,调用方只能靠"猜"和"文档同步",一旦接口内部实现变化,整个调用链都失去信任基础。
1. 定义 Result
public class Result implements java.io.Serializable { private static final long serialVersionUID = 752386055478765987L; /** 返回结果码 */ private String code; /** 返回结果信息 */ private String info; public Result() { } public Result(String code, String info) { this.code = code; this.info = info; } public static Result buildSuccessResult() { Result result = new Result(); result.setCode(Constants.ResponseCode.SUCCESS.getCode()); result.setInfo(Constants.ResponseCode.SUCCESS.getInfo()); return result; } // ...get/set }这里Constants.ResponseCode.SUCCESS是把成功/失败的结果码收敛到常量或枚举里,避免散落在业务代码中的魔法值。这种code + info的模型,在 CodeGuide 的多个实战项目(如 DDD 落地案例、知识星球对接等)中被反复使用,是分布式系统里 RPC 接口的事实标准形态。
2. 返回结果包装:继承
如果业务接口需要在标准结果之上再附加领域字段,可以通过继承扩展:
public class RuleResult extends Result { private String ruleId; private String ruleDesc; public RuleResult(String code, String info) { super(code, info); } // ...get/set } // 使用 public RuleResult execRule(DecisionMatter request) { return new RuleResult(Constants.ResponseCode.SUCCESS.getCode(), Constants.ResponseCode.SUCCESS.getInfo()); }3. 返回结果包装:泛型
如果希望结果与业务数据松耦合、可复用,可以用泛型包装:
public class ResultData<T> implements Serializable { private Result result; private T data; public ResultData(Result result, T data) { this.result = result; this.data = data; } // ...get/set } // 使用 public ResultData<Rule> execRule(DecisionMatter request) { return new ResultData<Rule>(Result.buildSuccessResult(), new Rule()); }两种接口返回结果的包装定义,都可以规范返回结果。在这样的方式包装后,使用方就可以用统一的方式来判断Code码并做出相应的处理——调用方永远只需要关心"成功/失败 + 数据"这个稳定契约,而不是每次对接都重新解析一种新的返回结构。这本身就是给"未来重构"留下的安全边界:接口契约不变,内部实现随便换。
四、库表设计:三范式与反三范式
数据库表是业务数据的地基,表结构设计得烂,代码写得再好也无从重构。三范式是数据库规范化的核心内容,通俗讲就是设计数据库表所应该遵守的一套规范,如果不遵守就会造成设计的数据库不规范,出现数据库字段冗余,数据的查询、插入等操作问题。
需要说明的是,数据库不仅仅只有三范式(1NF/2NF/3NF),还有 BCNF、4NF、5NF……不过在实际的数据库设计时,遵守前三个范式就足够了。再向下就会造成设计的数据库产生过多不必要的约束。
0NF
第零范式是指没有使用任何范式,数据存放冗余大量表字段,而且这样的表结构非常难以维护。这是最原始的"一张大表装天下"形态,任何字段变更都要动整张表,是后续一切重构噩梦的源头。
1NF
第一范式是在第零范式冗余字段上的改进,把重复字段抽离出来,设计成一个冗余数据较少便于存储和读取的表结构。同时第一范式也指出:表中的所有字段都应该是原子的、不可再分割的。例如你不能把公司雇员表的部门名称和职责存放到一个字段。需要确保每列保持原子性。
2NF
满足 1NF 后,要求表中的列都必须依赖主键,确保每个列都和主键列之间有联系,而不能间接联系,也就是一个表只能描述一件事情。需要确保表中的每列都和主键相关。典型的反例就是把订单信息和用户信息揉在同一张表里,主键语义模糊,改起来处处掣肘。
3NF
不能存在传递依赖关系:学号、姓名,到院系,院系到宿舍。需要确保每列都和主键列直接相关,而不是间接相关。3NF 的本质是把"非主属性对主键的传递依赖"拆掉,让每一张表都保持单一职责。
反三范式
三大范式是设计数据库表结构的规则约束,但在实际开发中允许局部变通:
- 有时候为了便于查询,会在如订单表冗余上当时用户的快照信息,比如用户下单时候的一些设置信息。
- 单列列表数据汇总到总表中一个数量值,便于查询的时候可以避免列表汇总操作。
- 可以在设计表的时候冗余一些字段,避免因业务发展情况多变,考虑不周导致该表繁琐的问题。
范式的目标是可维护,反范式的目标是可查询。二者的平衡点在于:频繁读、变化慢的数据可以适度冗余;核心写链路的数据一定要严守范式,避免更新异常。
五、算法逻辑:去中心化与时间复杂度
通常在实际的业务功能逻辑开发中,为了能满足一些高并发的场景,是不可能对数据库表上锁扣减库存,也不能直接 for 循环大量轮询操作的,通常需要考虑在这样的场景下怎么去中心化以及降低时间复杂度。
秒杀:去中心化
- 背景:这是一个商品活动秒杀的实现方案。最开始的设计是基于一个活动号 ID 进行锁定,秒杀时锁定这个 ID,用户购买完后就进行释放。但在大量用户抢购时,出现了秒杀分布式
独占锁后的业务逻辑处理中发生异常,释放锁失败,导致所有用户都不能再拿到锁,也就造成了有商品但不能下单的问题。 - 优化:优化独占竞态为分段静态,将
活动ID + 库存编号作为动态锁标识。当前秒杀的用户如果发生锁失败,后面的用户可以继续秒杀不受影响。而失败的锁会有 worker 进行补偿恢复,那么最终会避免超卖以及不能售卖。
这个案例体现的核心思想是:把"全局独占"拆成"分段竞争",把单点风险摊薄到多个独立单元上,同时用异步补偿兜底异常。这与后面"并发优化"小节中的去集中化思路一脉相承。
算法:反面教材
HashMap 数据获取时间复杂度在 O(1) -> O(logn) -> O(n),但经过"特殊"操作,可以把这个时间复杂度拉到 O(n):
@Test public void test_idx_hashMap() { Map<String, String> map = new HashMap<>(64); map.put("alderney", "未实现服务"); map.put("luminance", "未实现服务"); map.put("chorology", "未实现服务"); map.put("carline", "未实现服务"); map.put("fluorosis", "未实现服务"); map.put("angora", "未实现服务"); map.put("insititious", "未实现服务"); map.put("insincere", "已实现服务"); long startTime = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { map.get("insincere"); } System.out.println("耗时(initialCapacity):" + (System.currentTimeMillis() - startTime)); }这是一个定义HashMap存放业务实现 key、通过 key 调用服务的功能。但这里的 key,只有insincere有用,其他的都是未实现服务。问题在哪里?
- 它的目的就一个:要让所有的 key 形成一个链表放到 HashMap 中,而且把有用的 key 放到链表的最后,增加 get 时的耗时!这点代码乍一看没什么问题,看明白了就是代码里下砒霜。
- 首先,
new HashMap<>(64);为啥默认初始化 64 个长度?因为默认长度是 8,插入元素时,当链表长度为 8 的时候会进行扩容和链表树化判断,此时就会把原有的 key 散列了,不能让所有 key 构成一个时间复杂度较高的链表。 - 其次,所有的
key都是刻意选出来的,因为它们在HashMap计算下标时下标值都为 0,idx = (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16)),这样就能让所有key都散列到同一个位置进行碰撞。而且单词insincere的意思是"不诚恳的、不真诚的"! - 最后,前 7 个 key 其实都是废 key,不起任何作用,只有最后一个 key 有服务。那么就可以在 HashMap 中建出来很多这样耗时的碰撞链表,当然要满足
0.75的负载因子,不要让 HashMap 扩容。
这个案例的"反向"价值在于:它逼着你真正理解 HashMap 的扰动函数、初始化容量、负载因子、链表树化这套机制——而正是这些机制,构成了后面数据库路由组件的设计蓝本。
其实很多算法包括:散列、倒排、负载等,都可以用到很多实际的业务场景中,包括人群过滤、抽奖逻辑、数据路由等方面。这些功能的使用可以降低时间复杂度,提升系统的性能,降低接口响应时长。
六、职责分离:模板方法模式让业务逻辑可扩展
为了让程序的逻辑实现更具有扩展性,通常我们都需要使用设计模式来处理各个场景的代码实现结构。而设计模式的使用在代码开发中的体现也主要为接口的定义、抽象类的包装和继承类的实现,通过这样的方式来隔离各个功能领域的开发,以此保障每次需求扩展时可以更加灵活地添加,而不至于让代码因需求迭代而变得更加混乱。
案例:规则执行链
public interface IRuleExec { void doRuleExec(String req); } public class RuleConfig { protected Map<String, String> configGroup = new ConcurrentHashMap<>(); static { // ... } } public class RuleDataSupport extends RuleConfig{ protected String queryRuleConfig(String ruleId){ return "xxx"; } } public abstract class AbstractRuleBase extends RuleDataSupport implements IRuleExec{ @Override public void doRuleExec(String req) { // 1. 查询配置 String ruleConfig = super.queryRuleConfig("10001"); // 2. 校验信息 checkRuleConfig(ruleConfig); // 3. 执行规则{含业务逻辑,交给业务自己处理} this.doLogic(configGroup.get(ruleConfig)); } /** * 执行规则{含业务逻辑,交给业务自己处理} */ protected abstract void doLogic(String req); private void checkRuleConfig(String ruleConfig) { // ... 校验配置 } } public class RuleExec extends AbstractRuleBase { @Override protected void doLogic(String req) { // 封装自身业务逻辑 } }这是一种模板方法模式结构的定义,使用到了接口实现、抽象类继承。可以看到在AbstractRuleBase抽象类中,负责完成整个逻辑调用的定义,并且这个抽象类把一些通用的配置和数据使用单独隔离出去(RuleConfig、RuleDataSupport),而公用的简单方法放到自身实现,最后是关于抽象方法doLogic的定义和调用,业务类RuleExec就可以按需实现自己的逻辑功能了。
这套结构带来的重构价值非常直接:流程骨架(查询配置 → 校验 → 执行)被固定下来,业务差异被收敛到唯一的扩展点doLogic里。新增一种规则,只需要新增一个RuleExec的实现类,完全不用动骨架代码。CodeGuide 的 重学 Java 设计模式 系列(工厂方法、抽象工厂、建造者、原型、单例、适配器、桥接、组合、装饰器、外观、享元、代理、责任链、命令、迭代器、中介者、备忘录、观察者、状态、策略、模板、访问者)对这类模式有完整的实战讲解,是建立"可重构代码"设计直觉的最佳训练材料。
七、逻辑缜密:线上事故的典型套路
你的代码出过线上事故吗?为什么出的事故,是"树上有十只鸟开一枪还剩几只"的问题吗?比如枪是无声的吗、鸟聋吗、有怀孕的吗、有绑在树上的鸟吗、边上的树还有鸟吗、鸟害怕枪声吗、有残疾的鸟吗、打鸟的人眼睛花不花……
实际上,你的线上事故基本围绕在:数据库连接和慢查询、服务器负载和宕机、异常逻辑兜底、接口幂等性、数据防重性、MQ 消费速度、RPC 响应时长、工具类使用错误等等。下面举一个真实案例:用户积分多支付,造成批量客诉。
- 背景:这个产品功能的背景可能很大一部分研发都参与开发过,简单说就是满足用户使用积分抽奖的一个需求。最初设计的流程,是通过 RPC 接口扣减用户积分,扣减成功后进行抽奖。但由于当天 RPC 服务不稳定,造成 RPC 实际调用成功、但返回超时失败。而调用 RPC 接口的 uuid 是每次自动生成的,不具备调用幂等性,所以造成了用户积分多支付现象。
- 处理:事故后修改抽奖流程,先生成待抽奖的抽奖单,由抽奖单 ID 调用 RPC 接口,保证接口幂等性。在 RPC 接口失败时由定时任务补偿的方式执行抽奖。流程整改后发现,补偿任务每周发生 1~3 次,那么也就证明了 RPC 接口确实有可用率问题,同时也说明很久之前就有流程问题,但由于用户客诉较少,所以没有反馈。
这个案例揭示了"逻辑缜密"的三个关键动作:
- 幂等设计前置:凡是 RPC / MQ 这类可能"成功但超时"的调用,必须用业务单号(而不是每次随机生成的 uuid)作为幂等键。
- 状态先行:先落"待抽奖"单据、再执行外部扣减,把一次不可靠的远程调用变成"本地状态机 + 异步补偿"的可靠流程。
- 异常要有兜底:定时任务补偿不是可选项,而是分布式环境下 RPC 可用率问题的标准答案——它也同时证明了"没有补偿机制的原流程"其实一直在产生脏数据。
八、领域聚合:拒绝泥球小单体
不够抽象、不能写死、不好扩展,是不是总是你的代码,每次都像一锤子买卖,完全是写死的、绑定的,根本没有一点缝隙让新的需求扩展进去?
为什么?因为很多研发写出来的代码都不具有领域聚合的特点。当然这并不一定非得是在 DDD 的结构下,哪怕是在 MVC 的分层里,也一样可以写出很多好的聚合逻辑,把功能实现和业务的调用分离开。
依靠领域驱动设计(DDD)的设计思想,通过事件风暴建立领域模型,合理划分领域逻辑和物理边界,建立领域对象及服务矩阵和服务架构图,定义符合 DDD 分层架构思想的代码结构模型,保证业务模型与代码模型的一致性。通过上述设计思想、方法和过程,指导团队按照 DDD 设计思想完成微服务设计和开发:
- 拒绝泥球小单体、拒绝污染功能与服务、拒绝一加功能排期一个月
- 架构出高可用极易符合互联网高速迭代的应用服务
- 物料化、组装化、可编排的服务,提高人效
CodeGuide 的 DDD 专题案例一《初识领域驱动设计 DDD 落地》 给出了可落地的分层形态:
- 接口层(interfaces):处理用户发送的 Restful 请求和解析用户输入的配置文件等,并将信息传递给应用层。
- 应用层(application):表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,对外提供粗粒度的服务。
- 领域层(domain):领域服务封装了核心的业务逻辑;实体自身的行为在实体类内部实现,向上封装成领域服务暴露。原则上禁止跨聚合的领域服务调用和跨聚合的数据相互关联。
- 基础层(infrastructure):为各层提供资源服务(如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务逻辑的影响;主要为仓储服务,通过依赖反转的方式为各层提供基础资源服务。
而在 架构的本质之 DDD 架构 中,这套思想被进一步工程化为xfg-frame-api / app / domain / infrastructure / trigger / types的模块划分,领域层内再拆分为model(聚合、实体、值对象)、repository(仓储服务)、service(服务设计)。无论采用哪种落法,核心目标只有一个:在分治层面合理切割问题空间为更小规模的若干子问题,做到高内聚、低耦合,让需求扩展落在"新增领域/新增聚合"而不是"修改大泥球"上。
九、服务分层:把频繁变化的业务与沉淀的功能剥离开
如果你想让你的系统工程代码可以支撑绝对多数的业务需求,并且能沉淀下来可以复用的功能,那么基本你就需要在做代码开发实现的时候,抽离出技术组件、功能领域和业务逻辑这样几个分层。不要把频繁变化的业务逻辑写入到各个功能领域中,应该让功能领域更具有独立性,可以被业务层串联、编排、组合实现不同业务需求。这样你的功能领域才能被逐步沉淀下来,也更易于每次需求扩展。
这是一个简化的分层逻辑结构:有聚合的领域、SDK 组件、中间件和代码编排,并提供一些通用共性凝练出的服务治理功能。通过这样的分层和各个层级的实现方式,就可以更加灵活地承接需求了。
这一点在 CodeGuide 的中间件实践中体现得淋漓尽致。仓库的 《SpringBoot 中间件设计和开发》小册 中,从第 3 章到第 18 章每一章都对应一个中间件的设计和实现:服务治理的统一白名单控制、超时熔断、调用限流、自定义拦截方法、ORM 框架、ES-JDBC 查询引擎、RPC 框架、数据库路由组件、Redis 简化使用封装、分布式任务调度、ASM/JVMTI 非入侵监控等。每个中间件都遵循"需求背景 → 方案设计 → 技术实现 → 测试验证"的完整链路——把共性的问题从业务代码中提炼出来、做成可复用的组件,这正是服务分层的终极形态:业务层只负责编排,通用能力沉淀在中间件里。
十、并发优化:去集中化与最终一致性
在分布式场景开发系统,要尽可能运用上分布式的能力,从程序设计上尽可能去避免一些集中的、分布式事务的、数据库加锁的,因为这些方式的使用都可能在某些极端情况下,造成系统负载的超标,从而引发事故。
所以通常情况下更需要做去集中化处理:
- 使用MQ消除峰、降低耦合,让数据可以最终一致性;
- 也要考虑在Redis下的使用,减少对数据库的大量锁处理。
合理的运用 MQ、RPC、分布式任务、Redis、分库分表以及分布式事务,只有这样的操作你才可能让自己的程序代码支撑起更大的业务体量。这也就是为什么本章第一节里的秒杀案例要把"独占锁"改成"分段锁 + 补偿 worker"——任何集中式资源(一把锁、一张表、一个数据库连接池)在高并发下都会成为事故放大器的震中。
十一、源码能力:把 HashMap 的散列设计用到数据库路由
你有了解过 HashMap 的拉链寻址数据结构吗?知道哈希散列和扰动函数吗?懂得怎么结合 Spring 动态切换数据源吗?AOP 是怎么实现以及使用的?MyBatis 是怎么和 Spring 结合交管 Bean 对象的?等等。看似都是些面试的八股文,但在实际的开发中其实是可以解决很多问题的。
以 HashMap 的扰动函数为例。HashMap 源码中通过(h = key.hashCode()) ^ (h >>> 16)把哈希值右移 16 位再与原值异或,混合了原哈希值中的高位和低位,增大了随机性,让数据元素更加均衡地散列、减少碰撞。CodeGuide 的 面经手册 · 第 3 篇《HashMap 核心知识,扰动函数、负载因子、扩容链表拆分》 用大量实验和 10 万单词测试数据验证了这套机制。
而这样"看似八股"的散列算法、寻址方式,完全可以运用到数据库路由的设计实现中。在 基于 Hash 散列,数据库路由组件设计(以及对应的 db-router 实践文档)中,作者实现了一个分库分表路由组件,核心逻辑如下:
@Around("aopPoint() && @annotation(dbRouter)") public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable { String dbKey = dbRouter.key(); if (StringUtils.isBlank(dbKey)) throw new RuntimeException("annotation DBRouter key is null!"); // 计算路由 String dbKeyAttr = getAttrValue(dbKey, jp.getArgs()); int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount(); // 扰动函数 int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16)); // 库表索引 int dbIdx = idx / dbRouterConfig.getTbCount() + 1; int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1); // 设置到 ThreadLocal DBContextHolder.setDBKey(String.format("%02d", dbIdx)); DBContextHolder.setTBKey(String.format("%02d", tbIdx)); logger.info("数据库路由 method:{} dbIdx:{} tbIdx:{}", getMethod(jp).getName(), dbIdx, tbIdx); // 返回结果 try { return jp.proceed(); } finally { DBContextHolder.clearDBKey(); DBContextHolder.clearTBKey(); } }拆解这段"从 HashMap 学来的源码级设计":
- 把库表乘积当数组长度:提取
dbCount * tbCount的总长度,把它当成 HashMap 的容量size使用。 - 复用扰动函数加强散列:
(size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16))与 HashMap 的下标计算完全一致,让数据尽可能均匀地散列到各个库表中,避免数据集中到某个库的某张表,从而失去分库分表的意义。 - 索引折算到库和表:当
idx计算完总长度上的一个索引位置后,需要把这个位置折算到库表中:dbIdx = idx / tbCount + 1,tbIdx = idx - tbCount * (dbIdx - 1),看看总体长度的索引落到哪个库哪张表。 - ThreadLocal 传递索引:把计算的索引信息存放到
DBContextHolder(ThreadLocal)中,用于在方法调用过程中提取索引信息,同时用try/finally保证调用结束后清除,避免线程池复用下的数据串扰。
配合@DBRouter(key = "userId")注解标记 Mapper 方法、AbstractRoutingDataSource动态数据源切换,以及 MyBatis 语句中的${tbIdx}表名占位符,一个完整的"散列路由 + 动态数据源"中间件就成型了。从仓库文档中的单元测试日志可以看到实际效果:数据库路由 method:queryUserInfoByUserId dbIdx:2 tbIdx:3——一条数据被稳定地路由到了 2 库 3 表。
这就是"源码能力"的价值:HashMap 的散列算法是八股文,但把散列算法迁移到数据库路由就是生产级能力。同样的,AOP 拦截、Spring 动态切换数据源、MyBatis 与 Spring 的 Bean 交管,这些源码知识都在这个中间件里得到了实战落地。
十二、总结
讲道理,你几乎不太可能把一堆已经烂得不行了的代码,通过重构的方式处理干净。细了说,你要改变代码结构分层、属性对象整合、调用逻辑封装,但任何一步的操作都可能会对原有的接口定义和调用造成风险影响,而且外部现有调用你的接口还需要随着你的改动而升级。可能你会想着再包装一层,但这一层包装仍需要较大的时间成本和几乎没有价值的适配。
所以在实际开发中,如果能让这些代码具有重构的可能,几乎就是要实时重构:每当你在添加新的功能、新的逻辑、修复异常时,就要考虑是否可以通过代码结构、实现方式、设计模式等手段的使用,改变不合理的功能实现。每一次、一点的优化和改变,也不会有那么难。
当你在接需求的时候,认真思考承接这样的业务诉求,都需要建设怎样的数据结构、算法逻辑、设计模式、领域聚合、服务编排、系统架构等,才能更合理地搭建出良好的具有易维护、可扩展的系统服务。关于这些内容:
- 设计模式实战:见 重学 Java 设计模式 系列,从工厂方法到模板方法共 22 个模式,全部是真实业务场景的落地案例;
- 手写框架源码:见 手撸 Spring 与 手写 MyBatis 系列,从零实现框架,理解 AOP、IOC、ORM 的底层原理;
- 中间件与组件开发:见 SpringBoot 中间件设计和开发 与 数据库路由组件设计,看通用能力如何从业务中被提炼、沉淀为可复用的工程资产。
好的代码是"改"出来的,而不是"推倒重来"出来的。从今天起,每一次提交前多问一句:这段代码三个月后、半年后,还能不能被人看懂、被需求扩展?如果答案是"不能",那它现在就已经在欠债了。
【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!项目地址: https://gitcode.com/gh_mirrors/code/CodeGuide
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考