※この記事はLiferayコミュニティブログに投稿された、_Creating Headless APIs (Part 3)_を翻訳したものです。
はじめに
このシリーズのパート1では、ヘッドレスAPIを生成するためにLiferayの新しいREST Builderツールを活用するプロジェクトを開始しました。Reusable Components (再利用可能なコンポーネント)セクションで、リクエストとレスポンスのオブジェクトの定義、すなわちビタミンコンポーネントとLiferayのCreatorコンポーネントのコピーを定義しました。
シリーズのパート2では、Paths(エンドポイント)を定義することでOpenAPI Yamlファイルを完成させ、一般的な問題に遭遇しつつも無事コード生成に成功しました。
このパートでは、生成されたコードの適切な場所に実装コードを追加する方法を見ていきます。
生成されたコードに目を通す
コードが生成されたモジュールは、headless-vitamin-api
、headless-vitamin-client
、headless-vitamin-impl
、headless-vitamin-test
の4つです。
REST Builderはコードを生成しますが、build.gradle
とbnd.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つのクラスを取得するため、BaseVitaminResourceTestCase
とVitaminResourceTest
があります。
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
を取得します。ゆえに、BaseVitaminResourceImpl
とVitaminResourceImpl
の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
コードを利用します。
それではまた!