2. 整体架构设计:分层架构、模块划分、核心组件

聊到SDK的整体架构,我第一个想说的是:别急着写代码。先想清楚怎么分层。我在项目里见过太多人,上来就开始封装API,结果做到一半发现加密逻辑散落各处,配置管理乱七八糟,最后不得不推倒重来。说白了,架构设计就是你SDK的骨架,骨架歪了,后面再怎么填肉都别扭。

2.1 为什么需要分层架构?

你想想看,KMS SDK要干的事儿其实挺杂的。既要处理网络请求,又要管密钥的加解密,还得操心用户配置怎么存、异常怎么抛。如果把这些全揉在一个大模块里,维护起来就是个噩梦。

分层架构的核心目标就三个:

  • 职责分离 —— 每一层只管自己的事,不越界
  • 可替换性 —— 换掉底层通信库,上层不用动一行代码
  • 可测试性 —— 每一层都能单独写单元测试,不用启动整个SDK
我个人习惯是:先画出数据流的方向,再沿着数据流切层。KMS SDK的数据流其实很清晰——从用户代码到网络请求,再从网络响应回到用户代码。顺着这个流向,层就自然出来了。

我记得第一次设计SDK时,把签名逻辑放在网络层里,结果后来要支持不同的签名算法,改得我头皮发麻。从那以后,我坚持每层只负责一件事。

2.2 三层架构:协议层 / 核心层 / 扩展层

我最终把架构切成了三层。你可能会问为什么不是四层、五层?其实层数多了反而增加调用链的复杂度,三层刚好。来看具体每一层做什么。

层级 核心职责 你需要注意什么
协议层 处理HTTP通信、序列化/反序列化、重试与超时 协议层不要夹带业务逻辑,它只管收发
核心层 封装KMS API语义、密钥管理、数据加解密、缓存策略 这一层是SDK的心脏,要保证线程安全
扩展层 提供高级功能:自动轮换密钥、监控告警、日志集成 扩展层可以按需加载,不要强耦合

有件事我必须强调:依赖方向必须单向。扩展层依赖核心层,核心层依赖协议层,反过来不行。我曾经在某个项目中违反了这条规则,核心层直接引用了扩展层的日志组件,结果扩展层一升级,核心层跟着崩,教训深刻。

2.3 模块划分:核心模块与扩展模块

分层之后,我们再按功能把每个层拆成更小的模块。模块划分的原则是:高内聚、低耦合、按职责拆分。

核心模块(必选)

  1. 认证模块 —— 处理AK/SK签名、管理、凭证刷新。我的建议是把这个模块做成无状态的,方便多线程共享。
  2. 加密模块 —— 封装对称加密(AES)、非对称加密(RSA/SM2)、密钥派生。这块最容易踩坑,尤其是padding模式的选择,我后面会细说。
  3. 配置持久化模块 —— 管理SDK运行时的配置项,包括endpoint、超时时间、缓存开关等。支持从环境变量、配置文件、代码设定三种方式加载。
  4. 异常处理模块 —— 定义统一的异常体系,区分服务端异常、客户端异常、网络异常。不要把原始异常直接抛给用户。

扩展模块(可选)

  1. 监控与遥测模块 —— 采集API调用次数、延迟分布、错误率,方便接入Prometheus等监控系统。
  2. 插件系统模块 —— 允许用户插入自定义逻辑,比如在发送请求前做额外的参数校验。这块我做得比较轻量,只暴露了几个hook接口。
  3. 自动轮换模块 —— 定期从KMS拉取新的密钥版本,并更新本地缓存。这个功能默认关闭,需要用户显式开启。

核心模块和扩展模块的界限在哪里?

我的判断标准很简单:如果去掉这个模块,SDK还能正常加解密、调API吗?能,就是扩展模块;不能,就是核心模块。扩展模块可以单独打成JAR包,用户按需引用。

2.4 核心组件:Client、Config、Request Pipeline

到现在,我们有了层,有了模块。但真正把这些东西串起来跑通的,是几个核心组件。可以说,理解了这三个组件,你就理解了SDK的运行机制。

2.4.1 Client —— 用户的入口

Client是用户唯一直接打交道的对象。它负责组装配置、初始化各个模块、暴露业务方法。我的设计原则是:Client必须线程安全。用户应该能创建一次Client,在整个应用生命周期内复用。

// 用户视角的Client
KmsClient client = KmsClient.builder()
    .withEndpoint("kms.aliyuncs.com")
    .withCredentials(new DefaultCredentialProvider())
    .withCacheStrategy(new LocalCacheStrategy())
    .build();

// 加密,一行调用
byte[] cipherText = client.encrypt(keyId, plainText);

你可能会问:为什么不把Builder链暴露到内部?我试过,结果导致内部模块之间相互依赖Builder的细节,耦合很重。所以现在Builder只做配置的收集,真正的组装逻辑放在ClientFactory里。

2.4.2 Config —— 配置的集中管理

Config组件不只是存几个key-value。它要解决配置优先级的问题。我个人习惯的优先级是:代码设定 > 环境变量 > 配置文件 > 默认值。注意,Config组件要支持运行时动态刷新,不能只读一次就锁死。

避坑提醒:我曾经把Config设计成全局单例,结果在单元测试里吃了大亏。不同的测试用例需要不同的配置,全局单例改一次全变了。后来我改成每个Client持有自己的Config实例,问题才解决。

2.4.3 Request Pipeline —— 请求的执行链

这是最让我得意的部分。每个API调用会经过一条Pipeline:

  1. 参数校验与序列化
  2. 签名计算
  3. 发送HTTP请求(含重试与超时)
  4. 反序列化响应
  5. 错误码映射与异常转换

Pipeline的设计借鉴了中间件模式。每个阶段都是一个独立的Handler,可以编排顺序、动态增删。这样扩展模块想插入自定义逻辑时,只需要往Pipeline里加一个Handler即可,不需要改动其他代码。

2.5 架构总览:从用户请求到底层通信

我把整个架构画成了一张图。你可以对照着看:用户通过Client发起请求,Client拿到Config后构建Request,然后Request在Pipeline里依次经过各层处理,最后通过协议层发出去。整个过程就像一条流水线。

KMS SDK 整体架构总览 协议层 核心层 扩展层 HTTP 通信 序列化 / 反序列化 重试与超时 认证模块 加密模块 配置持久化 异常处理 监控与遥测 插件系统 自动轮换 核心组件 Client · Config · Request Pipeline 用户请求 → Client → Pipeline → 协议层 → 网络 依赖方向:扩展层 → 核心层 → 协议层(单向) 数据流向:用户 → Client → Pipeline → 协议层 → 远端

这张图把分层、模块、组件之间的关系串起来了。我建议你在读后续的章节时,时不时回到这张图上,看看某个知识点落在哪个层、哪个模块里。嗯,这样不容易迷路。

2.6 我的一些设计取舍

最后聊几个你可能会纠结的点,也算是我踩过的坑:

  • 同步 vs 异步 —— 我选择同步接口作为默认,异步用Future/Promise包装。原因很简单:90%的用户是同步调用场景,异步接口对大多数人来说是额外的心智负担。
  • 静态工厂 vs Builder —— Builder看起来啰嗦,但可读性和可扩展性远优于静态工厂。特别是当配置项超过5个以后,Builder的参数名比位置参数好理解太多了。
  • 要不要用IoC容器 —— 不要。SDK不是应用框架,引入Spring/Gradle的依赖管理反而会让用户的项目出现冲突。自己new对象虽然土,但可控。

再说安全。SDK整体架构里,密钥明文绝不允许在任何组件间传递。曾经有一个版本我把密钥放在Config对象里传递,结果用户打印日志时误把Config序列化输出到控制台。从那之后,密钥全部用引用的方式传递,任何时候都不暴露toString()。

这一章的内容基本就是这些。架构定下来之后,SDK的骨架就稳了。下一件事是逐个模块的细节实现,从协议层开始,我会把HTTP通信、重试策略、序列化工具这些底层细节拆开讲清楚。