4、SDK整体架构设计:分层设计原则(传输层、加密层、业务层),模块划分,接口抽象与实现分离

大家好,我是老张。今天咱们聊一个实在的话题——SDK的架构该怎么做分层。我做存储SDK这么多年,看过太多“大泥球”式的代码:加密逻辑混在网络请求里,业务回调又直接操作内存池,调试起来简直要命。所以分层不光是为了好看,更是为了让后续的人(包括六个月后的你自己)能改得动手。

说白了,SDK的本质就是一层壳,把底层的网络通信、加密计算、数据格式跟上层业务隔离开。那这一层壳该切几刀?我个人习惯切三刀:传输层、加密层、业务层。每层只干一件事,层与层之间用接口说话。

核心原则:每层只改变一件事。传输层只管怎么发数据,加密层只管怎么保护数据,业务层只管用户想要什么。
SDK三层架构与模块关系 SDK三层架构:接口抽象与模块划分 业务层 (Business Layer) 上传/下载/列举/删除接口 · 数据分片 · 进度回调 · 错误处理 加密层 (Crypto Layer) 密钥管理 · 加密/解密引擎 · 完整性校验 · 密钥轮换 传输层 (Transport Layer) HTTP/HTTPS连接池 · 重试/超时 · 流量控制 · 协议适配 业务调用 getObject() 数据加密 encrypt(plain) 网络请求 POST /bucket

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,就能无缝接入。

加密层的核心抽象:输入明文+元数据 → 输出密文+加密上下文(比如IV、tag、keyId)。 解密则是逆过程:密文+加密上下文 → 明文。

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,通过配置开关切换,万一出问题能秒级回退。
小技巧:把接口和基础数据结构(比如Request、Response、CryptoResult)放在一个「core」模块里,不依赖任何第三方库。所有实现模块只依赖core。这样依赖关系是单向的,不会循环。

我见过一个反面例子:某SDK把Transport接口定义在了实现模块里,导致业务层要想测试就得引一个臃肿的网络库。这就是抽象没做对。

4.4 模块划分实操

基于三层架构,我通常把SDK拆成以下几个模块(Java视角,其他语言类似):

  1. oss-sdk-core:接口定义 + 基础模型 + 异常类。纯POJO,零依赖。
  2. oss-sdk-transport-http:传输层的HTTP实现,依赖OkHttp或Netty。
  3. oss-sdk-crypto-aes:AES-GCM的加密层实现。
  4. oss-sdk-crypto-sm:国密SM4的实现。
  5. oss-sdk-api:业务层的公开API,用户直接引用这个包。
  6. oss-sdk-builder:提供Builder模式来组装各层实现,比如 OssClient.builder().transport(new OkHttpTransport()).crypto(new AesCrypto()).build()。
注意:不要把加密层和业务层混在一起。我见过有人把密钥ID放在业务层的配置里——结果换了密钥来源,业务代码也得改。正确的做法是:密钥配置只暴露给加密层。

4.5 避坑指南(来自血泪史)

坑一:层间数据拷贝过多
每层都复制一份字节数组,大文件上传时内存直接炸。我的建议是:传输层尽量支持 InputStream 或 ReadableByteChannel,减少中间缓冲。

坑二:抽象泄露
比如传输层接口的异常里抛出了网络库特有的 SocketTimeoutException,而不是自定义的 TransportTimeoutException。一旦换网络库,所有业务层的catch就失效了。一定要做异常转换。

坑三:加密层的密钥缓存与线程安全
密钥提供者如果是调用远程KMS,那么getKey()一定要加缓存,且考虑缓存失效和并发更新。我曾在生产环境遇到过几百个线程同时请求KMS,直接打爆了配额。正确的做法是用Guava的Cache或类似机制,设置合理的过期策略。

最后分享一个心得:设计接口时,先写测试代码。如果你发现写测试时需要mock很多内部细节,说明分层不够干净。好的分层应该让你在写测试时只需要关心当前层的逻辑。

好了,今天关于SDK分层设计的内容就到这。记住:传输层只管搬砖,加密层只管保护,业务层只管服务。接口抽象把路分开,实现分离让心不慌。