LoginSignup
0
0

More than 3 years have passed since last update.

LiferayのREST Builderを使用した独自ヘッドレスAPIの作成方法(パート3)

Posted at

※この記事はLiferayコミュニティブログに投稿された、Creating Headless APIs (Part 3)を翻訳したものです。

はじめに

このシリーズのパート1では、ヘッドレスAPIを生成するためにLiferayの新しいREST Builderツールを活用するプロジェクトを開始しました。Reusable Components (再利用可能なコンポーネント)セクションで、リクエストとレスポンスのオブジェクトの定義、すなわちビタミンコンポーネントとLiferayのCreatorコンポーネントのコピーを定義しました。

シリーズのパート2では、Paths(エンドポイント)を定義することでOpenAPI Yamlファイルを完成させ、一般的な問題に遭遇しつつも無事コード生成に成功しました。

このパートでは、生成されたコードの適切な場所に実装コードを追加する方法を見ていきます。

生成されたコードに目を通す

コードが生成されたモジュールは、headless-vitamin-apiheadless-vitamin-clientheadless-vitamin-implheadless-vitamin-testの4つです。

REST Builderはコードを生成しますが、build.gradlebnd.bndのいずれも変更しません。依存関係を追加してパッケージをエクスポートするかどうかはあなた次第です。以降のセクションでは私が使用した設定を共有しますが、実装に必要なセットは都度調整が必要です。

各モジュールを個別に見てみましょう。

headless-vitamins-api

APIモジュールの概念はService Builder APIモジュールに似ており、リソース(サービス)のインターフェイスが含まれています。また、コンポーネントタイプ(VitaminおよびCreator)の具体的なPOJOクラスも含まれています。それらは単なるPOJOだけではなく、コンポーネントタイプクラスには、オブジェクトをデシリアライズするときにフレームワークによって呼び出される追加のセッターがあります。 Creatorコンポーネントタイプの1つを見てみましょう。

@JsonIgnore
public void setAdditionalName(
    UnsafeSupplier additionalNameUnsafeSupplier) {

    try {
        additionalName = additionalNameUnsafeSupplier.get();
    }
    catch (RuntimeException re) {
        throw re;
    }
    catch (Exception e) {
        throw new RuntimeException(e);
    }
}

生成された上記コードはとても単純なものですが、心配はいりません。

VitaminResourceはリソース(サービス)のインターフェイスで、OpenAPI Yamlファイルで定義されたパスから取得されます。REST Builderを呼び出したあと、yamlファイルのoperationIdの各パスに新しい属性が追加され、これらの値がインターフェイスのメソッドと正確に一致することに気付くかもしれません。

生成されたコードだけではメソッドが少なすぎるため、ここでインターフェイスを共有します。

@Generated("")
public interface VitaminResource {

    public Page getVitaminsPage(
            String search, Filter filter, Pagination pagination, Sort[] sorts)
        throws Exception;

    public Vitamin postVitamin(Vitamin vitamin) throws Exception;

    public void deleteVitamin(String vitaminId) throws Exception;

    public Vitamin getVitamin(String vitaminId) throws Exception;

    public Vitamin patchVitamin(String vitaminId, Vitamin vitamin)
        throws Exception;

    public Vitamin putVitamin(String vitaminId, Vitamin vitamin)
        throws Exception;

    public void setContextCompany(Company contextCompany);

}

ビタミンオブジェクトの配列を返すパスである/vitaminsが、最初のメソッドであるgetVitaminsPage()です。独自のYamlファイルはPageVitaminコンポーネントを宣言しませんが、エクスポートされたYamlファイルには1つ挿入されます。

リソースインターフェースの他のメソッドは、Yamlファイルで定義されている他のパスと一致します。次に、APIモジュールのbuild.gradleファイルにいくつかの依存関係を追加する必要がありました:

dependencies {
    compileOnly group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: "2.9.9"
    compileOnly group: "com.liferay", name: "com.liferay.petra.function"
    compileOnly group: "com.liferay", name: "com.liferay.petra.string"
    compileOnly group: "com.liferay", name: "com.liferay.portal.vulcan.api"
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel"
    compileOnly group: "io.swagger.core.v3", name: "swagger-annotations", version: "2.0.5"
    compileOnly group: "javax.servlet", name: "javax.servlet-api"
    compileOnly group: "javax.validation", name: "validation-api", version: "2.0.1.Final"
    compileOnly group: "javax.ws.rs", name: "javax.ws.rs-api"
    compileOnly group: "org.osgi", name: "org.osgi.annotation.versioning"
}

コンポーネントとリソースインターフェイスを公開するために、bnd.bndファイルにもわずかな変更を加えました:

Export-Package: com.dnebinger.headless.vitamins.dto.v1_0, \
    com.dnebinger.headless.vitamins.resource.v1_0

headless-vitamins-client

このモジュールのコードは、ヘッドレスAPIを呼び出すためのJavaベースのクライアントを構築します。

クライアントエントリポイントは、<パッケージの接頭辞>.client.resource.v1_0<Component>Resourceクラスにあります。私のケースでは、com.dnebinger.headless.vitamins.client.resource.v1_0.VitaminResourceクラスが該当します。

各パスには静的メソッドがあり、各メソッドは同じ引数を取り、同じオブジェクトを返します。裏では各メソッドはHttpInvokerインスタンスを使用して、test@liferay.comとテスト用ログイン情報を使用してlocalhost:8080のWebサービスを呼び出します。リモートサービスをテストする場合や異なるログイン情報を使用する場合は、<Component>Resourceクラスを適宜編集して、異なる値を使用する必要があります。

クライアントコードを呼び出すための主要クラスやその他のコードを作成するかどうかは設計者次第ですが、テスト用の完全なクライアントライブラリを用意することは素晴らしい第一歩です!

注:生成されたheadless-vitamins-testモジュールは、サービス層のテストに際しheadless-vitamins-clientモジュールに依存します。

headless-vitamins-clientモジュールには外部依存関係はありませんが、bnd.bndファイルのパッケージをエクスポートする必要があります。

Export-Package: com.dnebinger.headless.vitamins.client.dto.v1_0, \
    com.dnebinger.headless.vitamins.client.resource.v1_0

headless-vitamins-test

headless-vitamins-implモジュールをスキップして、headless-vitamins-testについて簡単に説明します。

ここで生成されたコードは、サービスモジュールのすべての統合テストを提供し、クライアントモジュールを利用して、リモートAPIを呼び出します。
このモジュールでは、Base<Component>ResourceTestCase<Component>ResourceTestCaseの2つのクラスを取得するため、BaseVitaminResourceTestCaseVitaminResourceTestがあります。

VitaminResourceTestクラスは、Baseクラスがまだ実装していないテストを追加する場所です。他のモジュールを活用するための大規模なテストであり、重複した主キーの追加や存在しないオブジェクトを削除しようとしたときのエラー検証に利用されます。基本的に、素のリソースメソッドの単純な呼び出しでは個別にカバーできないテストがこれに該当します。

このモジュールのbuild.gradleファイルには、多くの追加が必要でした:


dependencies {
    testIntegrationCompile group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: "2.9.9"
    testIntegrationCompile group: "com.fasterxml.jackson.core", name: "jackson-core", version: "2.9.9"
    testIntegrationCompile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.9.1"
    testIntegrationCompile group: "com.liferay", name: "com.liferay.arquillian.extension.junit.bridge", version: "1.0.19"
    testIntegrationCompile group: "com.liferay.portal", name: "com.liferay.portal.kernel"
    testIntegrationCompile project(":modules:headless-vitamins:headless-vitamins-api")
    testIntegrationCompile project(":modules:headless-vitamins:headless-vitamins-client")
    testIntegrationCompile group: "com.liferay", name: "com.liferay.portal.odata.api"
    testIntegrationCompile group: "com.liferay", name: "com.liferay.portal.vulcan.api"
    testIntegrationCompile group: "com.liferay", name: "com.liferay.petra.function"
    testIntegrationCompile group: "com.liferay", name: "com.liferay.petra.string"
    testIntegrationCompile group: "javax.validation", name: "validation-api", version: "2.0.1.Final"
    testIntegrationCompile group: "commons-beanutils", name: "commons-beanutils"
    testIntegrationCompile group: "commons-lang", name: "commons-lang"
    testIntegrationCompile group: "javax.ws.rs", name: "javax.ws.rs-api"
    testIntegrationCompile group: "junit", name: "junit"
    testIntegrationCompile group: "com.liferay.portal", name: "com.liferay.portal.test"
    testIntegrationCompile group: "com.liferay.portal", name: "com.liferay.portal.test.integration"
}

これら依存関係の一部は、クラス(junitおよびliferayテストモジュール)にのみ必要なデフォルトであり、他の依存関係はプロジェクト(クライアントモジュールとapiモジュール、場合により他のモジュール)に依存します。要件を満たすリストを取得するために、いくらかの試行錯誤が必要になるかもしれません。

このモジュールのbnd.bndファイルはクラスまたはパッケージをエクスポートしないため、変更の必要はありませんでした。

headless-vitamins-impl

ようやく面白くなってきました。これは、実装コードが格納されているモジュールです。 REST Builderは、たくさんのスターターコードを生成してくれました。どんなものか見てみましょう。

com.dnebinger.headless.vitamins.internal.graphql、GraphQLの登場です!ヘッドレス実装には、定義したパスに基づきクエリとミューテーションを公開するGraphQLのエンドポイントが含まれます。 GraphQLは、この種の混合でよく見られるREST実装への呼び出しを単にプロキシするのではなく、<Component>Resourceを直接呼び出して、クエリとミューテーションの変更を処理することに注意してください。したがって、REST Builderを使用するだけでGraphQLも自動的に取得できるのです。

com.dnebinger.headless.vitamins.internal.jaxrs.application、これはJAX-RS Applicationクラスが格納されている場所です。特段面白いものが含まれているわけではありませんが、LiferayのOSGiコンテナへアプリケーションを登録します。

com.dnebinger.headless.vitamins.internal.resource.v1_0、これは、コード修正を施す場所です。

OpenAPIResourceImpl.javaクラスは、例えばSwagger HubにロードするOpenAPI yamlファイルを返すためのパスです。各<Component>Resourceインターフェースごとに、抽象基本クラスBase<Component>ResourceImplと、作業を行うためのコンクリートクラス<Component>ResourceImplを取得します。ゆえに、BaseVitaminResourceImplVitaminResourceImplの2つのクラスがあります。

基本クラスのメソッドを見てみると、SwaggerとJAX-RSのアノテーションで多量に装飾されていることがわかります。 /vitaminsに格納されているVitaminコンポーネントの配列を返すのに使用される、getVitaminsPage()メソッドの1つを見てみましょう:

@Override
@GET
@Operation(
  description = "Retrieves the list of vitamins and minerals. Results can be paginated, filtered, searched, and sorted."
)
@Parameters(
  value = {
    @Parameter(in = ParameterIn.QUERY, name = "search"),
    @Parameter(in = ParameterIn.QUERY, name = "filter"),
    @Parameter(in = ParameterIn.QUERY, name = "page"),
    @Parameter(in = ParameterIn.QUERY, name = "pageSize"),
    @Parameter(in = ParameterIn.QUERY, name = "sort")
  }
)
@Path("/vitamins")
@Produces({"application/json", "application/xml"})
@Tags(value = {@Tag(name = "Vitamin")})
public Page<Vitamin> getVitaminsPage(
    @Parameter(hidden = true) @QueryParam("search") String search,
    @Context Filter filter, @Context Pagination pagination,
    @Context Sort[] sorts)
  throws Exception {

  return Page.of(Collections.emptyList());
}

どうでしょう?

これはREST Builderが私たちにもたらす利点の1つです。すべてのアノテーションは基本クラスで定義されているため、それらについて心配する必要はないのです。

Page.of(Collections.emptyList())を渡しているreturnステートメントを見てみましょう。これが基本クラスが提供するスタブメソッドです。価値のある実装を提供するわけではありませんが、実装しない場合に確実に値が返されるようにします。

このメソッドを実装する準備ができたら、VitaminResourceImplクラス(現在は空)に次のメソッドを追加します:

@Override
public Page<Vitamin> getVitaminsPage(String search, Filter filter, Pagination pagination, Sort[] sorts) throws Exception {
  List<Vitamin> vitamins = new ArrayList<Vitamin>();
  long totalVitaminsCount = ...;

  // write code here, should add to the list of Vitamin objects

  return Page.of(vitamins, Pagination.of(0, pagination.getPageSize()), totalVitaminsCount);
}

アノテーションが一切無いことに注目してください。

先述したとおり、全アノテーションはオーバーライドしているメソッドに含まれているため、すべての構成の準備が整っているのです!そのため、Service Builderで生成されたコードとは異なり、「このファイルは生成されていますが、このファイルを変更しないでください」という旨のコメントはどこにも表示されません。 REST Builderを再度実行すると(再)生成されるすべてのクラスに@Generated("")アノテーションが表示されます。

Base<Component>ResourceImplクラスには、このようにアノテーションされています。これは、REST Builderを実行するたびに再生成されるファイルです。したがって、このファイルのアノテーションやメソッドの実装に手を加えないでください。すべての変更は<Component>ResourceImplクラスに対して行ってください。

アノテーションを変更する必要がある場合(推奨しません)、<Component>ResourceImplクラスでこれを行うことができ、基本クラスからのアノテーションをオーバーライドする必要があります。したがって、build.gradleファイルにはいくつかの依存関係を追加する必要があります。私のファイルは次のようになりました:

buildscript {
    dependencies {
        classpath group: "com.liferay", name: "com.liferay.gradle.plugins.rest.builder", version: "1.0.21"
    }

    repositories {
        maven {
            url "https://repository-cdn.liferay.com/nexus/content/groups/public"
        }
    }
}

apply plugin: "com.liferay.portal.tools.rest.builder"

dependencies {
    compileOnly group: "com.fasterxml.jackson.core", name: "jackson-annotations", version: "2.9.9"
    compileOnly group: "com.liferay", name: "com.liferay.adaptive.media.api"
    compileOnly group: "com.liferay", name: "com.liferay.adaptive.media.image.api"
    compileOnly group: "com.liferay", name: "com.liferay.headless.common.spi"
    compileOnly group: "com.liferay", name: "com.liferay.headless.delivery.api"
    compileOnly group: "com.liferay", name: "com.liferay.osgi.service.tracker.collections"
    compileOnly group: "com.liferay", name: "com.liferay.petra.function"
    compileOnly group: "com.liferay", name: "com.liferay.petra.string"
    compileOnly group: "com.liferay", name: "com.liferay.portal.odata.api"
    compileOnly group: "com.liferay", name: "com.liferay.portal.vulcan.api"
    compileOnly group: "com.liferay", name: "com.liferay.segments.api"
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.impl"
    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel"
    compileOnly group: "io.swagger.core.v3", name: "swagger-annotations", version: "2.0.5"
    compileOnly group: "javax.portlet", name: "portlet-api"
    compileOnly group: "javax.servlet", name: "javax.servlet-api"
    compileOnly group: "javax.validation", name: "validation-api", version: "2.0.1.Final"
    compileOnly group: "javax.ws.rs", name: "javax.ws.rs-api"
    compileOnly group: "org.osgi", name: "org.osgi.service.component", version: "1.3.0"
    compileOnly group: "org.osgi", name: "org.osgi.service.component.annotations"
    compileOnly group: "org.osgi", name: "org.osgi.core"
    compileOnly project(":modules:headless-vitamins:headless-vitamins-api")
}

パッケージはすべて内部にあるため、bnd.bndファイルには何も加える必要はありません。

まとめ

実装の構築を開始できる段階まで進みました!切りよく今回はここまでとします。

パート1では、プロジェクトを作成し、再利用可能なコンポーネントを定義してOpenAPI Yamlに触れました。
パート2では、OpenAPIサービスのすべてのパス定義を追加し、REST Builderを使用してコードを生成しました。
パート3(本記事)では、生成されたすべてのコードを確認し、コードの変更箇所や、実装コードのアノテーションについて心配する必要がないことを理解しました。

いよいよ最後となる次のパートでは、データストレージ用のプロジェクトにService Builderモジュールを追加し、すべてのリソースメソッドを実装してServiceBuilderコードを利用します。

それではまた!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0