1. 简介

近年来,Java 生态中函数式和响应式编程逐渐兴起,而 Ratpack 正是顺应这一趋势的产物。它提供了一种构建 HTTP 应用程序的方式,强调异步、非阻塞的设计理念。

底层基于 Netty 实现网络通信,使得整个框架完全异步、非阻塞。此外,Ratpack 还提供了配套的测试库,便于我们编写单元测试。

在本文中,我们将深入探讨 Ratpack 中 HTTP 客户端的使用方式及其相关组件,并在此前 Ratpack 入门教程 的基础上进一步拓展理解。

2. Maven 依赖

首先添加必要的 Ratpack 依赖

<dependency>
    <groupId>io.ratpack</groupId>
    <artifactId>ratpack-core</artifactId>
    <version>1.5.4</version>
</dependency>
<dependency>
    <groupId>io.ratpack</groupId>
    <artifactId>ratpack-test</artifactId>
    <version>1.5.4</version>
    <scope>test</scope>
</dependency>

✅ 实际上,只需要这两个依赖就足以开发和测试一个基础的 Ratpack 应用。当然,你也可以根据需要引入其他扩展库来增强功能。

3. 核心概念

在深入了解 HTTP 客户端之前,我们先熟悉一下 Ratpack 的核心设计思想。

3.1. 基于 Handler 的请求处理机制

Ratpack 采用 Handler 模式来处理请求。每个 Handler 负责特定路径或逻辑的处理任务。

举个最简单的例子:

public class FooHandler implements Handler {
    @Override
    public void handle(Context ctx) throws Exception {
        ctx.getResponse().send("Hello Foo!");
    }
}

这个 Handler 接收请求并直接返回一段文本响应。

3.2. Chain、Registry 和 Context

✅ Handlers 通过 Context 对象与请求交互。我们可以从中获取请求对象、响应对象以及调用其他 Handlers。

来看一个实际的例子:

Handler allHandler = context -> {
    Long id = Long.valueOf(context.getPathTokens().get("id"));
    Employee employee = new Employee(id, "Mr", "NY");
    context.next(Registry.single(Employee.class, employee));
};

这个 Handler 执行预处理操作,将计算结果放入 Registry 后,再传递给下一个 Handler。

⚠️ 通过 Registry 我们可以在 Handlers 之间共享数据。下面这个 Handler 就是从 Registry 中取出 Employee 数据并输出:

Handler empNameHandler = ctx -> {
    Employee employee = ctx.get(Employee.class);
    ctx.getResponse()
      .send("Name of employee with ID " + employee.getId() + " is " + employee.getName());
};

📌 在生产环境中,通常我们会把这些 Handlers 拆分为独立类,以提升可读性、调试效率和业务逻辑的复用性。

接着,我们可以把这些 Handlers 组装进 Chain 中,构建复杂的请求处理管道:

Action<Chain> chainAction = chain -> chain.prefix("employee/:id", empChain -> {
    empChain.all(allHandler)
      .get("name", empNameHandler)
      .get("title", empTitleHandler);
});

💡 我们还可以通过 insert(...) 方法组合多个 Chains,让每条链专注于不同的职责。

以下是一个完整的测试案例,展示了这些组件的协同工作方式:

@Test
public void givenAnyUri_GetEmployeeFromSameRegistry() throws Exception {
    EmbeddedApp.fromHandlers(chainAction)
      .test(testHttpClient -> {
          assertEquals("Name of employee with ID 1 is NY", testHttpClient.get("employee/1/name")
            .getBody()
            .getText());
          assertEquals("Title of employee with ID 1 is Mr", testHttpClient.get("employee/1/title")
            .getBody()
            .getText());
      });
}

这段代码使用了 Ratpack 提供的测试工具,在不启动真实服务器的情况下验证 Handler 行为。

4. 使用 Ratpack 发起 HTTP 请求

4.1. 异步才是王道

传统的 HTTP 协议是同步的,这意味着大多数 Web 应用也是同步且阻塞的 —— 每个请求都需要占用一个线程。这种方式资源消耗巨大。

✅ 而我们更倾向于构建非阻塞、异步的应用程序,这样可以使用少量线程池高效处理大量并发请求。

4.2. 回调函数带来的问题

在异步编程中,我们常通过回调函数来处理返回的数据。Java 中常用匿名内部类或 Lambda 表达式实现。

⚠️ 但随着应用复杂度上升,特别是出现多层嵌套异步调用时,这种写法会变得难以维护且调试困难。

4.3. Ratpack Promise 解决方案

为了解决这个问题,Ratpack 提供了 Promise 机制,它类似于 Java 中的 Future,表示将来某个时刻可用的值。

我们可以通过链式调用来定义值就绪后的一系列操作,每一步都会返回一个新的 Promise:

public class EmployeeHandler implements Handler {
    @Override
    public void handle(Context ctx) throws Exception {
        EmployeeRepository repository = ctx.get(EmployeeRepository.class);
        Long id = Long.valueOf(ctx.getPathTokens().get("id"));
        Promise<Employee> employeePromise = repository.findEmployeeById(id);
        employeePromise.map(employee -> employee.getName())
          .then(name -> ctx.getResponse()
          .send(name));
    }
}

📌 注意:只有当我们明确知道如何处理最终值时,才调用 .then(Action) 来触发执行。

即使数据源是同步的,我们也依然可以使用 Promise 包装:

@Test
public void givenSyncDataSource_GetDataFromPromise() throws Exception {
    String value = ExecHarness.yieldSingle(execution -> Promise.sync(() -> "Foo"))
      .getValueOrThrow();
    assertEquals("Foo", value);
}

4.4. HTTP Client 使用详解

Ratpack 提供了一个异步 HTTP 客户端(HttpClient),可以从 Server Registry 获取默认实例。

⚠️ 但官方建议我们自行创建客户端实例,因为默认配置没有启用连接池,并且参数较为保守。

我们可以通过 HttpClient.of(Action<HttpClientSpec>) 方法来自定义配置:

HttpClient httpClient = HttpClient.of(httpClientSpec -> {
    httpClientSpec.poolSize(10)
      .connectTimeout(Duration.of(60, ChronoUnit.SECONDS))
      .maxContentLength(ServerConfig.DEFAULT_MAX_CONTENT_LENGTH)
      .responseMaxChunkSize(16384)
      .readTimeout(Duration.of(60, ChronoUnit.SECONDS))
      .byteBufAllocator(PooledByteBufAllocator.DEFAULT);
});

由于其异步特性,HttpClient 返回的是 Promise<ReceivedResponse> 类型,支持构建复杂的非阻塞处理流程。

比如下面这个 RedirectHandler 会调用远程接口并将结果转成大写返回:

public class RedirectHandler implements Handler {
 
    @Override
    public void handle(Context ctx) throws Exception {
        HttpClient client = ctx.get(HttpClient.class);
        URI uri = URI.create("http://localhost:5050/employee/1");
        Promise<ReceivedResponse> responsePromise = client.get(uri);
        responsePromise.map(response -> response.getBody()
          .getText()
          .toUpperCase())
          .then(responseText -> ctx.getResponse()
            .send(responseText));
    }
}

使用 cURL 验证一下效果 ✅:

curl http://localhost:5050/redirect
JANE DOE

5. 总结

本文介绍了 Ratpack 中用于构建非阻塞、异步 Web 应用的核心组件,包括 Handler、Chain、Registry、Context 和 Promise。

重点讲解了 HttpClient 的使用方法,并演示了如何结合 Promise 构建高效的异步调用链。同时展示了如何借助 TestHttpClient 编写轻量级测试。

📌 示例代码已上传至 GitHub 仓库,欢迎查阅学习。


原始标题:Ratpack HTTP Client