4、SDK整体架构设计:分层设计原则(传输层、加密层、业务层),模块划分,接口抽象与实现分离
大家好,我是老张。今天咱们聊一个实在的话题——SDK的架构该怎么做分层。我做存储SDK这么多年,看过太多“大泥球”式的代码:加密逻辑混在网络请求里,业务回调又直接操作内存池,调试起来简直要命。所以分层不光是为了好看,更是为了让后续的人(包括六个月后的你自己)能改得动手。
说白了,SDK的本质就是一层壳,把底层的网络通信、加密计算、数据格式跟上层业务隔离开。那这一层壳该切几刀?我个人习惯切三刀:传输层、加密层、业务层。每层只干一件事,层与层之间用接口说话。
4.1 为什么一定要分层?
我刚开始做SDK时也踩过坑——想着“反正就几个接口,写一起算了”。结果客户要求换加密算法,从AES换成SM4,我愣是改了三天,因为加密代码散落在每个上传/下载方法里。这就是没分层的代价。分层最大的好处是:隔离变化。传输协议从HTTP/1.1升级到HTTP/2,只动传输层;密钥从本地文件换成KMS服务,只动加密层。
另一个好处是可测试性。每层都能单独mock,比如测试业务逻辑时注入一个假的加密层,连真实密钥都不用。这在单元测试里特别省事。
4.2 三层架构详解
4.2.1 传输层
传输层是最底层,它只管一件事:把字节流发出去,再收回来。我见过有些SDK直接把加密后的数据塞进HTTP body,但传输层根本不知道body内容是明文还是密文——这才对。传输层需要处理:连接池复用、超时重试、请求/响应编解码、协议版本协商。举个例子,一个典型的传输层接口可能是这样的:
// 传输层接口 —— 抽象,不关心业务
public interface Transport {
Response send(Request request) throws TransportException;
// 请求包含:method, url, headers, body(byte[])
// 响应包含:statusCode, headers, body(byte[])
}
注意:传输层的输入输出都是「裸字节」。为什么?因为加密层做了一层包装,传输层只管透传。
4.2.2 加密层
加密层是SDK的灵魂。它负责:密钥获取/缓存、加密/解密算法、完整性校验(比如GCM的tag)、密钥轮换策略。我在项目中遇到过最头疼的事——密钥泄露后的热切换。如果加密层直接把密钥硬编码在代码里,出事你都没法快速换。所以我建议加密层设计成一个可插拔的密钥提供者:
// 密钥提供者接口 —— 抽象密钥来源
public interface KeyProvider {
SecretKey getKey(String keyId) throws KeyException;
void rotateKey(String keyId, SecretKey newKey);
}
加密层不关心密钥是来自本地文件、KMS还是母鸡下的蛋。只要实现了KeyProvider,就能无缝接入。
4.2.3 业务层
业务层是用户直接看到的。它处理:数据分片(大文件分段上传)、进度回调、错误重试策略、多版本兼容。业务层不讲加密细节,也不讲网络握手。用户调 client.putObject(bucket, key, data) 就完事了。业务层内部会调用加密层和传输层,就像这样:
// 业务层实现上传
public PutObjectResult putObject(String bucket, String key, InputStream data) {
// 1. 读取数据,可选分段
byte[] plainBytes = readAll(data);
// 2. 调用加密层
CryptoResult cryptoResult = cryptoLayer.encrypt(plainBytes);
// 3. 构造传输层请求
Request req = Request.builder()
.url(composeUrl(bucket, key))
.method("PUT")
.body(cryptoResult.getCiphertext())
.header("x-oss-encrypt-keyid", cryptoResult.getKeyId())
.build();
// 4. 发送
Response resp = transport.send(req);
// 5. 解析响应
return parsePutResult(resp);
}
你发现没?业务层根本没碰加密算法,也没管连接池。这就是分层的力量。
| 层次 | 主要职责 | 对外输出 | 常见变更原因 |
|---|---|---|---|
| 传输层 | 网络通信、连接管理、超时重试 | Response (byte[] body) | 协议升级、网络库替换 |
| 加密层 | 加解密、密钥管理、完整性校验 | CryptoResult (密文+上下文) | 加密算法换SM4、密钥从KMS获取 |
| 业务层 | API接口、分片、进度、错误处理 | Put/Get等结果对象 | 新功能(如断点续传)、异常处理策略 |
4.3 接口抽象与实现分离
有了分层,还得定好各层之间的“合同”——也就是接口。我一直坚持一个原则:接口定义在抽象层,实现放在具体子模块。比如上面说的Transport、KeyProvider、CryptoEngine,这些都是接口。而HttpTransport、LocalFileKeyProvider、AesGcmEngine是实现。
这样做的好处太明显了:
- 单元测试:你可以用MockTransport模拟各种网络异常,不必真的搭服务器。
- 多平台适配:Android端用OkHttp,iOS端用URLSession,只要都实现Transport接口即可。
- 灰度切换:新算法上线时,先实现一个CryptoEngine v2,通过配置开关切换,万一出问题能秒级回退。
我见过一个反面例子:某SDK把Transport接口定义在了实现模块里,导致业务层要想测试就得引一个臃肿的网络库。这就是抽象没做对。
4.4 模块划分实操
基于三层架构,我通常把SDK拆成以下几个模块(Java视角,其他语言类似):
- oss-sdk-core:接口定义 + 基础模型 + 异常类。纯POJO,零依赖。
- oss-sdk-transport-http:传输层的HTTP实现,依赖OkHttp或Netty。
- oss-sdk-crypto-aes:AES-GCM的加密层实现。
- oss-sdk-crypto-sm:国密SM4的实现。
- oss-sdk-api:业务层的公开API,用户直接引用这个包。
- oss-sdk-builder:提供Builder模式来组装各层实现,比如
OssClient.builder().transport(new OkHttpTransport()).crypto(new AesCrypto()).build()。
4.5 避坑指南(来自血泪史)
坑一:层间数据拷贝过多
每层都复制一份字节数组,大文件上传时内存直接炸。我的建议是:传输层尽量支持 InputStream 或 ReadableByteChannel,减少中间缓冲。
坑二:抽象泄露
比如传输层接口的异常里抛出了网络库特有的 SocketTimeoutException,而不是自定义的 TransportTimeoutException。一旦换网络库,所有业务层的catch就失效了。一定要做异常转换。
坑三:加密层的密钥缓存与线程安全
密钥提供者如果是调用远程KMS,那么getKey()一定要加缓存,且考虑缓存失效和并发更新。我曾在生产环境遇到过几百个线程同时请求KMS,直接打爆了配额。正确的做法是用Guava的Cache或类似机制,设置合理的过期策略。
好了,今天关于SDK分层设计的内容就到这。记住:传输层只管搬砖,加密层只管保护,业务层只管服务。接口抽象把路分开,实现分离让心不慌。