Ratpack HTTP-клиент

Ratpack HTTP Client

1. Вступление

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

Поскольку он использует Netty для своих сетевых нужд,it’s completely asynchronous and non-blocking. Ratpack также поддерживает тестирование, предоставляя сопутствующую библиотеку тестов.

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

Поступая таким образом, мы постараемся уйти в наше понимание от точки, на которой мы остановились в концеintroductory Ratpack tutorial.

2. Maven Зависимости

Для начала добавим требуемыеRatpack dependencies:


    io.ratpack
    ratpack-core
    1.5.4


    io.ratpack
    ratpack-test
    1.5.4
    test

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

Однако мы всегда можем добавить и расширить, используя другие библиотеки 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. Цепочка, реестр и контекст

Handlers interact with the incoming request using a Context object. Через него мы получаем доступ к 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 и затем делегирует запрос другим обработчикам.

Through the use of the Registry, we can achieve inter-handler communication. Следующийhandler запрашивает ранее вычисленный результат изRegistry, используя тип объекта:

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

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

Now we can use these handlers inside a Chain in order to create complex custom request processing pipelines.

Например:

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

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

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

@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. Функции обратного вызова

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. В Java это обычно принимает форму анонимных внутренних классов и лямбда-выражений. Но по мере масштабирования нашего приложения или наличия множества вложенных асинхронных вызовов такое решение будет сложно поддерживать и труднее отлаживать.

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

4.3. Обещания Ratpack

RatpackPromise можно рассматривать как аналог объекта JavaFuture. It’s essentially a representation of a value which will become available later.

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

Как и следовало ожидать, это приводит к небольшому количеству переключений контекста между потоками и делает наше приложение эффективным.

Ниже приведена реализация обработчика, в которой используется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 employeePromise = repository.findEmployeeById(id);
        employeePromise.map(employee -> employee.getName())
          .then(name -> ctx.getResponse()
          .send(name));
    }
}

Следует иметь в виду, что apromise is especially useful when we define what to do with the eventual value. Мы можем сделать это, вызвав терминальную операцию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-клиент, экземпляр которого можно получить из реестра сервера. Однакоwe’re encouraged to create and use alternative instances as the default one doesn’t use connection pooling and has quite conservative defaults.

Мы можем создать экземпляр, используя методof(Action), который принимает в качестве параметра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 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, которые позволяют нам разрабатывать неблокирующие и асинхронные веб-приложения.

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

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