2. 整体架构设计:分层架构、模块划分、核心组件
聊到SDK的整体架构,我第一个想说的是:别急着写代码。先想清楚怎么分层。我在项目里见过太多人,上来就开始封装API,结果做到一半发现加密逻辑散落各处,配置管理乱七八糟,最后不得不推倒重来。说白了,架构设计就是你SDK的骨架,骨架歪了,后面再怎么填肉都别扭。
2.1 为什么需要分层架构?
你想想看,KMS SDK要干的事儿其实挺杂的。既要处理网络请求,又要管密钥的加解密,还得操心用户配置怎么存、异常怎么抛。如果把这些全揉在一个大模块里,维护起来就是个噩梦。
分层架构的核心目标就三个:
- 职责分离 —— 每一层只管自己的事,不越界
- 可替换性 —— 换掉底层通信库,上层不用动一行代码
- 可测试性 —— 每一层都能单独写单元测试,不用启动整个SDK
我记得第一次设计SDK时,把签名逻辑放在网络层里,结果后来要支持不同的签名算法,改得我头皮发麻。从那以后,我坚持每层只负责一件事。
2.2 三层架构:协议层 / 核心层 / 扩展层
我最终把架构切成了三层。你可能会问为什么不是四层、五层?其实层数多了反而增加调用链的复杂度,三层刚好。来看具体每一层做什么。
| 层级 | 核心职责 | 你需要注意什么 |
|---|---|---|
| 协议层 | 处理HTTP通信、序列化/反序列化、重试与超时 | 协议层不要夹带业务逻辑,它只管收发 |
| 核心层 | 封装KMS API语义、密钥管理、数据加解密、缓存策略 | 这一层是SDK的心脏,要保证线程安全 |
| 扩展层 | 提供高级功能:自动轮换密钥、监控告警、日志集成 | 扩展层可以按需加载,不要强耦合 |
有件事我必须强调:依赖方向必须单向。扩展层依赖核心层,核心层依赖协议层,反过来不行。我曾经在某个项目中违反了这条规则,核心层直接引用了扩展层的日志组件,结果扩展层一升级,核心层跟着崩,教训深刻。
2.3 模块划分:核心模块与扩展模块
分层之后,我们再按功能把每个层拆成更小的模块。模块划分的原则是:高内聚、低耦合、按职责拆分。
核心模块(必选)
- 认证模块 —— 处理AK/SK签名、管理、凭证刷新。我的建议是把这个模块做成无状态的,方便多线程共享。
- 加密模块 —— 封装对称加密(AES)、非对称加密(RSA/SM2)、密钥派生。这块最容易踩坑,尤其是padding模式的选择,我后面会细说。
- 配置持久化模块 —— 管理SDK运行时的配置项,包括endpoint、超时时间、缓存开关等。支持从环境变量、配置文件、代码设定三种方式加载。
- 异常处理模块 —— 定义统一的异常体系,区分服务端异常、客户端异常、网络异常。不要把原始异常直接抛给用户。
扩展模块(可选)
- 监控与遥测模块 —— 采集API调用次数、延迟分布、错误率,方便接入Prometheus等监控系统。
- 插件系统模块 —— 允许用户插入自定义逻辑,比如在发送请求前做额外的参数校验。这块我做得比较轻量,只暴露了几个hook接口。
- 自动轮换模块 —— 定期从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:
- 参数校验与序列化
- 签名计算
- 发送HTTP请求(含重试与超时)
- 反序列化响应
- 错误码映射与异常转换
Pipeline的设计借鉴了中间件模式。每个阶段都是一个独立的Handler,可以编排顺序、动态增删。这样扩展模块想插入自定义逻辑时,只需要往Pipeline里加一个Handler即可,不需要改动其他代码。
2.5 架构总览:从用户请求到底层通信
我把整个架构画成了一张图。你可以对照着看:用户通过Client发起请求,Client拿到Config后构建Request,然后Request在Pipeline里依次经过各层处理,最后通过协议层发出去。整个过程就像一条流水线。
这张图把分层、模块、组件之间的关系串起来了。我建议你在读后续的章节时,时不时回到这张图上,看看某个知识点落在哪个层、哪个模块里。嗯,这样不容易迷路。
2.6 我的一些设计取舍
最后聊几个你可能会纠结的点,也算是我踩过的坑:
- 同步 vs 异步 —— 我选择同步接口作为默认,异步用Future/Promise包装。原因很简单:90%的用户是同步调用场景,异步接口对大多数人来说是额外的心智负担。
- 静态工厂 vs Builder —— Builder看起来啰嗦,但可读性和可扩展性远优于静态工厂。特别是当配置项超过5个以后,Builder的参数名比位置参数好理解太多了。
- 要不要用IoC容器 —— 不要。SDK不是应用框架,引入Spring/Gradle的依赖管理反而会让用户的项目出现冲突。自己new对象虽然土,但可控。
再说安全。SDK整体架构里,密钥明文绝不允许在任何组件间传递。曾经有一个版本我把密钥放在Config对象里传递,结果用户打印日志时误把Config序列化输出到控制台。从那之后,密钥全部用引用的方式传递,任何时候都不暴露toString()。
这一章的内容基本就是这些。架构定下来之后,SDK的骨架就稳了。下一件事是逐个模块的细节实现,从协议层开始,我会把HTTP通信、重试策略、序列化工具这些底层细节拆开讲清楚。