1. 序章

過去数年にわたって、Javaでアプリケーションを作成する機能的で反応的な方法の台頭を目の当たりにしてきました。 Ratpackは、同じ方針でHTTPアプリケーションを作成する方法を提供します。

ネットワークのニーズにNettyを使用しているため、は完全に非同期で非ブロッキングです。 Ratpackは、コンパニオンテストライブラリを提供することにより、テストのサポートも提供します。

このチュートリアルでは、RatpackHTTPクライアントと関連コンポーネントの使用について説明します。

そうすることで、入門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));
};

このハンドラーは、いくつかの前処理を実行し、結果をR egistry に入れてから、他のハンドラーに要求を委任する役割を果たします。

レジストリを使用することで、ハンドラ間通信を実現できます。 次のhandlerは、オブジェクトタイプを使用して、Registryから以前に計算された結果をクエリします。

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

Chaininsert(..)メソッドを使用して複数のチェーンを一緒に構成し、それぞれに異なる懸念事項を持たせることで、このアプローチをさらに進めることができます。

次のテストケースは、これらの構成の使用法を示しています。

@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. Ratpackを使用したHTTP

4.1. 非同期に向けての作業

HTTPプロトコルは本質的に同期です。 その結果、多くの場合、Webアプリケーションは同期しているため、ブロックされます。 着信リクエストごとにスレッドを作成するため、これは非常にリソースを消費するアプローチです。

むしろ、ノンブロッキングで非同期のアプリケーションを作成したいと思います。 これにより、リクエストを処理するためにスレッドの小さなプールを使用するだけで済みます。

4.2. コールバック関数

非同期APIを処理する場合、通常、データを呼び出し元に返すことができるように、レシーバーにコールバック関数を提供します。 Javaでは、これは通常、匿名内部クラスとラムダ式の形式を取ります。 しかし、アプリケーションが拡張するにつれて、またはネストされた非同期呼び出しが複数あるため、このようなソリューションは保守が難しく、デバッグが困難になります。

Ratpackは、Promiseの形式でこの複雑さを処理するためのエレガントなソリューションを提供します。

4.3. ラットパックの約束

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

の約束は、最終的な値をどう処理するかを定義するときに特に役立つことを覚えておく必要があります。 これを行うには、端末操作を呼び出してから(アクション)を呼び出します。

約束を返送する必要があるが、データソースが同期している場合でも、それを行うことができます。

@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(アクション) パラメータとしてとるメソッドアクションタイプの 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);
});

非同期の性質から推測できるように、HttpClientPromiseオブジェクトを返します。 その結果、ブロックしない方法で操作の複雑なパイプラインを持つことができます。

説明のために、この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. 結論

この記事では、非ブロッキングおよび非同期Webアプリケーションの開発を可能にするRatpackで利用可能なプライマリライブラリ構造について説明しました。

Ratpack HttpClientとそれに付随するPromiseクラスを見てみました。これは、Ratpackのすべての非同期を表します。 また、付属のTestHttpClientを使用してHTTPアプリケーションを簡単にテストする方法も確認しました。

また、いつものように、このチュートリアルのコードスニペットは、GitHubリポジトリで入手できます。