无框架,无痛:编写 Aether Slices

发布: (2026年2月19日 GMT+8 18:08)
14 分钟阅读
原文: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have it, I’ll translate it into Simplified Chinese while preserving all formatting, markdown, and technical terms.

Source:

这里是一整个可部署的服务

@Slice
public interface OrderService {
    Promise placeOrder(PlaceOrderRequest request);

    static OrderService orderService(InventoryService inventory,
                                     PricingEngine pricing) {
        return request -> inventory.check(request.items())
                                   .flatMap(pricing::calculate)
                                   .map(OrderResult::placed);
    }
}

这并不是一个简化的例子,而是真实的代码。

  • 注解处理器会看到 @Slice,读取工厂方法的签名,并生成:
    • 装配代码,
    • 远程调用的代理,
    • 部署元数据。

你只需要 写一个接口,就能得到一个能够在分布式集群中透明扩展、故障转移和路由的服务。

No @Autowired
No application.yml
No @Configuration 类。
No @Bean 方法。
No 组件扫描。
No 服务定位器。
No 依赖注入容器。

工厂方法 本身就是 依赖注入。它的参数即声明的依赖。编译器会检查这些依赖;注解处理器负责装配。无需配置,亦无需在凌晨 2 点去排查遗漏或调试。

注意工厂返回的是什么:一个 lambda。没有实现类。接口只有一个方法时,工厂返回的 lambda 直接实现该接口——业务逻辑即函数。对于拥有多个方法的 slice,私有的 record 捕获依赖并实现接口——仍然没有单独的 Impl 类、没有需要维护的文件、也没有需要追踪的间接层。

每个 slice 遵循两条规则

  1. 工厂方法声明依赖 – slice 所需的外部资源全部集中在一个地方:工厂方法的签名。

    static OrderService orderService(InventoryService inventory,
                                     PricingEngine pricing) {
        return request -> inventory.check(request.items())
                                   .flatMap(pricing::calculate)
                                   .map(OrderResult::placed);
    }

    阅读工厂方法,了解依赖。 没有任何配置文件可以与之冲突。运行时也不会出现编译器未见过的依赖。

  2. Promise 返回类型 – 所有方法都返回 Promise。这不是一种风格选择,而是实现透明分布式的关键。无论调用是在进程内部还是跨网络,调用者看到的都是同一种类型。

就这么简单——两条规则。其余一切都由它们决定。

注解处理器如何对工厂参数进行分类

处理器看到的内容处理方式
@PrimaryDb DatabaseConnector db资源 – 从配置中提供
InventoryService inventory外部切片 – 生成网络代理
OrderValidator validator带工厂的本地接口 – 直接调用工厂

你不需要进行配置。你也不需要在依赖上使用 @Inject@Qualifier 注解(基础设施资源除外)。只需列出你需要的内容,处理器会自行决定如何提供它。

基础设施资源

使用 @ResourceQualifier 注解的参数属于基础设施——例如数据库连接、HTTP 客户端、消息队列。处理器会根据配置为其提供实例:

@ResourceQualifier(type = DatabaseConnector.class, config = "database.primary")
public @interface PrimaryDb {}

@Slice
public interface OrderRepository {
    Promise findOrder(FindOrderRequest request);

    static OrderRepository orderRepository(@PrimaryDb DatabaseConnector db) {
        return request -> db.query(request.orderId())
                            .map(OrderResult::fromRow);
    }
}

远程依赖

一个来自其他包的 @Slice 接口参数被视为 远程依赖。处理器会生成一个代理记录,将调用委托给运行时的调用框架。你的代码像普通方法调用一样调用 inventory.check(request);代理会处理序列化、路由、重试和故障转移。

本地依赖

一个参数是普通接口且具有静态工厂方法时,它是 本地 的。处理器直接调用工厂——没有代理、没有网络、没有开销。

All three categories together

static LoanService loanService(@PrimaryDb DatabaseConnector db,
                               CreditBureau creditBureau,
                               RiskCalculator riskCalculator) {
    return request -> riskCalculator.assess(request)
                                    .flatMap(risk -> creditBureau.check(request.applicant()))
                                    .flatMap(credit -> persistDecision(db, request, credit));
}
  • Database – 来自配置。
  • Credit bureau – 通过网络代理。
  • Risk calculator – 本地实例化。

一行代码声明了所有内容。处理器负责其余工作。

结构化错误处理

框架会教你抛出异常。Spring 将它们转换为 HTTP 状态码,Jackson 序列化错误响应,异常处理器将类型映射到消息。这在有人抛出意外异常时就会失效,而通用的 500 响应对调用方毫无提示。

Slices 使用 sealed Cause 层次结构

public sealed interface OrderCause extends Cause {
    OrderCause EMPTY_ORDER = new EmptyOrder("Order must have items");
    OrderCause INSUFFICIENT_STOCK = new InsufficientStock("Insufficient stock");

    static OrderCause insufficientStock(StockStatus stock) {
        return new InsufficientStock("Insufficient stock: " + stock);
    }

    record EmptyOrder(String message) implements OrderCause {}
    record InsufficientStock(String message) implements OrderCause {}
}

每一种失败模式都是一个类型。编译器知道所有这些类型。模式匹配可以穷尽地处理它们。不会再出现伪装成 500 错误的意外 NullPointerException

将错误处理视为业务关注点

当出现错误时,你应该使用流式风格 返回一个失败的 Promise,而不是吞掉异常:

return inventory.checkStock(stockRequest)
                .flatMap(this::verifyAvailability);
private Promise verifyAvailability(StockStatus stock) {
    return stock.sufficient()
        ? completeOrder(stock)
        : OrderCause.insufficientStock(stock).promise();
}
  • 运行时会在网络中传播 Cause
  • 调用方收到的是 类型化的失败,而不是普通的字符串消息。
  • 错误处理契约成为 API 的一部分,而不是 @ControllerAdvice 中的事后想法。

在没有容器的情况下进行测试

  • 没有需要启动的测试容器。
  • 没有需要配置的模拟服务器。
  • 没有加载半个宇宙的 @SpringBootTest
  • 没有模拟框架 —— 依赖都是接口,所以你可以传入返回恰好所需结果的 lambda。
class OrderServiceTest {

    @Test
    void placeOrder_succeeds_whenInventoryAvailable() {
        InventoryService inventory = request -> Promise.success(new StockResult("RES-123", true));
        PricingEngine pricing = request -> Promise.success(new PriceResult("ORD-456", 99.99));

        var service = OrderService.orderService(inventory, pricing);

        service.placeOrder(request)
               .await()
               .onFailure(Assertions::fail)
               .onSuccess(result -> assertEquals("ORD-456", result.orderId()));
    }

    @Test
    void placeOrder_fails_whenInventoryUnavailable() {
        InventoryService inventory = request -> OrderCause.INSUFFICIENT_STOCK.promise();
        PricingEngine pricing = request -> Promise.success(new PriceResult("ORD-456", 99.99));

        var service = OrderService.orderService(inventory, pricing);

        service.placeOrder(request)
               .await()
               .onSuccess(Assertions::fail);
    }
}
  • 依赖是接口 —— 传入返回成功或失败的 lambda。
  • 工厂方法负责把它们组装在一起。
  • 没有反射、没有类路径扫描、没有上下文初始化。
  • 没有 Mockito、没有 when(...).thenReturn(...)、没有 verify(...)

测试启动几乎是瞬间完成的,因为没有任何需要启动的东西:没有容器、没有框架、没有 Bean 解析 —— 只有对象之间的调用。

基于 Slice 的架构

单个 Maven 模块可以包含任意数量的 slice,只要有意义即可:

commerce/
  src/main/java/org/example/
    order/
      OrderService.java       # @Slice
    payment/
      PaymentService.java    # @Slice
    shipping/
      ShippingService.java   # @Slice
  • 每个 @Slice 会生成自己的 factoryAPI artifactdeployment metadata
  • Maven 插件会分别对它们进行打包。
  • 它们在共享领域类型、构建配置和仓库的同时,能够独立部署和扩展。

细粒度扩展

一个 slice 可以小到只有一个方法。由于没有运行时开销(没有容器、没有负载均衡器、没有每个服务的监控),可以对 slice 进行单独扩展:

  • 某个 slice 在峰值负载时提供 50 个实例
  • 另一个 slice 以 最小 容量闲置。

使用 Aether 时,这就是默认行为。

注解处理器生成的内容

考虑一个只有一个外部依赖的简单 slice:

@Slice
public interface OrderService {
    Promise placeOrder(PlaceOrderRequest request);

    static OrderService orderService(InventoryService inventory) {
        return request -> inventory.check(request.items())
                                   .map(OrderResult::fromAvailability);
    }
}

处理器检测到 InventoryService 是一个外部的 @Slice,并生成:

  1. 一个代理记录(proxy record),实现 InventoryService 并将每个方法调用委派给运行时的 SliceInvokerFacade
    你的代码调用 inventory.check(request) 时,代理会序列化请求,将其路由到托管 InventoryService 的节点,反序列化响应,并返回一个 Promise

  2. 一个工厂类,接受一个 AspectSliceInvokerFacade,创建代理并把所有部件连接起来。

  3. 部署元数据,位于 META-INF/slice/ 中——包括 slice 名称、其方法以及其依赖。
    运行时读取这些元数据以构建依赖图并确定部署顺序。

所有这些都在编译时生成、经过验证,并对你的业务逻辑透明。

逐步迁移现有 Spring 代码

您无需从头开始。任何现有的 Java 代码都可以通过一行代码转换为 slice。

传统 Spring 服务

@Service
public class OrderService {
    @Autowired private InventoryRepository inventory;
    @Autowired private PricingService pricing;

    @Transactional
    public OrderResult processOrder(OrderRequest request) {
        var availability = inventory.checkAvailability(request.getItems());
        if (!availability.isAvailable()) {
            return OrderResult.outOfStock(availability.getMissingItems());
        }
        var quote = pricing.calculateQuote(request.getItems(), request.getCustomerId());
        // ... payment, order creation, notification ...
        return OrderResult.success(order);
    }
}

包装为 slice

@Slice
public interface OrderProcessor {
    Promise processOrder(OrderRequest request);

    static OrderProcessor orderProcessor() {
        var legacyService = createLegacyService();
        return request -> Promise.lift(() -> legacyService.processOrder(request));
    }
}
  • Promise.lift() 包装同步调用,捕获任何异常,并返回一个带有 typed failure 的正确 Promise,而不是原始堆栈跟踪。

包装后的 slice 可以工作,但它是一个黑盒。剥离模式 可以逐层打开它——一次一层——同时在每一步都保持可运行的代码。

剥离外层结构

将不透明的 lift() 替换为 Sequencer,仍然对每一步进行包装:

return Promise.lift(() -> legacyCheckInventory(request))
              .flatMap(inv -> Promise.lift(() -> legacyCalculatePricing(inv)))
              .flatMap(quote -> Promise.lift(() -> legacyProcessPayment(quote, request)));

现在每个阶段都是显式的、可测试的、可组合的,同时整体流程仍然是一个单一的、类型安全的 promise 链。

Source:

迁移演练

ote)))
    .flatMap(payment -> Promise.lift(() -> legacyCreateOrder(payment)));

现在可以看到管道的全部步骤。你可以单独查看每一步,进行测试,并推理其流程。

再深入一步

挑选最关键的 lift() 并展开它:

private Promise checkInventory(OrderRequest request) {
    return Promise.all(
            Promise.lift(() -> legacyCheckWarehouse(request)),
            Promise.lift(() -> legacyCheckSupplier(request))
        )
        .map(this::combineAvailability);
}
  • 外层调用现在已经是干净的 JBCT
  • 内层调用仍然被包装着。
  • 每一步的测试都通过。

在任何位置都可以停止——混合的 JBCT 与遗留代码能够正常工作。剩余的 lift() 调用正好标记了遗留代码所在的位置。当这些 lift() 全部消除后,你就拥有了一段干净的代码片段。没有硬性截止日期;每一次剥离都能独立带来价值。

完整的迁移演练涵盖了整个路径——从最初的包装、容错处理到干净的 JBCT 代码。

Source:

为什么切片开发不同

传统的微服务开发是一场与框架的协商:

  • 你需要学习它们的抽象、生命周期钩子、配置 DSL、注解模型、错误处理约定以及测试工具。
  • 框架成为重心;你的业务逻辑围绕它旋转。

切片则颠倒了这一点。

  • 业务逻辑是中心。
  • 接口定义契约。
  • 工厂方法声明依赖。
  • 实现是一个 lambda 表达式。

其它一切——序列化、路由、扩展、故障转移、配置——都是运行时要处理的事。

“你不需要学习框架。你只需编写 Java 接口并实现它们。”

两条规则,其余的全部是你的领域。

没有框架。没有痛苦。

资源

  • Pragmatica Aether – 分布式 Java 运行时
  • GitHub Repository – [source code]
  • Slice Development Guide – 完整参考
0 浏览
Back to Blog

相关文章

阅读更多 »

测试,是鸡蛋还是鸡?

测试的封面图片:是先有鸡还是先有蛋? https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2F...

Java 中的接口

介绍 在 Java 中,接口用于实现抽象和多继承。它定义了类应该做什么,而不是如何去做。什么...