Перейти к основному содержимому

HTTP-клиент Ratpack

· 6 мин. чтения

1. Введение

За последние несколько лет мы стали свидетелями появления функционального и реактивного способов создания приложений на Java. Ratpack предлагает способ создания HTTP-приложений в том же духе.

Поскольку он использует Netty для своих сетевых нужд, он полностью асинхронен и неблокирует . Ratpack также обеспечивает поддержку тестирования, предоставляя сопутствующую тестовую библиотеку.

В этом руководстве мы рассмотрим использование HTTP-клиента Ratpack и связанных с ним компонентов.

И при этом мы попытаемся углубить наше понимание с того места, где мы остановились в конце нашего вводного руководства 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));
};

Этот обработчик выполняет некоторую предварительную обработку, помещая результат в реестр и затем делегируя запрос другим обработчикам.

Благодаря использованию реестра мы можем добиться взаимодействия между обработчиками . Следующий обработчик запрашивает ранее вычисленный результат из реестра , используя тип объекта:

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

Мы должны помнить, что в рабочем приложении эти обработчики будут отдельными классами для лучшей абстракции, отладки и разработки сложной бизнес-логики.

Теперь мы можем использовать эти обработчики внутри цепочки для создания сложных настраиваемых конвейеров обработки запросов .

Например:

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

Мы можем развить этот подход дальше, составив несколько цепочек вместе, используя метод вставки (..) в цепочке , и сделать каждую из них ответственной за разные задачи.

Следующий тестовый пример демонстрирует использование этих конструкций:

@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 является синхронным по своей природе. Следовательно, чаще всего веб-приложения являются синхронными и, следовательно, блокирующими. Это чрезвычайно ресурсоемкий подход, так как мы создаем поток для каждого входящего запроса.

Мы бы предпочли создавать неблокирующие и асинхронные приложения. Это гарантирует, что нам нужно будет использовать только небольшой пул потоков для обработки запросов.

4.2. Функции обратного вызова

При работе с асинхронными API мы обычно предоставляем функцию обратного вызова получателю, чтобы данные могли быть возвращены вызывающей стороне. В Java это обычно принимает форму анонимных внутренних классов и лямбда-выражений. Но по мере масштабирования нашего приложения или наличия нескольких вложенных асинхронных вызовов такое решение будет сложно поддерживать и труднее отлаживать.

Ratpack предлагает элегантное решение для решения этой сложности в виде Promise s.

4.3. Обещания крысиной стаи

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) .

Если нам нужно отправить обещание, но источник данных является синхронным, мы все равно сможем это сделать:

@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.

Используя это, мы можем настроить нашего клиента в соответствии с нашими предпочтениями:

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 . В результате мы можем иметь сложный конвейер операций неблокирующим образом.

Для иллюстрации предположим, что клиент вызывает наш EmployeeHandler , используя этот 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));
}
}

Быстрый вызов cURL подтвердит, что мы получили ожидаемый ответ:

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

5. Вывод

В этой статье мы рассмотрели основные конструкции библиотек, доступные в Ratpack, которые позволяют нам разрабатывать неблокирующие и асинхронные веб-приложения.

Мы взглянули на Ratpack HttpClient и сопутствующий класс Promise , который представляет все асинхронные вещи в Ratpack. Мы также увидели, как легко можно протестировать наше HTTP-приложение с помощью прилагаемого TestHttpClient .

И, как всегда, фрагменты кода из этого туториала доступны в нашем репозитории GitHub .