1. Introduction
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.
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.
In this tutorial, we’ll go over the use of the Ratpack HTTP client and related components.
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.
2. Maven Dependencies
To get started, let’s add the required Ratpack dependencies:
<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.
3. Background
Before we dive in, let’s get our head around the way things are done in Ratpack applications.
3.1. Handler-Based Approach
Ratpack uses a handler-based approach for request processing. The idea in itself is simple enough.
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
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.
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.
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:
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.
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.
4. HTTP With Ratpack
4.1. Working Towards Asynchrony
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.
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
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.
Ratpack provides an elegant solution to handle this complexity in the form of Promises.
4.3. Ratpack Promises
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.
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:
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.
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
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.
We can create an instance using the of(Action) method which takes as parameter an Action of type 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.
For illustration, let’s have a client call our EmployeeHandler using this HttpClient:
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 http://localhost:5050/redirect
JANE DOE
5. Conclusion
In this article, we went over the primary library constructs available in Ratpack which enable us to develop non-blocking and asynchronous web applications.
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.
And, as always, the code snippets from this tutorial are available in our GitHub repository.