1. Introduction
1.介绍
Over the last several years, we’ve witnessed the rise of the functional and reactive way of creating applications in Java. Ratpack offers a way of creating HTTP applications along the same lines.
在过去的几年里,我们见证了在Java中创建应用程序的功能性和反应性方式的崛起。Ratpack提供了一种按照同样思路创建HTTP应用程序的方法。
Since it uses Netty for its networking needs, it’s completely asynchronous and non-blocking. Ratpack also provides support for testing by providing a companion test library.
由于它使用Netty来满足其网络需求,它是完全异步的、非阻塞的。Ratpack还通过提供一个配套的测试库为测试提供支持。
In this tutorial, we’ll go over the use of the Ratpack HTTP client and related components.
在本教程中,我们将介绍Ratpack HTTP客户端和相关组件的使用。
And in doing so, we’ll try to take our understanding further from the point where we left at the end of our introductory Ratpack tutorial.
在这样做的过程中,我们将尝试从我们在Ratpack入门教程结束时离开的地方进一步理解。
2. Maven Dependencies
2.Maven的依赖性
To get started, let’s add the required Ratpack dependencies:
为了开始工作,让我们添加所需的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>
Interestingly, we only need this much to create and test our application.
有趣的是,我们只需要这么多来创建和测试我们的应用程序。
However, we can always choose to add and extend using other Ratpack libraries.
然而,我们总是可以选择使用其他Ratpack库来添加和扩展。
3. Background
3.背景
Before we dive in, let’s get our head around the way things are done in Ratpack applications.
在我们深入研究之前,让我们先了解一下Ratpack应用程序中的工作方式。
3.1. Handler-Based Approach
3.1.基于处理程序的方法
Ratpack uses a handler-based approach for request processing. The idea in itself is simple enough.
Ratpack使用一种基于处理程序的方法来处理请求。这个想法本身就很简单。
And in its simplest form, we could have each handler servicing requests on each specific path:
在其最简单的形式中,我们可以让每个处理程序为每个特定路径上的请求提供服务。
public class FooHandler implements Handler {
@Override
public void handle(Context ctx) throws Exception {
ctx.getResponse().send("Hello Foo!");
}
}
3.2. Chain, Registry, and Context
3.2.链、注册表和背景
Handlers interact with the incoming request using a Context object. Through it, we get access to the HTTP request and response, and capabilities to delegate to other handlers.
处理程序使用Context对象与传入的请求进行交互。通过它,我们可以获得对HTTP请求和响应的访问,以及委托给其他处理程序的能力。
Take for example the following handler:
以下面这个处理程序为例。
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));
};
This handler is responsible for doing some pre-processing, putting the result in the Registry and then delegating the request to the other handlers.
这个处理程序负责做一些预处理,将结果放入Registry,然后将请求委托给其他处理程序。
Through the use of the Registry, we can achieve inter-handler communication. The following handler queries the previously computed result from Registry using the object type:
通过使用Registry,我们可以实现处理程序间的通信。下面的handler使用对象类型从Registry查询先前计算的结果。
Handler empNameHandler = ctx -> {
Employee employee = ctx.get(Employee.class);
ctx.getResponse()
.send("Name of employee with ID " + employee.getId() + " is " + employee.getName());
};
We should keep in mind that in a production application, we’d have these handlers as separate classes for better abstraction, debugging and development of elaborate business logic.
我们应该记住,在生产应用中,我们会把这些处理程序作为独立的类,以便更好地进行抽象、调试和开发复杂的业务逻辑。
Now we can use these handlers inside a Chain in order to create complex custom request processing pipelines.
现在我们可以在链内使用这些处理程序,以创建复杂的定制请求处理管道。
For instance:
比如说。
Action<Chain> chainAction = chain -> chain.prefix("employee/:id", empChain -> {
empChain.all(allHandler)
.get("name", empNameHandler)
.get("title", empTitleHandler);
});
We can take this approach further by composing multiple chains together using the insert(..) method in Chain and make each responsible for a different concern.
我们可以通过使用Chain中的insert(..)方法将多个链组合在一起,使每个链负责不同的关注点,从而进一步采用这种方法。
The following test case showcases the use of these constructs:
下面的测试案例展示了这些结构的使用。
@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());
});
}
Here, we’re using Ratpack’s testing library to test our functionality in isolation and without starting an actual server.
在这里,我们使用Ratpack的测试库来隔离测试我们的功能,而不启动一个实际的服务器。
4. HTTP With Ratpack
4.HTTP与Ratpack
4.1. Working Towards Asynchrony
4.1.努力实现异步化
The HTTP protocol is synchronous in nature. Consequently, more often than not, web applications are synchronous and therefore, blocking. This is an extremely resource-intensive approach since we create a thread for each incoming request.
HTTP协议在本质上是同步的。因此,更多的时候,网络应用是同步的,因此是阻塞的。这是一种极其耗费资源的方法,因为我们为每个传入的请求创建一个线程。
We’d rather create non-blocking and asynchronous applications. This would ensure that we only need to use a small pool of threads to handle requests.
我们宁愿创建非阻塞和异步的应用程序。这将确保我们只需要使用一个小的线程池来处理请求。
4.2. Callback Functions
4.2.回调函数
When dealing with asynchronous API’s, we usually provide a callback function to the receiver so that the data can be returned to the caller. In Java, this typically takes the form of anonymous inner classes and lambda expressions. But as our application scales, or as there are multiple nested asynchronous calls, such a solution would be difficult to maintain and harder to debug.
在处理异步 API 时,我们通常会向接收器提供一个回调函数,以便将数据返回给调用者。在 Java 中,这通常采用匿名内部类和 lambda 表达式的形式。但随着我们的应用程序的扩展,或者有多个嵌套的异步调用,这样的解决方案将很难维护,也更难调试。
Ratpack provides an elegant solution to handle this complexity in the form of Promises.
Ratpack提供了一个优雅的解决方案,以Promises的形式处理这种复杂性。
4.3. Ratpack Promises
4.3.鼠辈的承诺
A Ratpack Promise could be considered akin to a Java Future object. It’s essentially a representation of a value which will become available later.
Ratpack的Promise可被视为类似于Java的Future对象。它本质上是一个值的表示,这个值以后会变得可用。
We can specify a pipeline of operations that the value will go through as it becomes available. Each operation would return a new promise object, a transformed version of the previous promise object.
我们可以指定一个操作流水线,当该值变得可用时,它将经历这些操作。每个操作都会返回一个新的承诺对象,即前一个承诺对象的转换版本。
As we might expect, this leads to few context-switches between threads and makes our application efficient.
正如我们所期望的,这导致了线程之间很少的上下文切换,并使我们的应用程序变得高效。
Following is a handler implementation which makes use of 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));
}
}
We need to keep in mind that a promise is especially useful when we define what to do with the eventual value. We can do that by calling the terminal operation then(Action) on it.
我们需要记住,当我们定义如何处理最终的值时,承诺特别有用。我们可以通过对它调用终端操作then(Action) 来实现。
If we need to send back a promise but the data source is synchronous, we’d still be able to do that:
如果我们需要送回一个承诺,但数据源是同步的,我们仍然能够做到这一点。
@Test
public void givenSyncDataSource_GetDataFromPromise() throws Exception {
String value = ExecHarness.yieldSingle(execution -> Promise.sync(() -> "Foo"))
.getValueOrThrow();
assertEquals("Foo", value);
}
4.4. The HTTP Client
4.4.HTTP客户端
Ratpack provides an asynchronous HTTP client, an instance of which can be retrieved from the server registry. However, we’re encouraged to create and use alternative instances as the default one doesn’t use connection pooling and has quite conservative defaults.
Ratpack提供了一个异步的HTTP客户端,它的实例可以从服务器注册表中检索到。然而,我们被鼓励创建和使用其他的实例,因为默认的实例不使用连接池,而且有相当保守的默认值。
We can create an instance using the of(Action) method which takes as parameter an Action of type HttpClientSpec.
我们可以使用of(Action)方法创建一个实例,该方法以Action类型的HttpClientSpec>为参数。
Using this, we can tweak our client to our preferences:
利用这一点,我们可以根据自己的喜好调整我们的客户端。
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);
});
As we might have guessed by its asynchronous nature, HttpClient returns a Promise object. As a result, we can have a complex pipeline of operations in a non-blocking way.
正如我们可能已经从它的异步性中猜到的那样,HttpClient返回一个Promise对象。因此,我们可以以非阻塞的方式拥有一个复杂的操作管道。
For illustration, let’s have a client call our EmployeeHandler using this HttpClient:
为了说明问题,让我们有一个客户端使用这个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));
}
}
A quick cURL call would confirm that we got an expected response:
一个快速的cURL调用将确认我们得到了一个预期的响应。
curl http://localhost:5050/redirect
JANE DOE
5. Conclusion
5.结论
In this article, we went over the primary library constructs available in Ratpack which enable us to develop non-blocking and asynchronous web applications.
在这篇文章中,我们介绍了Ratpack中的主要库结构,它使我们能够开发非阻塞和异步的Web应用程序。
We took a look at the Ratpack HttpClient and the accompanying Promise class which represents all things asynchronous in Ratpack. We also saw how we could easily test our HTTP application using the accompanying TestHttpClient.
我们看了一下Ratpack的HttpClient和附带的Promise类,它代表了Ratpack中所有异步的东西。我们还看到了如何使用附带的TestHttpClient轻松测试我们的HTTP应用程序。
And, as always, the code snippets from this tutorial are available in our GitHub repository.
而且,像往常一样,本教程的代码片段可在我们的GitHub仓库中找到。