Halyx:一个Jar包跨AWS、Azure和谷歌云运行云无关Java微服务
很多团队把自己的微服务称为"云无关",但只要仔细看看代码,这个说法往往就站不住脚。业务逻辑可能是中立的,集成层通常不是。数据访问、消息传递、缓存和认证一般都直接构建在某一家云厂商的 SDK 之上。把这样的服务迁移到另一朵云,或者让它同时运行在两朵云上,并不是改改配置就行:这意味着要重写并重新测试所有与平台打交道的部分。
Halyx 是我一直在开发的一个开源 Java 库,它把这层集成从你的代码里移了出去。服务需要哪一类云服务,就调用一个 Halyx API——从对象存储、文档数据库和 SQL 数据库,到消息传递、事件、Kafka 流、缓存和密钥——由 Halyx 在各个云自己的产品上执行。API 和错误在所有云上都一样;在云之间确实存在差异的地方,Halyx 会声明这种差异,而不是把它藏起来。迁移到另一朵云,或者同时运行在两朵云上,就变成了它本来就该是的那个配置变更,而与平台打交道的代码只写一次、只测一次。
使用 Halyx,你只需编写和构建一次,即可部署到任意云——AWS、Azure 或 Google Cloud——具体选择在部署时通过配置决定。
本文展示如何用 Halyx 构建一个真正云无关、可移植的微服务,示例是 sym(Speak Your Mind 的缩写):一个小型 Twitter 克隆,用到文档数据库、对象存储、一个 Kafka 流和一个缓存,而它的 Java 代码中任何地方都没有直接引用云 SDK。你只会看到涉及云的那部分代码,每个服务 API 一小段,在 AWS、Azure 和 Google Cloud 上完全相同。然后会说明这对迁移、云退出、多云以及日常维护意味着什么。完整代码在 github.com/kamranzafar/sym。
四种云服务,各用一个 API
sym 需要四种云服务。每一种在每朵云上都是不同的产品,而代码里只有一个 Halyx API:
[LOADING...]
sym 在模拟器上测试。Halyx 自身的真实云测试覆盖了 Event Hubs 和 Managed Kafka;MSK 的测试已就绪,只等一个允许使用 MSK 的 AWS 账号。
任何地方都不出现密码。Halyx 用平台的身份登录:EKS 上的 Pod Identity、GKE 上的 Workload Identity、AKS 上的 Entra Workload ID,或者在笔记本上使用你自己的 aws、az 或 gcloud 登录。
下面是一条消息如何流经这些服务:
[LOADING...]
资源只在配置里命名一次
代码从不指名某张表、某个 bucket 或某个 topic。application.yml 的共享部分负责命名,在所有云上都一样,每个键都会变成一个 Spring bean,代码按名字注入:
每朵云上会变化的是一个指明提供方的简短 profile,外加只有部署时才知道的信息:
local profile 不需要自己的 Kafka 地址:它把流集群指向 Halyx 的进程内 broker,并且不做认证:
构建中包含全部三个提供方模块——halyx-aws、halyx-azure 和 halyx-gcp——所以一个 jar 可以到处运行,你只需构建和测试一个产物。Halyx 使用资源,但不创建资源:表、bucket 和 topic 来自你的基础设施即代码。
每朵云上都是同一份代码
下面这些是 sym 代码中唯一涉及云的地方。没有一个导入云 SDK,每一个都能在 AWS、Azure、Google Cloud 和内存中不加修改地运行。
文档:一个 Spring Data repository
Halyx 在启动时检查每个派生查询。它必须使用分区键,或者使用已声明的索引,并且对排序键最多只能有一个条件。任何会扫描表的写法——例如 Or、findAll 或按偏移分页——都会导致启动失败,因此在一朵云上开销很低的查询,在三朵云上开销都很低。
流:发送与监听
StreamProducer.send 会等到 broker 已存储该记录后才返回,在 MSK、Event Hubs 或 Managed Kafka 上都一样。每朵云都保证按 key 有序以及至少一次投递。在云之间有差异的地方,Halyx 会明说:事务是一项声明的能力,而 Event Hubs 没有,所以 sym 使用幂等生产者——三家都支持。
图片:一个 BlobStore
BlobStore.put 在 S3、Blob Storage 和 Cloud Storage 上是同一个调用,而且每家都会随图片一起存储内容类型。图片不存在时,在每朵云上都是 Halyx 的 NotFoundException,sym 把它转成 404。
缓存:普通的 Spring 注解
可选的 cache profile 用 ElastiCache、Azure Managed Redis 或 Memorystore 来支撑 Spring 的缓存抽象。它会失败时放行(fail open):如果缓存不可用,查询就直接走数据库。
错误:一个处理器
失败在每朵云上都抛出相同的异常类型,所以一个处理器就能覆盖三家。只有当异常是 ThrottledException 和 UnavailableException 时 isRetryable() 才为真,因此也只有这两种会告诉客户端重试;AccessDeniedException 或 InvalidArgumentException 是服务本身或权限上的故障,重试解决不了。消息或图片不存在会在这之前处理,作为 NotFoundException 由 sym 转成 404。sym 中没有任何地方捕获 DynamoDB、Cosmos DB 或 Firestore 的异常。
就这些。sym 共 12 个文件、约 300 行代码,没有一个文件知道自己跑在哪朵云上。
证明它:一套测试,覆盖每个目标
没测过的可移植性只是一种愿望。sym 的测试是一套,只针对 HTTP API 编写,每个子类在不同的 profile 上运行它。模拟器运行用 Testcontainers 启动各家云的模拟器:
- 内存:
LocalDeploymentTest(local)和LocalCacheDeploymentTest(local,cache),各 11 个测试 - AWS 模拟器:
AwsEmulatorIT(aws,cache),在 Floci 上跑 DynamoDB 和 S3,外加 Kafka 和 Valkey,11 个测试 - Azure 模拟器:
AzureEmulatorIT(azure,cache),在 Floci-az 上跑 Cosmos DB 和 Blob Storage,外加 Kafka 和 Valkey,11 个测试 - Google Cloud 模拟器:
GcpEmulatorIT(gcp,cache),在 Floci-gcp 上跑 Firestore 和 Cloud Storage,外加 Kafka 和 Valkey,11 个测试 - 打包后的 jar:
PackagedJarIT用java -jar在local,cache上启动构建好的 jar,1 个测试
ProfilesTest 在不连接任何地方的情况下解析每朵云的真实 profile,并检查提供方类型、IAM 登录方式、凭据的不存在以及资源名称。加上它和 HashtagIndexerTest(把同一条 Kafka 记录交给消费者两次,期望只产生一条条目),总共 67 个测试,在笔记本上 mvn verify 不到两分钟跑完。为了确认这套测试确实能失败,我一次一处地破坏了七个地方,从错误的表名到非幂等的消费者,它全都发现了。Halyx 本身针对真实的 AWS、Azure 和 Google Cloud 账号运行同一套一致性测试并公布结果;在 AWS 上,Kafka 那一层仍在等待 MSK 权限。
这带来什么好处
当云是部署选择而不是代码依赖时,重大的云决策就变成了配置和测试工作,而不是重写。
迁移与云退出。 换云通常意味着重写并重新测试每一个调用旧云 SDK 的类,而退出方案是在真正需要它的那天之前没人能验证的东西。用 Halyx,你为目标云开通资源、切换 profile、对它跑同一套测试、迁移数据。因为目标云的 profile 已经能通过测试,你可以随时演练退出。
多云与灾难恢复。 在第二朵云上运行通常意味着一个分支或第二套代码库,它会逐渐漂移,而且很少真正跑起来。用 Halyx,同一个 jar 在每朵云上用各自的 profile 运行,并通过同一套测试,所以故障切换是一次你演练过的部署。一个服务甚至可以跨云:每个资源在配置里指明自己的提供方,所以文档可以放在一朵云上,而流放在另一朵云上。让备用环境中的数据保持最新仍然是你的工作,可以通过定时导出或跨云流镜像来做,而这决定了你可能丢失多少数据。
客户或监管机构指定云。 银行、政府机构和大企业常常要求特定的云或主权区域。通常这意味着在你能答应之前先做一次移植。用 Halyx,这只是一个新 profile 加一次同一套测试的运行。
一致的运维。 在多个云上运行服务的值班团队通常需要了解每个 SDK 的故障模式。用 Halyx,失败在每朵云上都抛出相同的异常,具有相同的投递和顺序保证,所以一套告警、仪表盘和运维手册就能覆盖全部。
可维护性。 按云分开的代码、错误和测试通常需要手工保持同步。用 Halyx,只有一套代码库、一个异常模型和一套测试,而提供方 SDK 和登录都放在库里。升级云 SDK 就是通过 BOM 升级一个 Halyx 版本;而由于所有登录都用云自己的身份,没有密钥需要轮换或泄露。
迁移一个正在运行的服务仍然不只是代码的事。DynamoDB 和 S3 里的数据需要用各云自己的传输工具复制到 Firestore 和 Cloud Storage,权限来自你的基础设施即代码。Halyx 去掉的是重写这件事。对于已经构建在 Spring Cloud AWS、Azure 或 GCP 上的服务,Halyx 的迁移指南会把每个集成映射到对应的 Halyx 等价物,包括既有数据和上线方式。
你需要放弃什么
Halyx 让代码可移植;它并不让各朵云变得一模一样。有几件事仍然是你要操心的:
- 并非每个特性到处都有。 差异以能力的形式声明出来,而不是被隐藏:Pub/Sub 没有消息去重或延迟投递,Halyx 不声明 Event Hubs 上的 Kafka 事务,而 Google Cloud 上的预签名 URL 需要能够签名的凭据。代码用
supports()检查某项能力,或用require()断言它,不支持的调用会抛出UnsupportedFeatureException,而不是被静默忽略。 - 每朵云的限制依然适用。 一条文档在 DynamoDB 上最大 400 KB,在 Cosmos DB 上是 2 MB;二级索引在 DynamoDB 上是最终一致的;Firestore 把小数存成 double。可移植的数据设计要遵守三者中最严格的那一条。
- 原生特性会离开可移植路径。
unwrap会把配置好的 SDK 客户端交给你,用于 S3 Object Lock 或 Cosmos DB 变更流这类事情。把这些调用放在你自己的一个接口后面,因为在另一朵云上它们会按设计失败。 - 基础设施和重试是你的事。 Halyx 不创建表、bucket 或 topic,不等待最终一致性过去,默认也不添加自己的重试:云 SDK 已经会重试瞬时故障。要在各云之间统一重试,可以安装可选的
RetryInterceptor,它会重试那些因限流或不可用错误而失败的幂等操作。 - 它是 Java 的。 Halyx 需要 Java 21 或更高版本。多语言环境中的其他语言需要类似 Dapr 的东西。
Halyx 与 Spring Cloud、Dapr 有何不同
Spring Cloud AWS、Spring Cloud Azure 和 Spring Cloud GCP 各自把一朵云带进 Spring:针对 Spring Cloud AWS 的 DynamoDB 支持编写的代码无法在 Cosmos DB 上运行。Halyx 在三者之上放了一个 API,而 halyx-ai-migrate 的存在就是为了把服务从这些项目上迁走。Dapr 也是可移植的,但它是运行时:每个服务通过 HTTP 或 gRPC 调用一个 sidecar,而 sidecar 及其组件要部署并运维在每个服务旁边。Halyx 是 jar 里面的一个库,没有额外进程,而且它使用 Java 团队已经熟悉的惯用法:Spring Data repository、Spring 的缓存注解,以及保留顺序和消费者组的 Kafka。
保持可移植:三个 AI 技能
sym 的大部分内容我并不是手敲的。Halyx 在 .claude/skills 里为 AI 编程助手提供了三个技能,都是纯 Markdown,Claude Code 会把它们作为技能运行,其他助手也可以把它们当指令来遵循:
- halyx-ai-create 在写代码之前先规划一个服务:每个资源、它在每朵云上对应的服务,以及各云之间的差异。在我批准计划之后,它构建了 sym。
- halyx-ai-review 追问一个服务是否只改 profile 就能在每朵云上运行且行为一致。它会查找位于某个类之外的云 SDK 调用、共享配置里的云设置、某朵云缺少的消息特性、profile 里的凭据,以及只跑一朵云的测试。sym 的报告
PORTABILITY-REVIEW.md给出的结论是在 AWS、Azure 和 Google Cloud 上均已就绪,没有阻塞项。 - halyx-ai-migrate 把已有的 Spring Cloud AWS、Azure 或 GCP 服务迁移到 Halyx 上,每次一个经过审查的步骤。
要试用其中一个,把它的文件夹复制到你项目的 .claude/skills,然后运行 /halyx-ai-create、/halyx-ai-review 或 /halyx-ai-migrate。## 试一试
- 代码: github.com/kamranzafar/sym。使用 Java 21 和 Maven,克隆后在 Docker 运行的情况下执行
mvn verify,或者完全不用 Docker,直接在local上启动。 - Halyx: halyx.dev 为每个服务提供了指南,以及每个云厂商的真实云测试结果。
- 更完整的示例: Halyx 的
portable-orders示例 用到了所有 Halyx 服务,并附带一个 Helm chart,可将同一个镜像部署到 EKS、GKE 和 AKS。 - 你自己的服务: Halyx 迁移指南 将 Spring Cloud AWS、Azure 和 GCP 映射到 Halyx,包括现有数据和上线流程。
如果你在真实云上试用了 sym,或在你自己的服务上试用了 Halyx,我很想听听哪些地方可行、哪些地方不可行。欢迎在 Halyx 的 GitHub 仓库 上提交 issue 或发起讨论。
分享此页面
发现错误,或有补充?在 GitHub 上编辑此页面
[LOADING...]
作者:
Kamran Zafar
Kamran Zafar 是一位资深 Java、云和 AI 工程师,也是 Halyx 的创建者。Halyx 是一个开源库,用于构建云无关的 Java 微服务,这些微服务无需修改即可在 AWS、Azure 和 Google Cloud 上运行。
相关文章
云 [LOADING...]
[LOADING...]Hector Ventura2026年4月3日 2,228 次浏览
Introducing Floci: A High-Performance, GraalVM-Powered AWS Emulator
云 [LOADING...]
[LOADING...][LOADING...]2 位作者 2026年5月25日 1,427 次浏览
Foojay Podcast #96: Local AWS Development Without LocalStack: Meet Floci, the GraalVM-Powered Alternative
设计模式 [LOADING...]
[LOADING...]Youssef Ben Henda2026年8月4日 2,437 次浏览
Idempotent Spring Boot: Safe REST with One Annotation
云 [LOADING...]
[LOADING...]Frank Delporte2026年6月18日 841 次浏览
Building Java Containers Without a Dockerfile: Azul Zulu and Paketo Buildpacks
参与讨论
云 [LOADING...]
[LOADING...]Hector Ventura2026年4月3日 2,228 次浏览
Introducing Floci: A High-Performance, GraalVM-Powered AWS Emulator
云 [LOADING...]
[LOADING...][LOADING...]2 位作者 2026年5月25日 1,427 次浏览
Foojay Podcast #96: Local AWS Development Without LocalStack: Meet Floci, the GraalVM-Powered Alternative
设计模式 [LOADING...]
[LOADING...]Youssef Ben Henda2026年8月4日 2,437 次浏览
Idempotent Spring Boot: Safe REST with One Annotation
云 [LOADING...]
[LOADING...]Frank Delporte2026年6月18日 841 次浏览