基础设施总览
能力全景
七剑下天山——能力矩阵
天擎七项基础设施,如七把名剑——刀斩肉身,心斩灵魂。每把剑各斩一痛,各开一疆。合在一起,便是完整的云原生基础设施基座。
剑阵
七把剑不是各自孤立的——它们有同炉、有同师。剑与剑之间的连线,就是模块与模块之间的连线。
泰阿剑 · 配置 横扫六合,百川归服
│ bind
▼
赤霄剑 · 选项 斩蛇立规,万象归型
│ IOptions
▼
轩辕剑 · 依赖注入 日月星辰,尽在其中
│
┌────────┬───────┼───────┐
▼ ▼ ▼ ▼
龙泉剑 湛卢剑 鱼肠剑
·HttpClient ·日志 ·缓存
一剑独行 过而不留 藏于无形
万里如风 纤毫毕现 应于瞬息
│
│ JsonContent
▼
干将剑
·序列化
干将铸剑
剑自铸己连线:
配置 ──bind──▶ 选项 ──IOptions<T>──▶ DI ──分发──▶ HTTP / 日志 / 缓存
七剑出鞘
| 剑 | 模块 | 判词 | 斩 · 所解之痛 | 能 · 所赋之力 | 点睛 |
|---|---|---|---|---|---|
| 泰阿 | 配置系统 | 横扫六合,百川归服 | 配置来源格式各异,JSON/XML/YAML/环境变量/命令行读写方式各不相同 | 多源叠加→后发覆盖;section:key 统一读写;三行代码切换环境 | 一统天下的不是剑,是度量衡 |
| 赤霄 | 选项模式 | 斩蛇立规,万象归型 | config["key"] 拼写错误编译通过、运行时静默炸 | DI↔配置的类型安全桥梁;validateOnStart 启动即校验;命名选项多租户共存 | 立规矩比斩蛇难,但管用四百年 |
| 轩辕 | 依赖注入 | 日月星辰,尽在其中 | 各模块各自启动,日志/缓存/HTTP/序列化各有各的初始化方式,无统一入口 | 构造函数自动注入;Singleton/Scoped/Transient 三级生命周期;validateOnBuild 拦截缺失依赖 | 众神铸的剑,黄帝一个人用 |
| 龙泉 | HttpClient | 一剑独行,万里如风 | 写 HTTP 业务很自由,但日志/重试/熔断/认证这些横切关注点无处切入,只能硬塞在 send() 前后 | IHttpClientFactory 管生命周期;DelegatingHandler 洋葱管道;baseAddress/timeout 统一收敛 | 你以为单挑,其实群殴 |
| 湛卢 | 日志系统 | 过而不留,纤毫毕现 | 日志逻辑与业务代码缠死,换输出目标改几十处;无法统一移除或热切换 | 分类名+级别组合过滤;addConfiguration() 改 JSON 控日志不重编译;一次记录多 Provider 分发 | 一把可以被遗忘的剑,才是好剑 |
| 鱼肠 | 分布式缓存 | 藏于无形,应于瞬息 | 存储介质太多,Redis / MongoDB / Memcached 每种 API 不同,换一次改一次代码 | IDistributedCache 四个方法统一接口;相对/绝对/滑动三策略可组合;后台扫描+被动清理 | 藏得最深的剑,出鞘最快 |
| 干将 | 序列化 | 干将铸剑,剑自铸己 | 每个类都要手写序列化逻辑,字段变动时同步维护繁琐易错 | @Serialization 编译期宏自动生成,字段变动只改类定义;DataModel 中间表示零反射映射 | 最好的铸剑师,手上没有茧 |
合剑生威——组合效应
单剑斩一痛,合剑开天地——每多一把剑,不是加法,是乘法。
| 组合 | 剑谱 | 释出之力 | 一叶知秋 |
|---|---|---|---|
| 三剑归元 | 赤霄 · 轩辕 · 龙泉 | IOptions<T> 定义配置契约 → IHttpClientFactory 管理生命周期 → IServiceCollection 收敛为一行 addXxx()。云服务 SDK 标准化生产线 | services.addObsClient { options => config.getSection("Obs").bind(options) } |
| 三才联动 | 泰阿 · 赤霄 · 轩辕 | 配置中心实时推送 → IOptionsMonitor<T>.onChange() 感知变化 → DI 容器刷新。不重启切换数据库、换密钥、调日志级别——热更新闭环 | Nacos 推送新库地址 → IOptionsMonitor<DbOptions> 自动更新连接字符串 |
| 一气贯之 | 龙泉 · 湛卢 · 干将 | DelegatingHandler 记录请求/响应 → ILoggerProvider 写入 ES/Kafka → JsonSerializer 反序列化强类型。全链路可观测 + 类型安全 API 调用 | client.get<User>("/api/user/1") → 日志落 ES → 链路可检索 → 返回强类型 User |
| 四方守一 | 轩辕 · 鱼肠 · 泰阿 · 赤霄 | 配置定义缓存策略 → 选项类型化 → DI 注入 IDistributedCache。开发用 Memory,生产切 Redis,改 JSON 切换——代码不动 | appsettings.json 改 "Cache:Provider": "Redis" → 全系统切换 Redis 集群 |
| 五行贯通 | 龙泉 · 干将 · 鱼肠 · 湛卢 · 赤霄 | 调用 API → 反序列化 → 缓存结果 → 日志记录 → 配置控超时/重试/降级。完整 API 网关数据通路 | AI 网关:收 prompt → 路由 → 缓存 → 日志记调用链 → 配置控 fallback |
| 七剑归位 | 全部 | 泰阿收纳 → 赤霄化形 → 轩辕分发 → 龙泉飞赴、湛卢透视、鱼肠在侧、干将化形。一套完整的"配置→组装→运行"流水线 | 微服务从拉起到对外服务:配置加载 → 选项校验 → DI 组装 → 日志就绪 → HTTP 监听 → 缓存预热 → 序列化就位 |
逐个击破:七大痛点的解决之道
下面不是罗列 API,而是展示每一项基础设施解决了什么实际问题、如何与其它设施组合、以及社区能在此基础上构建什么。
痛点一 · 轩辕剑:各模块各自初始化,无统一组装入口
众神铸的剑,黄帝一个人用 - 轩辕剑
加日志要 LoggerFactory.create { },用缓存要 new MemoryDistributedCache(),发 HTTP 要 new HttpClient()——七项设施七种启动方式,各自为政。哪个模块先启动、哪个后启动、生命周期谁来管?全没谱。
天擎的答案:日志、缓存、HTTP、数据库——七项设施七种启动方式,各自为政。混乱源于没有容器统管创建和生命周期。依赖注入作为统一入口——所有设施收敛到 services.addXxx() 一行注册,创建顺序、生命周期、依赖关系全由容器接管。各团队铸各自的剑,你一个人拔。
实践案例:众神铸剑,黄帝拔剑
一个订单服务,依赖日志、缓存、HTTP 客户端、邮件服务、数据库——外加自己的 6 个内部组件。没有 DI 时,散落一地:
let loggerFactory = LoggerFactory.create { it.addConsole() }
let cache = MemoryDistributedCache()
let client = HttpClient()
let email = SmtpEmailService()
let db = AppDbContext()
// 11 个组件各自 new,启动顺序靠运气,生命周期没人管
let service = OrderService(loggerFactory.createLogger("app"), cache, client, email, db)第一步:每个模块把自己的注册封进 extend ServiceCollection。
日志团队在自己的包里:
extend ServiceCollection {
public func addLogging(configureAction: (ILoggingBuilder) -> Unit): ServiceCollection {
let factory = LoggerFactory.create(configureAction)
this.addSingleton<ILoggerFactory>(factory)
this.addSingleton<ILogger> { sp => sp.getOrThrow<ILoggerFactory>().createLogger("app") }
return this
}
}缓存团队在自己的包里:
extend ServiceCollection {
public func addDistributedMemoryCache(): ServiceCollection {
this.addSingleton<IDistributedCache>(MemoryDistributedCache(Options.create(DistributedCacheOptions())))
return this
}
}订单团队在自己的包里——把所有订单相关组件封成一把剑:
extend ServiceCollection {
public func addOrderModule(configureAction: ?(OrderModuleOptions) -> Unit = None): ServiceCollection {
match (configureAction) {
case Some(let action): this.configure(action)
case None: {}
}
this.addScoped<IOrderRepository, SqlOrderRepository>()
this.addScoped<IInventoryService, InventoryService>()
this.addSingleton<IDiscountCalculator, DiscountCalculator>()
this.addSingleton<IEmailService, SmtpEmailService>()
this.addSingleton<IInvoiceService, InvoiceService>()
this.addScoped<OrderService, OrderService>()
return this
}
}三个团队互不知晓,各自铸剑,各自 return this——链不断。
第二步:黄帝拔剑。 启动代码只剩三条链:
let provider = ServiceCollection()
.addLogging { it.addConsole() } // 日志团队铸的
.addDistributedMemoryCache() // 缓存团队铸的
.addOrderModule { options => // 订单团队铸的——里面 6 个注册全搞定
options.connectionString = config["db:order:connection"]
}
.build() // 就这一下——所有模块就绪11 个组件的初始化收敛为 3 行 .addXxx()。哪个模块先注册不重要——容器在 build() 阶段自动编排依赖顺序。新团队加入?加一个 extend ServiceCollection,链一行 .addXxx(),构造不动。
社区可构建的方向:
| 扩展类型 | 案例 |
|---|---|
| 邮件服务 | SmtpSender、SendGridSender、MailgunSender,统一 IEmailSender 接口 |
| 短信服务 | 阿里云短信、腾讯云短信、Twilio,统一 ISmsSender 接口 |
| 支付服务 | 微信支付、支付宝、Stripe,统一 IPaymentService 接口 |
| 存储服务 | OBS、OSS、MinIO、S3,统一 IStorageService 接口 |
| 消息队列 | Kafka、RabbitMQ、RocketMQ,统一 IMessageBus 接口 |
只要面向接口编程 + DI 注入,切换实现只需改一行注册代码。.NET 生态中的 MailKit、Pomelo、Dapper 正是基于这个模式生长起来的。
痛点二 · 泰阿剑:配置来源格式各异,每种都要写一套读取逻辑
一统天下的不是剑,是度量衡 - 泰阿剑
JSON 文件用 JsonValue.fromStr(),环境变量用 getVariable(),命令行参数用 getCommandLine() 数组遍历,YAML 还要引入第三方解析库——六种来源六套读法。叠加顺序变了、优先级要调?所有读取逻辑重写。
天擎的答案:六种来源,六种读法,缺一把统一的尺。配置系统就是这把尺——JSON、环境变量、命令行参数、内存默认值……addJsonFile() / addEnvVars() / addCmdArgs() 链式添加,最终都是 config["section:key"] 一种读法。后添加的自动覆盖先添加的,优先级不用手写代码判断。
appsettings.json MYAPP_Database__Host=prod-db --db:host=overridden
│ │ │
▼ ▼ ▼
① 默认值 ──────▶ ② 环境变量覆盖 ──────▶ ③ 命令行最终覆盖
│
config["database:host"]
返回最后注册的值实践案例:横扫六合——四种来源,一种读法
没有配置系统时,每种来源各写一套读取逻辑,优先级靠 if-else:
import std.fs.{File, OpenMode}
import std.io.StringReader
import stdx.encoding.json.*
import std.env.*
// 六种来源,六种读法——混乱
let content = StringReader(File("appsettings.json", OpenMode.Read)).readToEnd()
let jsonConfig = JsonValue.fromStr(content)
let dbHost = jsonConfig.asObject()["database"].asObject()["host"].asString().getValue() // JSON 读法
let envHost = getVariable("MYAPP_Database__Host") // 环境变量读法,返回 ?String
let args = getCommandLine()
let cliHost = args.find({ it.startsWith("--db:") }) // 命令行读法
// 谁覆盖谁?自己写 if-else 判断优先级有了配置系统——所有来源链式添加,最终都是同一种读法:
let config = ConfigurationManager()
.addJsonFile("appsettings.json") // ① JSON 文件
.addJsonFile("appsettings.Production.json") // ② 环境专属 JSON(不存在则跳过)
.addEnvVars("MYAPP_") // ③ 环境变量
.addCmdArgs(args) // ④ 命令行参数
.build()
// 不管值最初从哪来的——JSON、环境变量还是命令行——读法只有一种
let host = config["database:host"]
let port = config["database:port"]
let key = config["llm:apiKey"]JSON 文件、环境变量、命令行——不管当初是什么格式进来的,最终都是 config["section:key"] 一种读法。后注册的自动覆盖先注册的,优先级在链上而不是藏在 if-else 里。一统天下的,从来不是剑——是所有人都用同一把尺。
K8s ConfigMap 注入环境变量 → 自动覆盖 JSON 默认值,不需要改代码、不需要重编译。新增配置源只需再链一个 .addXxx()——Nacos、Apollo、Consul、Vault——全部走同一个 ConfigurationProvider 接口。
社区可构建的方向:
| 配置源 | 场景 |
|---|---|
| Nacos / Apollo / Consul | 配置中心,实时推送,无需重启 |
| AWS Secrets Manager / Azure Key Vault | 密钥管理,加密存储,自动轮换 |
| Kubernetes ConfigMap | 容器化部署,环境变量注入 |
| etcd / ZooKeeper | 分布式配置,服务发现联动 |
| 数据库配置表 | 动态配置,管理后台修改即时生效 |
只需继承 ConfigurationProvider、重写 load(),就能把任何数据源变成天擎的配置源。用户可以像用内置的 addJsonFile() 一样用你的扩展。
痛点三 · 赤霄剑:config["key"] 拼写错误编译通过,运行时才炸
立规矩比斩蛇难,但管用四百年 - 赤霄剑
当系统有三四十个配置项时,"这个服务用了哪些配置?"、"llm:apiKey 拼成 llm:apikey 会怎样?"——答案分别是:看构造函数完全看不出来,运行时返回 None 静默失败。
天擎的答案:config["key"] 满天飞,读法不统一——同一个 apiKey,有人写 llm:apiKey,有人写 llm:apikey,拼错编译通过、运行时才炸。混乱源于没规矩。选项模式立下规矩——一个强类型类统一定义所有字段,点出来用,写错编译不过,validateOnStart 启动即校验。斩蛇只需一剑,立规矩要多写一个类,但这个类管你整个项目生命周期。
没有选项模式时,配置读写靠字符串 key:
// ❌ 字符串 key:拼写错误编译通过,运行时静默炸
class ChatService {
public init(config: IConfiguration) {
let key = config["llm:apiKey"] // 写成 "llm:apikey" 照样编译通过
let url = config["llm:baseUrl"] // 构造函数签名看不出依赖了哪些 key
}
}选项模式立下规矩——类定义 → 从配置绑定 → 注入使用,一条完整的类型安全链路:
// 1. 立规矩:定义强类型配置类
class LlmOptions {
public var apiKey: String = ""
public var model: String = "gpt-4o"
public var baseUrl: String = ""
}
// 2. 绑规矩:从 IConfiguration 绑定到强类型
services.addOptions<LlmOptions>()
.bind(config.getSection("Llm")) // JSON 里的 apiKey/model/baseUrl → LlmOptions 字段
.validate { options =>
!options.apiKey.isEmpty() && !options.baseUrl.isEmpty()
}
.validateOnStart() // 启动即校验,密钥没配直接报错
// 3. 用规矩:注入 IOptions<T>,点出来用,写错编译不过
class ChatService {
public init(options: IOptions<LlmOptions>) {
let key = options.value.apiKey // 点出来,不是字符串
let url = options.value.baseUrl // 字段名写错 → 编译器直接报错
}
}对应 appsettings.json:
{
"Llm": {
"apiKey": "sk-xxx",
"model": "gpt-4o",
"baseUrl": "https://api.openai.com"
}
}三步走完规矩落成——JSON 里的值经 bind 写入强类型字段,经 validateOnStart 启动校验,经 IOptions<T> 注入到业务代码。从此 apiKey 只有一个写法,拼错编译不过,没配启动就报。
实践案例:多模型网关
一个 AI 网关同时接入 OpenAI、Claude。两边配置结构完全一样(apiKey + model + baseUrl),值不同。命名选项 + IOptionsMonitor<T>,同一套规矩管多套配置:
// 同一类,两个名字,两套配置——规矩只立一次
services.addOptions<LlmOptions>("openai")
.bind(config.getSection("Llm:OpenAI"))
.validate { options => !options.apiKey.isEmpty() && !options.baseUrl.isEmpty() }
.validateOnStart()
services.addOptions<LlmOptions>("claude")
.bind(config.getSection("Llm:Claude"))
.validate { options => !options.apiKey.isEmpty() }
.validateOnStart()
// 使用时按名获取——同一种读法
let monitor = provider.getOrThrow<IOptionsMonitor<LlmOptions>>()
let openai = monitor.get("openai") // 点 openai.apiKey,不是字符串
let claude = monitor.get("claude") // 点 claude.model,写错编译不过
对应 `appsettings.json`:
```json
{
"Llm": {
"OpenAI": { "apiKey": "sk-xxx", "model": "gpt-4o", "baseUrl": "https://api.openai.com" },
"Claude": { "apiKey": "sk-ant-xxx", "model": "claude-sonnet-4-6", "baseUrl": "https://api.anthropic.com" }
}
}关键细节:validateOnStart() 在应用启动时立即校验 apiKey 不为空——应用还没对外服务就先发现问题,fail-fast 比用户点按钮时才发现 API 密钥没配好要安全得多。
社区可构建的方向:
| 扩展类型 | 案例 |
|---|---|
| 多租户配置 | 每个租户一套命名选项,按租户 ID 动态获取 |
| 链路配置校验 | validate + validateOnStart 多选项交叉校验(如"A 开启时必须配 B") |
| 热更新感知 | IOptionsMonitor<T> + 配置中心推送,不停机切换配置 |
痛点四 · 湛卢剑:日志与业务缠死,换输出目标改几十处
一把可以被遗忘的剑,才是好剑 - 湛卢剑
println 散落在控制器、服务类、数据访问层——日志和业务代码长在一起。想从控制台切到文件?几十个文件逐个改。想把数据库模块的 Debug 日志关掉?只能全局关,牵连一片。想在生产环境临时拔掉日志排查性能?根本没开关。
天擎的答案:println 散落各处,日志和业务缠死——想切输出目标得改几十处,想关某个模块的 Debug 只能全局关。混乱源于日志没有从业务中分离出来。日志系统把记录和输出拆开——ILogger 从 DI 注入,穿梭每个模块记录完就走。输出目标从控制台切到 ES/Kafka/飞书,业务代码毫无感知。配完就可以忘掉——输出到了几个目标、过滤了什么级别,跟你没关系了。分类+级别组合过滤则让纤毫毕现:
logger.info("请求完成") ← 级别 Info,分类名 "app.http"
logger.debug("执行 SQL") ← 级别 Debug,分类名 "app.database"
过滤规则:
setMinimumLevel(Info) ← 全局:只看 Info 及以上
addFilter("app.database", Debug) ← 数据库模块:Debug 也看
addFilter("app.*", Warn) ← app 下其他模块:只看 Warn 及以上
结果:
"app.http" Info → ✅ 输出(Info >= Info)
"app.database" Debug → ✅ 输出(数据库特例,Debug >= Debug)
"app.http" Debug → ❌ 不输出(app.* 规则,Debug < Warn)实践案例:换目标不动代码
一个生产级应用通常需要同时输出到多个目标:控制台给本地开发、文件给运维审计、Elasticsearch 给集中分析。但在天擎里,加一个 provider 只是链上多一行 builder.addXxx()——日志穿梭其中,业务代码毫无感知:
let factory = LoggerFactory.create { builder =>
builder.addSimpleConsole { options =>
options.timestampFormat = "yyyy-MM-dd HH:mm:ss"
}
builder.addFile() // 自定义 provider
builder.setMinimumLevel(LogLevel.Info)
builder.addFilter("app.database", LogLevel.Debug)
}ILogger.info("xxx") 调一次,控制台、文件两个 provider 各自输出——业务代码完全不知道有几个输出目标。
社区可构建的方向:
| 输出目标 | 场景 |
|---|---|
| 文件(按天切割) | 审计日志、操作记录、合规要求 |
| Elasticsearch / Loki | 集中式日志分析、全文搜索、链路关联 |
| Kafka | 日志流式处理,实时告警 |
| 飞书 / 钉钉 / 企业微信 | 关键错误即时通知 |
| 数据库(MySQL / MongoDB) | 结构化存储,管理后台查询 |
| Seq / Splunk | 结构化日志分析平台 |
| OpenTelemetry | 链路追踪 + 日志 + 指标的统一出口 |
实现一个 provider 只需两个类:MyProvider <: ILoggerProvider 负责创建,MyLogger <: ILogger 负责写入。打包成一个包,用户用 builder.addXxx() 一行注册——控制台 provider 已经在用的模式。
痛点五 · 龙泉剑:写 HTTP 业务很自由,但横切关注点无处切入
你以为单挑,其实群殴 - 龙泉剑
client.get(url) 三行发一个请求,业务逻辑本身很干净。但现实是:每个请求要打日志、要带 Token、要失败重试、要熔断保护——这些跟业务无关的系统级逻辑无处安放,只能在 send() 前后硬塞,业务代码越裹越厚。
天擎的答案:每个请求都要打日志、带 Token、失败重试、熔断保护——这些跟业务无关的系统逻辑无处安放,只能在 send() 前后硬塞。混乱源于横切关注点和业务代码缠在一起。DelegatingHandler 管道把它们拆成独立 Handler——你只负责喊冲,背后一拥而上。client.get(url) 一行代码,日志、认证、重试、熔断各自从管道杀出。你以为单挑,其实群殴。
IHttpClientFactory
├── 管理底层 Handler 生命周期 → 定期回收 → DNS 自动刷新
├── 复用连接池 → 减少 TIME_WAIT
└── 统一配置 baseAddress / timeout / defaultRequestHeaders
DelegatingHandler 管道:
业务代码.send(request)
│
▼
LoggingHandler ← 记录请求/响应日志
│
▼
AuthHandler ← 附加 Bearer Token
│
▼
RetryHandler ← 失败重试
│
▼
HttpClientHandler ← 真正发送实践案例:云服务 SDK 的标准配方
以 OBS 客户端为例,展示云服务 SDK 如何利用天擎封装为开箱即用的扩展包:
// 1. 定义选项
class ObsOptions {
public var endpoint: String = ""
public var accessKey: String = ""
public var secretKey: String = ""
public var bucket: String = ""
public var timeout: Duration = Duration.second * 30
}
// 2. 服务类 —— 注入 HttpClient + IOptions
class ObsClient {
private let client: HttpClient
private let options: ObsOptions
public init(client: HttpClient, options: IOptions<ObsOptions>) {
this.client = client
this.options = options.value
}
public func putObject(key: String, data: Array<Byte>): Unit {
client.put("/${options.bucket}/${key}", ByteArrayContent(data))
}
public func getObject(key: String): Array<Byte> {
client.get("/${options.bucket}/${key}").content.readAsByteArray()
}
}
// 3. 收敛注册 —— 扩展方法
extend ServiceCollection {
public func addObsClient(configureAction: (ObsOptions) -> Unit): HttpClientBuilder {
this.configure<ObsOptions>(configureAction)
return this.addHttpClient<ObsClient, ObsClient> { client, sp =>
let options = sp.getOrThrow<IOptions<ObsOptions>>().value
client.baseAddress = URL.parse(options.endpoint)
client.timeout = options.timeout
}
}
}HttpClient 写完了,Handler 随意往 Builder 上插:
// addObsClient 返回 HttpClientBuilder,Handler 直接链在注册行后面——还能顺手补一刀 HttpClient 配置
services.addObsClient { options =>
config.getSection("Obs").bind(options)
}
.configureHttpClient { client => // 补一刀:细调 HttpClient 自身
client.defaultRequestVersion = HttpVersion.Version2
client.defaultRequestHeaders.add("User-Agent", "ObsClient/1.0")
}
.addHttpMessageHandler<LoggingHandler>() // ① 先记日志
.addHttpMessageHandler<AuthHandler>() // ② 再签 AK/SK
.addHttpMessageHandler<RetryHandler>() // ③ 失败重试
.addHttpMessageHandler<CircuitBreakerHandler>() // ④ 连续失败熔断
// 注册顺序 = 管道顺序:Logging → Auth → Retry → CircuitBreaker → 真正发送
// 你以为只调了一行 addObsClient,其实背后一群 Handler 群殴addObsClient 返回 HttpClientBuilder,.addHttpMessageHandler<T>() 在 Builder 上链式注册。日志、认证、重试、熔断——想加哪个加哪个,想换顺序换顺序。每个 Handler 只做一件事,组合起来就是完整的横切关注点矩阵。你以为单挑,其实群殴。
同一个配方可以复用到:
| 云服务 SDK | 封装对象 |
|---|---|
| 华为云 OBS / 阿里云 OSS / AWS S3 | 对象存储客户端 |
| 阿里云短信 / 腾讯云短信 / Twilio | 短信发送客户端 |
| 微信支付 / 支付宝 / Stripe | 支付客户端 |
| 微信公众号 / 企业微信 / 钉钉 | 消息推送客户端 |
| OpenAI / Claude / 文心一言 | AI API 客户端 |
| 高德地图 / 百度地图 / Google Maps | 地图服务客户端 |
| 快递 100 / 顺丰 / 菜鸟 | 物流查询客户端 |
**对 SDK 开发者意味着什么?**不再需要在自己的 SDK 里管理 HTTP 连接池、处理 DNS 刷新、实现重试和日志——这些全由天擎的 IHttpClientFactory + DelegatingHandler 管道接管。SDK 开发者专注业务逻辑即可。
社区可扩展的 Handler:
| Handler | 功能 |
|---|---|
RetryHandler | 指数退避重试,可配置次数和间隔 |
CircuitBreakerHandler | 熔断,连续失败 N 次后拒绝请求一段时间 |
TracingHandler | 注入 X-Trace-Id,联动 OpenTelemetry |
AuthHandler | 自动刷新 OAuth Token / AK/SK 签名 / HMAC 鉴权 |
RateLimitHandler | 令牌桶/漏桶限流,保护下游 |
CachingHandler | 响应缓存,按 URL + Method 缓存 GET 结果 |
CompressionHandler | 自动 GZip/Brotli 解压响应 |
这些 Handler 写好一个,所有 HttpClient 都能复用——addHttpMessageHandler<T>() 一行注册到管道。
痛点六 · 鱼肠剑:存储介质太多,每种 SDK 一套 API,换一次改一次代码
藏得最深的剑,出鞘最快 - 鱼肠剑
Redis 用 SET/GET,Memcached 用 set/get,MongoDB 用 find/insert,MySQL 写 SQL——四种存储四种 SDK。开发时用 Memory 跑通,切 Redis 要全局改签名,再切 MongoDB 又是几十处——业务代码和存储强耦合。
天擎的答案:Redis 用 SET/GET,Memcached 用 set/get,MongoDB 用 find/insert——四种存储四种 SDK,换一次改一次代码。混乱源于业务代码和存储强耦合。IDistributedCache 把存储藏到接口后面——四个方法 get/set/refresh/remove 通吃所有存储介质。平时看不见底层是 Memory 还是 Redis,用时 cache.getString() 一击即出。剑藏得越深,拔得越快。就像鱼肠剑藏于鱼腹之中,平时看不见、摸不着,但拔出来一击即中:
IDistributedCache(接口)
/ \
MemoryDistributedCache RedisDistributedCache
(当前内置) (未来 / 社区提供)
业务代码始终只和 IDistributedCache 打交道:
cache.setString("key", "value")
cache.getString("key") ?? "default"
cache.refresh("key")
cache.remove("key")实践案例:从开发到生产无缝切换
没有 IDistributedCache 时,换一次存储改一次代码:
// ❌ 绑死了 Redis——切 MongoDB 要全局改签名
class OrderService {
private let redis: RedisClient
public init(redis: RedisClient) { this.redis = redis }
public func getConfig(key: String): ?String {
return redis.get("config:${key}") // SET/GET 是 Redis 的方言
}
}有了 IDistributedCache——存储藏到接口后面,底层随便换:
// ✅ 只面对 IDistributedCache——底层是 Memory 还是 Redis,代码不动
class OrderService {
private let cache: IDistributedCache
public init(cache: IDistributedCache) { this.cache = cache }
public func getConfig(key: String): ?String {
return cache.getString("config:${key}") // getString 是统一的语言
}
}
// 开发环境 —— 藏在内存里,零依赖启动
services.addDistributedMemoryCache()
// 生产环境 —— 藏在 Redis 里,拔出来一样快
services.addDistributedRedisCache { options =>
options.connectionString = "redis://prod-cluster:6379"
}业务代码中 cache.setString("session.user", "alice") 的写法完全不变。剑藏得越深,拔得越快——换存储只改注册行,业务代码零改动。
社区可构建的方向:
| 缓存后端 | 特点 |
|---|---|
| Redis | 分布式、持久化、集群、哨兵 |
| MongoDB | 文档型、灵活 TTL 索引 |
| MySQL / PostgreSQL | 利用已有数据库,无需额外中间件 |
| Memcached | 简单 KV,极致性能 |
每种只需要实现 IDistributedCache 的四个方法:get()、set()、refresh()、remove()。另外,定义 DistributedCacheEntryOptions 统一了三种过期策略(相对过期、绝对过期、滑动过期),缓存实现不用各自发明过期机制。
痛点七 · 干将剑:每个类都要手写序列化逻辑,字段变动时同步维护繁琐易错
最好的铸剑师,手上没有茧 - 干将剑
50 个字段的类,不用宏就得手写三四百行。每个字段都要在属性声明、serializeObject()、deserializeObject()、构造器四个地方各写一遍——新增一个字段漏了任何一处,编译期查不出来,运行时才炸。
天擎的答案:50 个字段的类,不用宏就得手写三四百行——属性声明、序列化、反序列化、构造器,每个字段要在四个地方各写一遍。混乱源于手工维护——新增一个字段漏了任何一处,编译期查不出来,运行时才炸。@Serialization 编译期宏把重复劳动全自动生成——字段只定义一次,属性、序列化、反序列化、构造器全部由宏接管。新增字段只改类定义一处。真正的高手,手上没有茧。
没有宏时,一个类手写序列化接口——每个字段四处重复:
class User {
private var _id: Int64 = 0
private var _name: String = ""
// 1. 手写公开属性(_id → id)—— 50 个字段 50 个 prop
public prop id: Int64 {
get() { _id }
set(value) { _id = value }
}
public prop name: String {
get() { _name }
set(value) { _name = value }
}
// 2. 手写序列化 — 每个字段逐个塞进 DataModel
public func serializeObject(): DataModel {
let dm = DataModelStruct()
dm.add("id", DataModelInt(_id))
dm.add("name", DataModelString(_name))
return dm
}
// 3. 手写反序列化 — 每个字段逐个 match → 判 null
// (三个字段已经这么多行,50 个字段就是 200 行 match)
}每个字段四处重复——属性、序列化、反序列化、构造器。新增 email 漏了 deserializeObject?编译通过,运行时空值。
@Serialization 宏——字段只定义一次,其余全自动:
import soulsoft_serialization.*
import soulsoft_serialization.macros.*
@Serialization
class User {
private var _id: Int64 = 0
private var _name: String = ""
private var _tags: Array<String> = []
}
// 序列化——零手写
let user = User(id: 1, name: "alice", tags: ["admin", "dev"])
let json = JsonSerializer.serializeObject(user)
// {"id":1,"name":"alice","tags":["admin","dev"]}
// 反序列化——零手写
let restored = JsonSerializer.deserializeObject<User>(json)
println(restored.name) // alice
// 新增字段?只改类定义——属性、序列化、反序列化、构造器全由宏自动更新真正的高手,手上没有茧。你只定义类,宏把重复劳动全接管。嵌套对象?OrderItem 也加 @Serialization,自动递归——不用手写一行递归代码。
序列化的价值不止于 JSON ↔ 对象转换。DataModel 作为中间表示,可以在两个 @Serialization 类之间直接映射——不经过 JSON 字符串,性能更高:
// Entity → DTO 映射,不走 JSON 字符串
let entity = UserEntity(id: 1, name: "alice", password: "secret")
let dto = MapperUtilities.map<UserDto>(entity)
// UserDto 没有 password 字段 → 忽略,UserDto 多了字段 → 保留默认值社区可扩展的方向:
| 扩展类型 | 案例 |
|---|---|
自定义 JsonConverter<T> | Money → 写为 {"amount":100,"currency":"CNY"};PhoneNumber → 格式校验后写为字符串 |
| 自定义序列化格式 | 基于 DataModel 中间表示,扩展输出为 YAML、XML、MessagePack |
| 加密序列化 | 敏感字段自动加密后再序列化,反序列化时自动解密 |
| 版本兼容 | 字段重命名时保留旧名兼容读取,写入用新名 |
统一的集成公式
回顾上面七个案例,所有基础设施的集成都遵循同一条公式:
1. 定义接口/抽象类(IDistributedCache / ILoggerProvider / ConfigurationProvider / DelegatingHandler / JsonConverter / IOptions<T>)
│
2. 实现接口(写业务逻辑,不关心底层管道) ← 你只做这一步
│
3. 收敛注册为扩展方法(addXxx() / addXxx { ... })
│
4. 注入容器(天擎接管生命周期、配置绑定、依赖解析) ← 天擎帮你做这些开发者只需要做第 2 步。 第 1 步的接口天擎已经定义好了,第 3、4 步是天擎接管的部分。这也是为什么 .NET 生态中 NuGet 上有超过 30 万个包——不是因为 .NET 本身功能多,而是因为基础设施提供了标准接口,社区在上面快速生长。
展望:天擎生态的可能性
每项基础设施都是一个"接口平台"。接口下面,实现可以无限替换;接口上面,业务代码以统一的方式消费。
开发者体验层
一行 services.addXxx() 即可集成
│
┌────────────────┼────────────────┐
▼ ▼ ▼
云服务 SDK 生态 企业办公生态 数据中间件生态
────────────── ──────────── ──────────────
OBS / OSS / S3 飞书 / 钉钉 Redis / Kafka
短信 / 支付 企业微信 MongoDB / ES
AI API 网关 OA / 审批 Nacos / Apollo
│ │ │
└────────────────┼────────────────┘
▼
天擎基础设施层
DI · 配置 · 选项 · 日志 · HTTP · 缓存 · 序列化三个生态方向已经在前面展示了明确的实现路径:
| 生态方向 | 核心接口 | 标准化注册 | 开发者只需关注 |
|---|---|---|---|
| 云服务 SDK | HttpClient + IOptions<T> | services.addXxxClient { } | API 业务逻辑 |
| 企业办公集成 | HttpClient + DelegatingHandler | services.addXxx() + addHttpMessageHandler | 消息推送/审批流程 |
| 数据中间件 | IDistributedCache / ConfigurationProvider | services.addDistributedXxx() | 存储/配置读写 |
生态共建
天擎的基础设施层从设计之初就为扩展预留了接口——不是"先有内部实现再对外开放",而是每个模块都先定接口,内部实现和第三方扩展站在同一条起跑线上。
- 缓存:
IDistributedCache等待 Redis、MongoDB、MySQL 实现 - 日志:
ILoggerProvider等待文件、ES、Kafka、飞书实现 - 配置:
ConfigurationProvider等待 Nacos、Apollo、Consul 实现 - HTTP:
DelegatingHandler等待链路追踪、熔断、签名实现 - 序列化:
JsonConverter<T>等待自定义类型转换实现 - DI 集成:任何
interface + 实现都能收敛为一行services.addXxx()
如果你正在为仓颉生态编写类库,请尽量基于天擎的接口和选项模式来设计。用户只需一行 services.addXxx(),剩下的由天擎接管生命周期、配置绑定和依赖解析。统一的注册体验,降低整个生态的学习成本。
欢迎广大开发者参与天擎生态共建。