Skip to content

基础设施总览


能力全景


七剑下天山——能力矩阵

天擎七项基础设施,如七把名剑——刀斩肉身,心斩灵魂。每把剑各斩一痛,各开一疆。合在一起,便是完整的云原生基础设施基座。

剑阵

七把剑不是各自孤立的——它们有同炉、有同师。剑与剑之间的连线,就是模块与模块之间的连线。

                    泰阿剑 · 配置         横扫六合,百川归服
                         │  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 时,散落一地:

cangjie
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

日志团队在自己的包里:

cangjie
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
    }
}

缓存团队在自己的包里:

cangjie
extend ServiceCollection {
    public func addDistributedMemoryCache(): ServiceCollection {
        this.addSingleton<IDistributedCache>(MemoryDistributedCache(Options.create(DistributedCacheOptions())))
        return this
    }
}

订单团队在自己的包里——把所有订单相关组件封成一把剑:

cangjie
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——链不断。

第二步:黄帝拔剑。 启动代码只剩三条链:

cangjie
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:

cangjie
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 判断优先级

有了配置系统——所有来源链式添加,最终都是同一种读法:

cangjie
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:

cangjie
// ❌ 字符串 key:拼写错误编译通过,运行时静默炸
class ChatService {
    public init(config: IConfiguration) {
        let key = config["llm:apiKey"]   // 写成 "llm:apikey" 照样编译通过
        let url = config["llm:baseUrl"]  // 构造函数签名看不出依赖了哪些 key
    }
}

选项模式立下规矩——类定义 → 从配置绑定 → 注入使用,一条完整的类型安全链路:

cangjie
// 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

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>,同一套规矩管多套配置:

cangjie
// 同一类,两个名字,两套配置——规矩只立一次
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()——日志穿梭其中,业务代码毫无感知:

cangjie
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 如何利用天擎封装为开箱即用的扩展包:

cangjie
// 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 上插:

cangjie
// 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 时,换一次存储改一次代码:

cangjie
// ❌ 绑死了 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——存储藏到接口后面,底层随便换:

cangjie
// ✅ 只面对 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 编译期宏把重复劳动全自动生成——字段只定义一次,属性、序列化、反序列化、构造器全部由宏接管。新增字段只改类定义一处。真正的高手,手上没有茧。

没有宏时,一个类手写序列化接口——每个字段四处重复:

cangjie
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 宏——字段只定义一次,其余全自动:

cangjie
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 字符串,性能更高:

cangjie
// 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 · 缓存 · 序列化

三个生态方向已经在前面展示了明确的实现路径:

生态方向核心接口标准化注册开发者只需关注
云服务 SDKHttpClient + IOptions<T>services.addXxxClient { }API 业务逻辑
企业办公集成HttpClient + DelegatingHandlerservices.addXxx() + addHttpMessageHandler消息推送/审批流程
数据中间件IDistributedCache / ConfigurationProviderservices.addDistributedXxx()存储/配置读写

生态共建

天擎的基础设施层从设计之初就为扩展预留了接口——不是"先有内部实现再对外开放",而是每个模块都先定接口,内部实现和第三方扩展站在同一条起跑线上

  • 缓存IDistributedCache 等待 Redis、MongoDB、MySQL 实现
  • 日志ILoggerProvider 等待文件、ES、Kafka、飞书实现
  • 配置ConfigurationProvider 等待 Nacos、Apollo、Consul 实现
  • HTTPDelegatingHandler 等待链路追踪、熔断、签名实现
  • 序列化JsonConverter<T> 等待自定义类型转换实现
  • DI 集成:任何 interface + 实现 都能收敛为一行 services.addXxx()

如果你正在为仓颉生态编写类库,请尽量基于天擎的接口和选项模式来设计。用户只需一行 services.addXxx(),剩下的由天擎接管生命周期、配置绑定和依赖解析。统一的注册体验,降低整个生态的学习成本。

欢迎广大开发者参与天擎生态共建。