Ratpack HTTP客户端

2025/04/08

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. 背景

在深入研究之前,让我们先了解一下Ratpack应用程序中的完成方式。

3.1 基于处理程序的方法

Ratpack使用基于处理程序的方法来处理请求,这个想法本身很简单。

最简单的形式是,我们可以让每个处理程序在每个特定路径上处理请求:

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

3.2 链、注册表和上下文

处理程序使用Context对象与传入请求进行交互,通过它,我们可以访问HTTP请求和响应,以及委托给其他处理程序的功能。

以下面的处理程序为例:

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));
};

该处理程序负责进行一些预处理,将结果放入Registry,然后将请求委托给其他处理程序。

通过使用Registry,我们可以实现处理程序之间的通信。以下处理程序使用对象类型从Registry查询先前计算的结果:

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

我们应该记住,在生产应用程序中,我们会将这些处理程序作为单独的类,以便更好地抽象、调试和开发复杂的业务逻辑。

现在我们可以在Chain中使用这些处理程序来创建复杂的自定义请求处理管道

例如:

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

我们可以进一步采用这种方法,通过使用Chain中的insert(…)方法将多个链组合在一起,并让每个链负责不同的关注点。

以下测试用例展示了这些结构的使用:

@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的测试库来单独测试我们的功能,而无需启动实际的服务器。

4. HTTP与Ratpack

4.1 努力实现异步

HTTP协议本质上是同步的;因此,Web应用程序通常是同步的,导致会阻塞。这是一种极其耗费资源的方法,因为我们会为每个传入请求创建一个线程。

我们宁愿创建非阻塞和异步应用程序,这将确保我们只需要使用一小部分线程来处理请求。

4.2 回调函数

处理异步API时,我们通常会向接收方提供回调函数,以便将数据返回给调用方。在Java中,这通常采用匿名内部类和Lambda表达式的形式。但是随着我们的应用程序扩展,或者有多个嵌套的异步调用,这样的解决方案将难以维护且更难调试。

Ratpack以Promise的形式提供了一种优雅的解决方案来处理这种复杂性。

4.3 Ratpack Promise

Ratpack Promise可以被视为类似于Java Future对象,它本质上是稍后可用的值的表示

我们可以指定值可用时要经过的一系列操作,每个操作都会返回一个新的Promise对象,即前一个Promise对象的转换版本。

正如我们所料,这会导致线程之间的上下文切换很少,从而使我们的应用程序更加高效。

以下是使用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));
    }
}

我们需要记住,当我们定义如何处理最终值时,Promise特别有用,我们可以通过在其上调用终端操作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客户端

Ratpack提供了一个异步HTTP客户端,可以从服务器注册表中检索该客户端的实例。但是,我们鼓励创建和使用替代实例,因为默认实例不使用连接池,并且默认值相当保守

我们可以使用of(Action)方法创建一个实例,该方法以HttpClientSpec类型的Action作为参数。

利用这一点,我们可以根据自己的偏好调整客户端:

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对象。因此,我们可以以非阻塞方式拥有复杂的操作管道。

为了说明,让我们让客户端使用这个HttpClient调用我们的EmployeeHandler:

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应用程序。

我们了解了Ratpack HttpClient和随附的Promise类,该类代表Ratpack中的所有异步操作;我们还了解了如何使用随附的TestHttpClient轻松测试我们的HTTP应用程序。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章