このプロジェクトは、Spring、特に Spring MVC を操作するときに HATEOAS [Wikipedia] (英語) 原則に従う REST 表現の作成を容易にするいくつかの API を提供します。解決しようとしている中心的な問題は、リンクの作成と表現の組み立てです。
© 2012-2021 The original authors.
このドキュメントのコピーは、あなた自身の使用および他者への配布のために作成することができますが、そのようなコピーに料金を請求しないこと、さらに、印刷物または電子的に配布されるかどうかにかかわらず、各コピーにこの著作権表示が含まれていることを条件とします。 |
1. 序文
1.1. Spring HATEOAS 1.0 への移行
1.0 では、0.x ブランチで行った設計とパッケージ構造の選択の一部を再評価する機会を得ました。これには信じられないほどの量のフィードバックがあり、メジャーバージョンの更新は、リファクタリングするのに最も自然な場所のように思われました。
1.1.1. 変更
パッケージ構造の最大の変更は、Spring HATEOAS で追加のメディア型をサポートするためのハイパーメディア型登録 API の導入によってもたらされました。これにより、クライアント API とサーバー API (それぞれに名前が付けられたパッケージ) が明確に分離され、パッケージ mediatype
でのメディア型の実装が行われました。
コードベースを新しい API にアップグレードする最も簡単な方法は、移行スクリプトを使用することです。それにジャンプする前に、変更点を一目で確認できます。
表現モデル
クラスの ResourceSupport
/Resource
/Resources
/PagedResources
グループは、実際には適切な名前が付けられていません。結局のところ、これらの型は実際にリソースを明示するのではなく、ハイパーメディア情報とアフォーダンスで強化できる表現モデルです。新しい名前が古い名前にどのようにマッピングされるかを次に示します。
ResourceSupport
はRepresentationModel
になりましたResource
はEntityModel
になりましたResources
はCollectionModel
になりましたPagedResources
はPagedModel
になりました
その結果、ResourceAssembler
は RepresentationModelAssembler
に名前が変更され、そのメソッド toResource(…)
と toResources(…)
はそれぞれ toModel(…)
と toCollectionModel(…)
に名前が変更されました。また、名前の変更は TypeReferences
に含まれるクラスにも反映されています。
RepresentationModel.getLinks()
は、さまざまな戦略を使用して異なるLinks
インスタンスを連結およびマージするための追加の API を公開するため、(List<Link>
を介して)Links
インスタンスを公開するようになりました。また、インスタンスにリンクを追加するメソッドがインスタンス自体を返すことができるように、自己バインドされたジェネリクス型に変わりました。LinkDiscoverer
API はclient
パッケージに移動されました。LinkBuilder
およびEntityLinks
API はserver
パッケージに移動されました。ControllerLinkBuilder
はserver.mvc
に移動され、推奨されなくなり、WebMvcLinkBuilder
に置き換えられました。RelProvider
はLinkRelationProvider
に名前が変更され、String
の代わりにLinkRelation
インスタンスを返します。VndError
はmediatype.vnderror
パッケージに移動されました。
1.1.2. 移行スクリプト
ソースコードリポジトリに移動した Spring HATEOAS 型へのすべてのインポートステートメントと静的メソッド参照を更新する、アプリケーションルートから実行するスクリプト [GitHub] (英語) を見つけることができます。それをダウンロードして、プロジェクトのルートから実行するだけです。デフォルトでは、すべての Java ソースファイルをインスペクションし、従来の Spring HATEOAS 型参照を新しいものに置き換えます。
$ ./migrate-to-1.0.sh
Migrating Spring HATEOAS references to 1.0 for files : *.java
Adapting ./src/main/java/…
…
Done!
スクリプトは必ずしもすべての変更を完全に修正できるわけではありませんが、最も重要なリファクタリングをカバーする必要があることに注意してください。
お気に入りの Git クライアントでファイルに加えられた変更を確認し、必要に応じてコミットします。メソッドまたは型の参照が移行されていない場合は、チケットを課題トラッカーに送信してください。
1.1.3. 1.0 M3 から 1.0 RC1 への移行
Link.andAffordance(…)
テイクアフォーダンスの詳細はAffordances
に移動されました。Affordance
インスタンスを手動で構築するには、Affordances.of(link).afford(…)
を使用します。また、流れるような使用箇所のためにAffordances
から公開された新しいAffordanceBuilder
型にも注意してください。詳細については、アフォーダンスを参照してください。AffordanceModelFactory.getAffordanceModel(…)
はResolvableType
の代わりにInputPayloadMetadata
およびPayloadMetadata
インスタンスを受け取るようになり、非型ベースの実装が可能になりました。カスタムメディア型の実装は、それに応じて調整する必要があります。HAL フォームは、値が仕様でデフォルトとして定義されているものに準拠している場合、プロパティ属性をレンダリングしなくなりました。つまり以前は
required
が明示的にfalse
に設定されていた場合、現在はrequired
のエントリを省略しています。また、HTTP メソッドとしてPATCH
を使用するテンプレートに対してのみ強制的に不要にするようになりました。
2. 基礎
このセクションでは、Spring HATEOAS の基本とその基本的なドメインの抽象化について説明します。
2.1. リンク
ハイパーメディアの基本的な考え方は、リソースの表現をハイパーメディア要素で豊かにすることです。その最も単純な形式はリンクです。それらは、クライアントが特定のリソースに移動できることを示します。関連リソースのセマンティクスは、いわゆるリンク関係で定義されます。HTML ファイルのヘッダーですでにこれを見たことがあるかもしれません:
<link href="theme.css" rel="stylesheet" type="text/css" />
ご覧のとおり、リンクはリソース theme.css
を指しており、それがスタイルシートであることを示しています。リンクには、リソースが指すメディア型などの追加情報が含まれていることがよくあります。ただし、リンクの基本的な構成要素は、その参照と関係です。
Spring HATEOAS を使用すると、不変の Link
値型を介してリンクを操作できます。そのコンストラクターはハイパーテキスト参照とリンク関係の両方を取り、後者はデフォルトで IANA リンク関係 self
に設定されています。後者についてはリンク関係を参照してください。
Link link = Link.of("/something");
assertThat(link.getHref()).isEqualTo("/something");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
link = Link.of("/something", "my-rel");
assertThat(link.getHref()).isEqualTo("/something");
assertThat(link.getRel()).isEqualTo(LinkRelation.of("my-rel"));
Link
は、RFC-8288 [IETF] (英語) で定義されている他の属性を公開します。Link
インスタンスで対応する wither メソッドを呼び出すことで、設定できます。
Spring MVC でのリンクの構築および Spring WebFlux でのリンクの構築で Spring MVC および Spring WebFlux コントローラーを指すリンクを作成する方法の詳細を確認してください。
2.2. URI テンプレート
Spring HATEOAS Link
の場合、ハイパーテキスト参照は URI だけでなく、RFC-6570 [IETF] (英語) に従った URI テンプレートにすることもできます。URI テンプレートには、いわゆるテンプレート変数が含まれており、これらのパラメーターを拡張できます。これにより、クライアントは、最終的な URI の構造を知らなくても、パラメーター化されたテンプレートを URI に変換できます。変数の名前だけを知る必要があります。
Link link = Link.of("/{segment}/something{?parameter}");
assertThat(link.isTemplated()).isTrue(); (1)
assertThat(link.getVariableNames()).contains("segment", "parameter"); (2)
Map<String, Object> values = new HashMap<>();
values.put("segment", "path");
values.put("parameter", 42);
assertThat(link.expand(values).getHref()) (3)
.isEqualTo("/path/something?parameter=42");
1 | Link インスタンスは、テンプレート化されていることを示します。つまり、URI テンプレートが含まれています。 |
2 | テンプレートに含まれるパラメーターを公開します。 |
3 | パラメーターの拡張が可能です。 |
URI テンプレートは手動で作成でき、後でテンプレート変数を追加できます。
UriTemplate template = UriTemplate.of("/{segment}/something")
.with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM);
assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}");
2.3. リンク関係
ターゲットリソースと現在のリソースの関連を示すには、いわゆるリンク関連が使用されます。Spring HATEOAS は、String
ベースのインスタンスを簡単に作成するための LinkRelation
型を提供します。
2.3.1. IANA リンク関係
Internet Assigned Numbers Authority には、定義済みの一連のリンク関係 (英語) が含まれています。それらは IanaLinkRelations
経由で参照できます。
Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT);
assertThat(link.getRel()).isEqualTo(LinkRelation.of("next"));
assertThat(IanaLinkRelation.isIanaRel(link.getRel())).isTrue();
2.4. 代表モデル
ハイパーメディアを強化した表現を簡単に作成するために、Spring HATEOAS はルートに RepresentationModel
を持つ一連のクラスを提供します。これは基本的に Link
のコレクションのコンテナーであり、モデルに追加するための便利なメソッドがあります。モデルは、ハイパーメディア要素が表現でどのように見えるかを定義するさまざまなメディア型 フォーマットに後でレンダリングできます。詳細については、メディアの種類を参照してください。
RepresentationModel
クラス階層 RepresentationModel
を操作するデフォルトの方法は、そのサブクラスを作成して、表現に含まれると想定されるすべてのプロパティを含め、そのクラスのインスタンスを作成し、プロパティを設定し、リンクで強化することです。
class PersonModel extends RepresentationModel<PersonModel> {
String firstname, lastname;
}
RepresentationModel.add(…)
が自身のインスタンスを返せるようにするには、一般的な自己型指定が必要です。モデル型は次のように使用できるようになりました。
PersonModel model = new PersonModel();
model.firstname = "Dave";
model.lastname = "Matthews";
model.add(Link.of("https://myhost/people/42"));
Spring MVC または WebFlux コントローラーからそのようなインスタンスを返し、クライアントが application/hal+json
に設定された Accept
ヘッダーを送信した場合、レスポンスは次のようになります。
{
"_links" : {
"self" : {
"href" : "https://myhost/people/42"
}
},
"firstname" : "Dave",
"lastname" : "Matthews"
}
2.4.1. アイテムのリソース表現モデル
単一のオブジェクトまたは概念に基づくリソースの場合、便利な EntityModel
型が存在します。コンセプトごとにカスタムモデル型を作成する代わりに、既存の型を再利用して、そのインスタンスを EntityModel
にラップすることができます。
EntityModel
を使用して既存のオブジェクトをラップする Person person = new Person("Dave", "Matthews");
EntityModel<Person> model = EntityModel.of(person);
2.4.2. コレクションのリソース表現モデル
概念的にコレクションであるリソースの場合、CollectionModel
を使用できます。その要素は、単純なオブジェクトまたは RepresentationModel
インスタンスのいずれかになります。
CollectionModel
を使用して既存のオブジェクトのコレクションをラップする Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews"));
CollectionModel<Person> model = CollectionModel.of(people);
EntityModel
は常にペイロードを含むように制約されているため、唯一のインスタンスで型の配置について推論することができますが、CollectionModel
の基礎となるコレクションは空である可能性があります。Java の型消去により、CollectionModel<Person> model = CollectionModel.empty()
が実際に CollectionModel<Person>
であることを実際に検出することはできません。これは、ランタイムインスタンスと空のコレクションしか表示されないためです。欠落している型情報は、CollectionModel.empty(Person.class)
を介して構築時に空のインスタンスに追加するか、基になるコレクションが空の可能性がある場合のフォールバックとして、モデルに追加できます。
Iterable<Person> people = repository.findAll();
var model = CollectionModel.of(people).withFallbackType(Person.class);
3. サーバー側のサポート
3.1. Spring MVC でのリンクの構築
これでドメインボキャブラリの準備は整いましたが、主な課題が残っています。それは、脆弱性の低いメソッドで Link
インスタンスにラップされる実際の URI をどのように作成するかということです。現時点では、URI 文字列をあちこちに複製する必要があります。そうすることはもろく、維持できません。
Spring MVC コントローラーが次のように実装されているとします。
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll() { … }
@GetMapping("/{person}")
HttpEntity<PersonModel> show(@PathVariable Long person) { … }
}
ここには 2 つの規則があります。1 つ目は、コントローラーメソッドの @GetMapping
アノテーションによって公開されるコレクションリソースで、そのコレクションの個々の要素が直接のサブリソースとして公開されます。コレクションリソースは、単純な URI (上記のように) またはより複雑な URI ( /people/{id}/addresses
など) で公開される場合があります。すべての人のコレクションリソースにリンクするとします。上記のアプローチに従うと、次の 2 つの問題が発生します。
絶対 URI を作成するには、プロトコル、ホスト名、ポート、サーブレットベース、その他の値を検索する必要があります。これは面倒であり、見苦しい手動の文字列連結コードが必要です。
おそらく、ベース URI の上に
/people
を連結したくないでしょう。複数の場所で情報を維持する必要があるからです。マッピングを変更すると、それを指すすべてのクライアントを変更する必要があります。
Spring HATEOAS は、コントローラークラスを指すことでリンクを作成できる WebMvcLinkBuilder
を提供するようになりました。次の例は、その方法を示しています。
Link link = linkTo(PersonController.class).withRel("people");
assertThat(link.getRel()).isEqualTo(LinkRelation.of("people"));
assertThat(link.getHref()).endsWith("/people");
WebMvcLinkBuilder
は内部で Spring の ServletUriComponentsBuilder
を使用して、現在のリクエストから基本的な URI 情報を取得します。アプリケーションが localhost:8080/your-app
で実行されると仮定すると、これはまさに URI であり、その上に追加のパーツを構築しています。ビルダーは、指定されたコントローラークラスのルートマッピングをインスペクションするため、最終的に localhost:8080/your-app/people
になります。さらにネストされたリンクを作成することもできます。次の例は、その方法を示しています。
Person person = new Person(1L, "Dave", "Matthews");
// /person / 1
Link link = linkTo(PersonController.class).slash(person.getId()).withSelfRel();
assertThat(link.getRel(), is(IanaLinkRelation.SELF.value()));
assertThat(link.getHref(), endsWith("/people/1"));
ビルダーでは、URI インスタンスを作成して構築することもできます (たとえば、レスポンスヘッダー値)。
HttpHeaders headers = new HttpHeaders();
headers.setLocation(linkTo(PersonController.class).slash(person).toUri());
return new ResponseEntity<PersonModel>(headers, HttpStatus.CREATED);
3.1.1. メソッドを指すリンクの作成
メソッドを指すリンクを作成したり、ダミーのコントローラーメソッド呼び出しを作成したりすることもできます。最初のアプローチは、Method
インスタンスを WebMvcLinkBuilder
に渡すことです。次の例は、その方法を示しています。
Method method = PersonController.class.getMethod("show", Long.class);
Link link = linkTo(method, 2L).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2"));
最初に Method
インスタンスを取得する必要があるため、これにはまだ少し不満があります。これは例外をスローし、通常は非常に面倒です。少なくとも、マッピングを繰り返しません。さらに良い方法は、コントローラープロキシでターゲットメソッドのダミーメソッド呼び出しを行うことです。これは、methodOn(…)
ヘルパーを使用して作成できます。次の例は、その方法を示しています。
Link link = linkTo(methodOn(PersonController.class).show(2L)).withSelfRel();
assertThat(link.getHref()).endsWith("/people/2");
methodOn(…)
は、メソッド呼び出しを記録するコントローラークラスのプロキシを作成し、メソッドの戻り値の型用に作成されたプロキシでそれを公開します。これにより、マッピングを取得したいメソッドの流れるような表現が可能になります。ただし、この手法を使用して取得できるメソッドにはいくつかの制約があります。
メソッド呼び出しを公開する必要があるため、戻り値の型はプロキシできる必要があります。
メソッドに渡されるパラメーターは通常無視されます (URI を構成するため、
@PathVariable
を介して参照されるものを除く)。
リクエストパラメーターのレンダリングの制御
コレクション値のリクエストパラメーターは、実際には 2 つの異なる方法で具体化できます。URI テンプレートの仕様には、値ごとにパラメーター名を繰り返す複合的なレンダリング方法 (param=value1¶m=value2
) と、値をコンマで区切る非複合的な方法 (param=value1,value2
) がリストされています。Spring MVC は、両方の形式からコレクションを適切に解析します。値のレンダリングは、デフォルトで複合スタイルになります。値を非複合スタイルでレンダリングする場合は、リクエストパラメーターハンドラーメソッドパラメーターで @NonComposite
アノテーションを使用できます。
@Controller
class PersonController {
@GetMapping("/people")
HttpEntity<PersonModel> showAll(
@NonComposite @RequestParam Collection<String> names) { … } (1)
}
var values = List.of("Matthews", "Beauford");
var link = linkTo(methodOn(PersonController.class).showAll(values)).withSelfRel(); (2)
assertThat(link.getHref()).endsWith("/people?names=Matthews,Beauford"); (3)
1 | @NonComposite アノテーションを使用して、値をカンマ区切りでレンダリングすることを宣言します。 |
2 | 値のリストを使用してメソッドを呼び出します。 |
3 | リクエストパラメーターが期待される形式でどのようにレンダリングされるかを確認してください。 |
@NonComposite を公開する理由は、リクエストパラメーターをレンダリングする複合的な方法が Spring の UriComponents ビルダーの内部に組み込まれており、Spring HATEOAS 1.4 でのみその非複合スタイルを導入したためです。今日ゼロから始めた場合、デフォルトでそのスタイルを使用し、ユーザーが明示的に複合スタイルを選択できるようにするのではなく、複合スタイルを明示的に選択できるようにするでしょう。 |
3.3. アフォーダンス
環境のアフォーダンスは、それが提供するものです... 善悪を問わず、環境が提供または提供するものです。動詞の「余裕」は辞書にありますが、名詞の「アフォーダンス」はありません。それを作りました。
視覚に対する生態学的アプローチ (126 ページ)
REST ベースのリソースは、データだけでなくコントロールも提供します。柔軟なサービスを形成するための最後の要素は、さまざまなコントロールの使用方法に関する詳細なアフォーダンスです。アフォーダンスはリンクに関連付けられているため、Spring HATEOAS は、必要な数の関連メソッドをリンクにアタッチするための API を提供します。Spring MVC コントローラーメソッド (詳細については Spring MVC でのリンクの構築を参照) を指定してリンクを作成できるのと同じように、…
次のコードは、セルフリンクを取得し、さらに 2 つのアフォーダンスを関連付ける方法を示しています。
GET /employees/{id}
に接続する @GetMapping("/employees/{id}")
public EntityModel<Employee> findOne(@PathVariable Integer id) {
Class<EmployeeController> controllerClass = EmployeeController.class;
// Start the affordance with the "self" link, i.e. this method.
Link findOneLink = linkTo(methodOn(controllerClass).findOne(id)).withSelfRel(); (1)
// Return the affordance + a link back to the entire collection resource.
return EntityModel.of(EMPLOYEES.get(id), //
findOneLink //
.andAffordance(afford(methodOn(controllerClass).updateEmployee(null, id))) (2)
.andAffordance(afford(methodOn(controllerClass).partiallyUpdateEmployee(null, id)))); (3)
}
1 | セルフリンクを作成します。 |
2 | updateEmployee メソッドを self リンクに関連付けます。 |
3 | partiallyUpdateEmployee メソッドを self リンクに関連付けます。 |
.andAffordance(afford(…))
を使用すると、コントローラーのメソッドを使用して PUT
および PATCH
操作を GET
操作に接続できます。上記の関連メソッドが次のようになっていると想像してください。
PUT /employees/{id}
に対応する updateEmpoyee
方式 @PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee( //
@RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
PATCH /employees/{id}
に対応する partiallyUpdateEmployee
方式 @PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee( //
@RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
afford(…)
メソッドを使用してこれらのメソッドを指すと、Spring HATEOAS はリクエスト本文とレスポンス型を分析し、メタデータをキャプチャーして、さまざまなメディア型の実装がその情報を使用して、それを入力と出力の説明に変換できるようにします。
3.3.1. アフォーダンスを手動で構築する
リンクのアフォーダンスを登録する主な方法ですが、手動で作成する必要がある場合もあります。これは、Affordances
API を使用して実現できます。
Affordances
API を使用して手動でアフォーダンスを登録する var methodInvocation = methodOn(EmployeeController.class).all();
var link = Affordances.of(linkTo(methodInvocation).withSelfRel()) (1)
.afford(HttpMethod.POST) (2)
.withInputAndOutput(Employee.class) //
.withName("createEmployee") //
.andAfford(HttpMethod.GET) (3)
.withOutput(Employee.class) //
.addParameters(//
QueryParameter.optional("name"), //
QueryParameter.optional("role")) //
.withName("search") //
.toLink();
1 | アフォーダンスを記述するためのコンテキストを作成する Link インスタンスから Affordances のインスタンスを作成することから始めます。 |
2 | 各アフォーダンスは、サポートするはずの HTTP メソッドから始まります。次に、型をペイロードの説明として登録し、アフォーダンスに明示的に名前を付けます。後者は省略可能で、デフォルト名は HTTP メソッドと入力型名から派生します。これにより、作成された EmployeeController.newEmployee(…) へのポインターと同じアフォーダンスが効果的に作成されます。 |
3 | 次のアフォーダンスは、EmployeeController.search(…) へのポインターで何が起こっているかを反映するように構築されています。ここでは、Employee を作成されたレスポンスのモデルとして定義し、明示的に QueryParameter を登録します。 |
アフォーダンスは、一般的なアフォーダンスメタデータを特定の表現に変換するメディア型固有のアフォーダンスモデルによって支えられています。そのメタデータの公開を制御する方法の詳細については、メディアの種類セクションのアフォーダンスのセクションを確認してください。
3.4. Forwarded ヘッダーの処理
RFC-7239 転送ヘッダー [IETF] (英語) は、アプリケーションがプロキシの背後にある場合、ロードバランサーの背後にある場合、またはクラウド内にある場合に最も一般的に使用されます。Web リクエストを実際に受信するノードはインフラストラクチャの一部であり、リクエストをアプリケーションに転送します。
アプリケーションは localhost:8080
で実行されている可能性がありますが、外部からは reallycoolsite.com
(および Web の標準ポート 80) にいることが期待されます。プロキシに追加のヘッダーを含めることにより (多くの場合すでに行われています)、Spring HATEOAS は Spring Framework 機能を使用して元のリクエストのベース URI を取得するため、適切にリンクを生成できます。
外部入力に基づいてルート URI を変更できるものはすべて、適切に保護する必要があります。そのため、デフォルトでは、転送されたヘッダーの処理が無効になっています。操作できるようにする必要があります。クラウドにデプロイする場合、またはプロキシとロードバランサーを制御する構成にデプロイする場合は、必ずこの機能を使用する必要があります。 |
転送されたヘッダー処理を有効にするには、Spring MVC 用の Spring の ForwardedHeaderFilter
(詳細はこちら ) または Spring WebFlux 用の ForwardedHeaderTransformer
(詳細はこちら ) をアプリケーションに登録する必要があります。Spring Boot アプリケーションでは、ここで説明するように、これらのコンポーネントを Spring Bean として簡単に宣言できます。
ForwardedHeaderFilter
の登録 @Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
これにより、すべての X-Forwarded- …
ヘッダーを処理するサーブレットフィルターが作成されます。そして、それをサーブレットハンドラーに適切に登録します。
Spring WebFlux アプリケーションの場合、対応するリアクティブは ForwardedHeaderTransformer
です。
ForwardedHeaderTransformer
の登録 @Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
return new ForwardedHeaderTransformer();
}
これにより、リアクティブ Web リクエストを変換し、X-Forwarded- …
ヘッダーを処理する関数が作成されます。そして、WebFlux に正しく登録します。
上記のように構成すると、X-Forwarded- …
ヘッダーを渡すリクエストは、生成されたリンクに反映されたものを確認します。
X-Forwarded- …
ヘッダーを使用したリクエスト curl -v localhost:8080/employees \
-H 'X-Forwarded-Proto: https' \
-H 'X-Forwarded-Host: example.com' \
-H 'X-Forwarded-Port: 9001'
{
"_embedded": {
"employees": [
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "https://example.com:9001/employees/1"
},
"employees": {
"href": "https://example.com:9001/employees"
}
}
}
]
},
"_links": {
"self": {
"href": "https://example.com:9001/employees"
},
"root": {
"href": "https://example.com:9001"
}
}
}
3.5. EntityLinks インターフェースの使用
EntityLinks とそのさまざまな実装は、現在、Spring WebFlux アプリケーション用にすぐに使用できるものではありません。EntityLinks SPI で定義された契約は、もともと Spring Web MVC を対象としており、Reactor 型を考慮していません。リアクティブプログラミングをサポートする同等の契約の開発は、まだ進行中です。 |
これまでのところ、Web フレームワークの実装 (つまり、Spring MVC コントローラー) を指すことによってリンクを作成し、マッピングをインスペクションしました。多くの場合、これらのクラスは基本的に、モデルクラスに基づく表現の読み取りと書き込みを行います。
EntityLinks
インターフェースは、モデル型に基づいて Link
または LinkBuilder
を検索する API を公開するようになりました。これらのメソッドは基本的に、コレクションリソース ( /people
など) またはアイテムリソース ( /people/1
など) を指すリンクを返します。次の例は、EntityLinks
の使用方法を示しています。
EntityLinks links = …;
LinkBuilder builder = links.linkFor(Customer.class);
Link link = links.linkToItemResource(Customer.class, 1L);
EntityLinks
は、Spring MVC 構成で @EnableHypermediaSupport
を有効にすることにより、依存性注入を介して利用できます。これにより、EntityLinks
のさまざまなデフォルト実装が登録されます。最も基本的なものは、SpringMVC コントローラークラスをインスペクションする ControllerEntityLinks
です。EntityLinks
の独自の実装を登録する場合は、このセクションを確認してください。
3.5.1. Spring MVC コントローラーに基づく EntityLinks
エンティティリンク機能を有効にすると、現在の ApplicationContext
で使用可能なすべての Spring MVC コントローラーが @ExposesResourceFor(…)
アノテーションについてインスペクションされます。アノテーションは、コントローラーが管理するモデル型を公開します。さらに、次の URI マッピングの設定と規則に従っていることを前提としています。
コントローラーがコレクションおよびアイテムリソースを公開するエンティティ型を宣言する型 レベル
@ExposesResourceFor(…)
。コレクションリソースを表すクラスレベルのベースマッピング。
追加のパスセグメントとして識別子を追加するようにマッピングを継承する、追加のメソッドレベルのマッピング。
次の例は、EntityLinks
-capable コントローラーの実装を示しています。
@Controller
@ExposesResourceFor(Order.class) (1)
@RequestMapping("/orders") (2)
class OrderController {
@GetMapping (3)
ResponseEntity orders(…) { … }
@GetMapping("{id}") (4)
ResponseEntity order(@PathVariable("id") … ) { … }
}
1 | コントローラーは、エンティティ Order のコレクションとアイテムのリソースを公開していることを示しています。 |
2 | そのコレクションリソースは /orders で公開されます |
3 | そのコレクションリソースは、GET リクエストを処理できます。必要に応じて、他の HTTP メソッドのメソッドを追加します。 |
4 | パス変数を使用して項目リソース (つまり、単一の Order ) を公開する従属リソースを処理する追加のコントローラーメソッド。 |
これにより、Spring MVC 構成で EntityLinks
@EnableHypermediaSupport
を有効にすると、次のようにコントローラーへのリンクを作成できます。
@Controller
class PaymentController {
private final EntityLinks entityLinks;
PaymentController(EntityLinks entityLinks) { (1)
this.entityLinks = entityLinks;
}
@PutMapping(…)
ResponseEntity payment(@PathVariable Long orderId) {
Link link = entityLinks.linkToItemResource(Order.class, orderId); (2)
…
}
}
1 | @EnableHypermediaSupport によって使用可能になった EntityLinks を構成に挿入します。 |
2 | API を使用して、コントローラークラスの代わりにエンティティ型を使用してリンクを作成します。 |
ご覧のとおり、OrderController
を明示的に参照しなくても、Order
インスタンスを管理するリソースを参照できます。
3.5.2. EntityLinks API の詳細
基本的に、EntityLinks
を使用すると、LinkBuilder
および Link
インスタンスを構築して、エンティティ型のリソースをコレクションおよびアイテムにすることができます。linkFor …
で始まるメソッドは、LinkBuilder
インスタンスを生成し、追加のパスセグメントやパラメーターなどで拡張および拡張できます。linkTo
で始まるメソッドは、完全に準備された Link
インスタンスを生成します。
コレクションリソースの場合はエンティティ型を提供するだけで十分ですが、アイテムリソースへのリンクには識別子を指定する必要があります。これは通常、次のようになります。
entityLinks.linkToItemResource(order, order.getId());
これらのメソッド呼び出しを繰り返していることに気付いた場合は、識別子抽出ステップを再利用可能な Function
にプルして、さまざまな呼び出しで再利用できます。
Function<Order, Object> idExtractor = Order::getId; (1)
entityLinks.linkToItemResource(order, idExtractor); (2)
1 | 識別子の抽出は、フィールドまたは定数に保持できるように外部化されます。 |
2 | エクストラクターを使用したリンクルックアップ。 |
TypedEntityLinks
コントローラーの実装はエンティティ型ごとにグループ化されることが多いため、コントローラークラス全体で同じ抽出関数 (詳細については EntityLinks API の詳細を参照) を使用していることに気付くことがよくあります。エクストラクターを提供する TypedEntityLinks
インスタンスを一度取得することで、識別子抽出ロジックをさらに一元化できるため、実際のルックアップで抽出を処理する必要がまったくなくなります。
class OrderController {
private final TypedEntityLinks<Order> links;
OrderController(EntityLinks entityLinks) { (1)
this.links = entityLinks.forType(Order::getId); (2)
}
@GetMapping
ResponseEntity<Order> someMethod(…) {
Order order = … // lookup order
Link link = links.linkToItemResource(order); (3)
}
}
1 | EntityLinks インスタンスを注入します。 |
2 | 特定の識別子抽出関数を使用して Order インスタンスを検索することを示します。 |
3 | 唯一の Order インスタンスに基づいてアイテムリソースリンクを検索します。 |
3.5.3. SPI としての EntityLinks
@EnableHypermediaSupport
によって作成された EntityLinks
インスタンスの型は DelegatingEntityLinks
であり、ApplicationContext
で Bean として使用可能な他のすべての EntityLinks
実装を取得します。これはプライマリ Bean として登録されているため、一般的に EntityLinks
を注入するときは常に唯一の注入候補になります。ControllerEntityLinks
はセットアップに含まれるデフォルトの実装ですが、ユーザーは自由に独自の実装を実装および登録できます。これらを EntityLinks
インスタンスでインジェクションに使用できるようにするには、実装を Spring Bean として登録するだけです。
@Configuration
class CustomEntityLinksConfiguration {
@Bean
MyEntityLinks myEntityLinks(…) {
return new MyEntityLinks(…);
}
}
このメカニズムの拡張性の例は、Spring Data REST の RepositoryEntityLinks
[GitHub] (英語) です。これは、リポジトリマッピング情報を使用して、Spring Data リポジトリによってサポートされるリソースを指すリンクを作成します。同時に、他の型のリソース用の追加のルックアップメソッドも公開します。これらを利用したい場合は、明示的に RepositoryEntityLinks
を注入するだけです。
3.6. 代表モデルアセンブラ
エンティティから表現モデルへのマッピングは複数の場所で使用する必要があるため、それを担当する専用のクラスを作成することは理にかなっています。変換には非常にカスタムな手順が含まれていますが、定型的な手順もいくつか含まれています。
モデルクラスのインスタンス化
レンダリングされるリソースを指す
self
のrel
を含むリンクを追加します。
Spring HATEOAS は、記述する必要のあるコードの量を減らすのに役立つ RepresentationModelAssemblerSupport
基本クラスを提供するようになりました。次の例は、その使用方法を示しています。
class PersonModelAssembler extends RepresentationModelAssemblerSupport<Person, PersonModel> {
public PersonModelAssembler() {
super(PersonController.class, PersonModel.class);
}
@Override
public PersonModel toModel(Person person) {
PersonModel resource = createResource(person);
// … do further mapping
return resource;
}
}
createResource(…) は、Person オブジェクトを指定して PersonModel オブジェクトをインスタンス化するために作成するコードです。Links の設定ではなく、属性の設定のみに焦点を当てる必要があります。 |
前の例で行ったようにクラスを設定すると、次の利点が得られます。
リソースのインスタンスを作成し、
self
の rel を追加したLink
を作成できるcreateModelWithId(…)
メソッドがいくつかあります。そのリンクの href は、構成されたコントローラーのリクエストマッピングとエンティティの ID (たとえば、/people/1
) によって決定されます。リソース型はリフレクションによってインスタンス化され、引数のないコンストラクターが必要です。専用のコンストラクターを使用するか、リフレクションパフォーマンスのオーバーヘッドを回避する場合は、
instantiateModel(…)
をオーバーライドできます。
その後、アセンブラを使用して RepresentationModel
または CollectionModel
をアセンブルできます。次の例では、PersonModel
インスタンスの CollectionModel
を作成します。
Person person = new Person(…);
Iterable<Person> people = Collections.singletonList(person);
PersonModelAssembler assembler = new PersonModelAssembler();
PersonModel model = assembler.toModel(person);
CollectionModel<PersonModel> model = assembler.toCollectionModel(people);
3.7. 表現モデルプロセッサー
場合によっては、ハイパーメディア表現を組み立てた後で微調整したり調整したりする必要があります。
完璧な例は、オーダーのフルフィルメントを処理するコントローラーがあるが、支払いに関連するリンクを追加する必要がある場合です。
この型のハイパーメディアを生成するオーダーシステムを想像してみてください。
{
"orderId" : "42",
"state" : "AWAITING_PAYMENT",
"_links" : {
"self" : {
"href" : "http://localhost/orders/999"
}
}
}
クライアントが支払いを行えるようにリンクを追加したいが、PaymentController
に関する詳細を OrderController
に混ぜたくない . オーダーシステムの詳細を汚す代わりに、次のように RepresentationModelProcessor
を書くことができます:
public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> { (1)
@Override
public EntityModel<Order> process(EntityModel<Order> model) {
model.add( (2)
Link.of("/payments/{orderId}").withRel(LinkRelation.of("payments")) //
.expand(model.getContent().getOrderId()));
return model; (3)
}
}
1 | このプロセッサーは、EntityModel<Order> オブジェクトにのみ適用されます。 |
2 | 無条件リンクを追加して、既存の EntityModel オブジェクトを操作します。 |
3 | リクエストされたメディア型に直列化できるように、EntityModel を返します。 |
プロセッサーをアプリケーションに登録します。
@Configuration
public class PaymentProcessingApp {
@Bean
PaymentProcessor paymentProcessor() {
return new PaymentProcessor();
}
}
Order
のハイパーメディア表現を発行すると、クライアントはこれを受け取ります。
{
"orderId" : "42",
"state" : "AWAITING_PAYMENT",
"_links" : {
"self" : {
"href" : "http://localhost/orders/999"
},
"payments" : { (1)
"href" : "/payments/42" (2)
}
}
}
1 | このリンクの関係としてプラグインされた LinkRelation.of("payments") が表示されます。 |
2 | URI はプロセッサーによって提供されました。 |
この例は非常に単純ですが、次のことが簡単にできます。
WebMvcLinkBuilder
またはWebFluxLinkBuilder
を使用して、PaymentController
への動的リンクを構築します。状態によって駆動される他のリンク (例:
cancel
、amend
) を条件付きで追加するために必要なサービスを注入します。Spring Security などの横断的サービスを活用して、現在のユーザーのコンテキストに基づいてリンクを追加、削除、修正します。
また、この例では、PaymentProcessor
は提供された EntityModel<Order>
を変更します。また、それを別のオブジェクトに置き換える力もあります。API では、戻り値の型が入力型と同じである必要があることに注意してください。
3.7.1. 空のコレクションモデルの処理
RepresentationModel
インスタンスに対して呼び出す RepresentationModelProcessor
インスタンスの適切なセットを見つけるために、呼び出し元のインフラストラクチャは、登録されている RepresentationModelProcessor
のジェネリクス宣言の詳細な分析を実行します。CollectionModel
インスタンスの場合、これには基になるコレクションの要素のインスペクションが含まれます。これは、実行時に唯一のモデルインスタンスがジェネリクス情報を公開しないためです (Java の型消去のため)。つまり、デフォルトでは、RepresentationModelProcessor
インスタンスは空のコレクションモデルに対して呼び出されません。インフラストラクチャがペイロード型を正しく推測できるようにするには、空の CollectionModel
インスタンスを最初から明示的なフォールバックペイロード型で初期化するか、CollectionModel.withFallbackType(…)
を呼び出して登録します。詳細については、コレクションのリソース表現モデルを参照してください。
3.8. LinkRelationProvider
API の使用
リンクを作成するときは、通常、リンクに使用する関係型を決定する必要があります。ほとんどの場合、関係型は (ドメイン) 型に直接関連付けられています。詳細なアルゴリズムをカプセル化して、LinkRelationProvider
API の背後にある関係型を検索し、単一およびコレクションリソースの関係型を決定できるようにします。リレーション型を検索するアルゴリズムは次のとおりです。
型に
@Relation
のアノテーションが付けられている場合、アノテーションで構成された値を使用します。そうでない場合は、コレクション
rel
の追加されたList
に加えて、大文字ではない単純なクラス名がデフォルトになります。EVO インフレクター [GitHub] (英語) JAR がクラスパスにある場合、複数形化アルゴリズムによって提供される単一リソース
rel
の複数形を使用します。@ExposesResourceFor
でアノテーションが付けられた@Controller
クラス (詳細については EntityLinks インターフェースの使用を参照) は、アノテーションで構成された型の関係型を透過的に検索するため、LinkRelationProvider.getItemResourceRelFor(MyController.class)
を使用してドメイン型の関係型を公開できます。
@EnableHypermediaSupport
を使用すると、LinkRelationProvider
は Spring Bean として自動的に公開されます。インターフェースを実装し、Spring Bean として順番に公開することで、カスタムプロバイダーをプラグインできます。
4. メディアの種類
4.1. HAL – ハイパーテキストアプリケーション言語
JSON ハイパーテキストアプリケーション言語 [IETF] (英語) または HAL は、特定の Web スタックについて議論しない場合に採用される、最も単純で最も広く採用されているハイパーメディアメディア型の 1 つです。
これは、Spring HATEOAS で採用された最初の仕様ベースのメディア型でした。
4.1.1. HAL 表現モデルの構築
Spring HATEOAS 1.1 の時点で、HAL イディオム API を介して RepresentationModel
インスタンスを作成できる専用の HalModelBuilder
を提供しています。これらはその基本的な仮定です:
HAL 表現は、表現に含まれるドメインフィールドを構築する任意のオブジェクト (エンティティ) によってサポートできます。
表現は、任意のオブジェクトまたは HAL 表現自体 (つまり、ネストされた埋め込みとリンクを含む) のいずれかである、さまざまな埋め込みドキュメントによって強化できます。
特定の HAL 固有のパターン (プレビューなど) を API で直接使用できるため、表現を設定するコードは、これらのイディオムに従って HAL 表現を記述するように読めます。
使用される API の例を次に示します。
// An order
var order = new Order(…); (1)
// The customer who placed the order
var customer = customer.findById(order.getCustomerId());
var customerLink = Link.of("/orders/{id}/customer") (2)
.expand(order.getId())
.withRel("customer");
var additional = …
var model = HalModelBuilder.halModelOf(order)
.preview(new CustomerSummary(customer)) (3)
.forLink(customerLink) (4)
.embed(additional) (5)
.link(Link.of(…, IanaLinkRelations.SELF));
.build();
1 | いくつかのドメイン型を設定しました。この場合、発注した顧客と関連のあるオーダーです。 |
2 | 顧客の詳細を公開するリソースへのリンクを用意します |
3 | _embeddable 句内でレンダリングされるはずのペイロードを提供することで、プレビューの構築を開始します。 |
4 | ターゲットリンクを提供することで、そのプレビューを終了します。これは透過的に _links オブジェクトに追加され、そのリンク関係は前のステップで提供されたオブジェクトのキーとして使用されます。 |
5 | 他のオブジェクトを追加して、_embedded に表示できます。それらがリストされているキーは、オブジェクトの関係設定から派生します。これらは、@Relation または専用の LinkRelationProvider を介してカスタマイズできます (詳細については、 LinkRelationProvider API の使用を参照してください)。 |
{
"_links" : {
"self" : { "href" : "…" }, (1)
"customer" : { "href" : "/orders/4711/customer" } (2)
},
"_embedded" : {
"customer" : { … }, (3)
"additional" : { … } (4)
}
}
1 | 明示的に提供された self リンク。 |
2 | … .preview(…).forLink(…) を介して透過的に追加された customer リンク。 |
3 | 提供されたプレビューオブジェクト。 |
4 | 明示的な … .embed(…) によって追加された追加要素。 |
HAL では、_embedded
はトップコレクションを表すためにも使用されます。それらは通常、オブジェクトの型から派生したリンク関係にグループ化されます。つまりオーダーのリストは、HAL では次のようになります。
{
"_embedded" : {
"order : [
… (1)
]
}
}
1 | 個別オーダードキュメントはこちら |
このような表現の作成は、次のように簡単です。
Collection<Order> orders = …;
HalModelBuilder.emptyHalDocument()
.embed(orders);
つまり、オーダーが空の場合、_embedded
内に表示されるリンク関係を導出する方法がないため、コレクションが空の場合、ドキュメントは空のままになります。
空のコレクションを明示的に通信したい場合は、型を Collection
を取る … .embed(…)
メソッドのオーバーロードに渡すことができます。メソッドに渡されたコレクションが空の場合、指定された型から派生したリンク関係でフィールドがレンダリングされます。
HalModelBuilder.emptyHalModel()
.embed(Collections.emptyList(), Order.class);
// or
.embed(Collections.emptyList(), LinkRelation.of("orders"));
次のより明示的な表現を作成します。
{
"_embedded" : {
"orders" : []
}
}
4.1.2. リンクレンダリングの構成
HAL では、_links
エントリは JSON オブジェクトです。プロパティ名はリンク関係で、各値はリンクオブジェクトまたはリンクオブジェクトの配列 [IETF] (英語) のいずれかです。
2 つ以上のリンクを持つ特定のリンク関係の場合、仕様は表現に関して明確です。
{
"_links": {
"item": [
{ "href": "https://myhost/cart/42" },
{ "href": "https://myhost/inventory/12" }
]
},
"customer": "Dave Matthews"
}
しかし、特定のリレーションに対してリンクが 1 つしかない場合、仕様はあいまいです。それを単一のオブジェクトまたは単一項目の配列としてレンダリングできます。
デフォルトでは、Spring HATEOAS は最も簡潔なアプローチを使用し、次のような単一リンクの関係をレンダリングします。
{
"_links": {
"item": { "href": "https://myhost/inventory/12" }
},
"customer": "Dave Matthews"
}
一部のユーザーは、HAL を使用するときに配列とオブジェクトを切り替えたくない場合があります。彼らはこの型のレンダリングを好みます:
{
"_links": {
"item": [{ "href": "https://myhost/inventory/12" }]
},
"customer": "Dave Matthews"
}
このポリシーをカスタマイズする場合は、アプリケーション構成に HalConfiguration
Bean を挿入するだけです。複数の選択肢があります。
@Bean
public HalConfiguration globalPolicy() {
return new HalConfiguration() //
.withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1)
}
1 | すべての単一リンク関係を配列としてレンダリングすることにより、Spring HATEOAS のデフォルトをオーバーライドします。 |
特定のリンク関係のみをオーバーライドしたい場合は、次のように HalConfiguration
Bean を作成できます。
@Bean
public HalConfiguration linkRelationBasedPolicy() {
return new HalConfiguration() //
.withRenderSingleLinksFor( //
IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) (1)
.withRenderSingleLinksFor( //
LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); (2)
}
1 | item リンク関係を常に配列としてレンダリングします。 |
2 | リンクが 1 つしかない場合、prev リンク関係をオブジェクトとしてレンダリングします。 |
これらのいずれもニーズに合わない場合は、Ant スタイルのパスパターンを使用できます。
@Bean
public HalConfiguration patternBasedPolicy() {
return new HalConfiguration() //
.withRenderSingleLinksFor( //
"http*", RenderSingleLinks.AS_ARRAY); (1)
}
1 | http で始まるすべてのリンク関係を配列としてレンダリングします。 |
パターンベースのアプローチでは、Spring の AntPathMatcher を使用します。 |
これらすべての HalConfiguration
ウィザーを組み合わせて、1 つの包括的なポリシーを形成できます。驚きを避けるために、API を広範囲にテストしてください。
4.1.3. リンクタイトルの国際化
HAL は、そのリンクオブジェクトの title
属性を定義します。これらのタイトルは、クライアントが UI で直接使用できるように、Spring のリソースバンドルの抽象化と rest-messages
という名前のリソースバンドルを使用して設定できます。このバンドルは自動的にセットアップされ、HAL リンクの直列化中に使用されます。
リンクのタイトルを定義するには、キーテンプレート _links.$relationName.title
を次のように使用します。
rest-messages.properties
_links.cancel.title=Cancel order
_links.payment.title=Proceed to checkout
これにより、次の HAL 表現が生成されます。
{
"_links" : {
"cancel" : {
"href" : "…"
"title" : "Cancel order"
},
"payment" : {
"href" : "…"
"title" : "Proceed to checkout"
}
}
}
4.1.4. CurieProvider
API の使用
Web リンク RFC [IETF] (英語) は、登録済みおよび拡張リンクの関係型を記述します。登録された rel は、リンク関係型の IANA レジストリ (英語) に登録された既知の文字列です。拡張 rel
URI は、関係型を登録したくないアプリケーションで使用できます。それぞれが関係型を一意に識別する URI です。rel
URI は、コンパクト URI またはキュリー [W3C] (英語) としてシリアライズできます。例: ex
が example.com/rels/{rel} (英語)
として定義されている場合、ex:persons
のキュリーはリンク関係型 example.com/rels/persons (英語)
を表します。キュリーを使用する場合、ベース URI がレスポンススコープに存在する必要があります。
デフォルトの RelProvider
によって作成される rel
値は拡張関係型であり、その結果、多くのオーバーヘッドを引き起こす可能性がある URI でなければなりません。CurieProvider
API がそれを処理します。ベース URI を URI テンプレートとして定義し、そのベース URI を表すプレフィックスを定義できます。CurieProvider
が存在する場合、RelProvider
はすべての rel
値の先頭にキュリープレフィックスを追加します。さらに、curies
リンクが HAL リソースに自動的に追加されます。
次の構成は、デフォルトのキュリープロバイダーを定義します。
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type= {HypermediaType.HAL})
public class Config {
@Bean
public CurieProvider curieProvider() {
return new DefaultCurieProvider("ex", new UriTemplate("https://www.example.com/rels/{rel}"));
}
}
ex:
プレフィックスは、ex:orders
のように、IANA に登録されていないすべての rel 値の前に自動的に表示されることに注意してください。クライアントは curies
リンクを使用して、キュリーを完全な形式に解決できます。次の例は、その方法を示しています。
{
"_links": {
"self": {
"href": "https://myhost/person/1"
},
"curies": {
"name": "ex",
"href": "https://example.com/rels/{rel}",
"templated": true
},
"ex:orders": {
"href": "https://myhost/person/1/orders"
}
},
"firstname": "Dave",
"lastname": "Matthews"
}
CurieProvider
API の目的はキュリーの自動作成を可能にすることなので、アプリケーションスコープごとに CurieProvider
Bean を 1 つだけ定義できます。
4.2. HAL-FORMS
HAL-FORMS (英語) は、実行時の FORM サポートを HAL メディア型に追加するように設計されています。
HAL-FORMS「HAL みたい」ただし、HAL-FORMS は HAL と同じではないことに注意してください。この 2 つを交換可能と考えるべきではありません。
HAL-FORMS 仕様
このメディア型を有効にするには、コードに次の構成を追加します。
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {
}
クライアントが application/prs.hal-forms+json
で Accept
ヘッダーを提供するときはいつでも、次のようなことが期待できます。
{
"firstName" : "Frodo",
"lastName" : "Baggins",
"role" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost:8080/employees/1"
}
},
"_templates" : {
"default" : {
"method" : "put",
"properties" : [ {
"name" : "firstName",
"required" : true
}, {
"name" : "lastName",
"required" : true
}, {
"name" : "role",
"required" : true
} ]
},
"partiallyUpdateEmployee" : {
"method" : "patch",
"properties" : [ {
"name" : "firstName",
"required" : false
}, {
"name" : "lastName",
"required" : false
}, {
"name" : "role",
"required" : false
} ]
}
}
}
HAL-FORMS 仕様 (英語) を調べて、_ テンプレート属性の詳細を理解しましょう。アフォーダンス API について読んで、この追加のメタデータでコントローラーを強化してください。
単一項目 (EntityModel
) および集約ルートコレクション (CollectionModel
) に関しては、Spring HATEOAS はそれらを HAL ドキュメントと同じようにレンダリングします。
4.2.1. HAL-FORMS メタデータの定義
HAL-FORMS では、フォームフィールドごとに条件を記述できます。Spring HATEOAS では、入力型と出力型のモデル型を整形し、アノテーションを使用することで、カスタマイズできます。
各テンプレートには、次の属性が定義されています。
属性 | 説明 |
---|---|
| サーバーが受信すると予想されるメディアの種類。指すコントローラーメソッドが |
| テンプレートを送信するときに使用する HTTP メソッド。 |
| フォームの送信先のターゲット URI。アフォーダンスターゲットが宣言されたリンクと異なる場合にのみレンダリングされます。 |
| テンプレートを表示するときの人間が読めるタイトル。 |
| フォームで送信するすべてのプロパティ (以下を参照)。 |
各プロパティは、定義された次の属性を取得します。
属性 | 説明 |
---|---|
| プロパティに setter メソッドがない場合は、 |
| フィールドまたは型のいずれかで JSR-303 の |
| JSR-303 の |
| プロパティに許可される最大値。JSR-303 の |
| プロパティに許可される最大長の値。Hibernate Validator の |
| プロパティに許可される最小値。JSR-303 の |
| プロパティに許可される最小の長さの値。Hibernate Validator の |
| フォームを送信するときに値を選択するオプション。詳細については、プロパティの HAL-FORMS オプションの定義を参照してください。 |
| フォーム入力をレンダリングするときに使用する、ユーザーが読み取り可能なプロンプト。詳細については、プロパティプロンプトを参照してください。 |
| 予想される形式の例を示す、ユーザーが読み取り可能なプレースホルダー。定義する方法はプロパティプロンプトに従いますが、サフィックス |
| 明示的な |
手動でアノテーションを付けることができない型については、アプリケーションコンテキストに存在する HalFormsConfiguration
Bean を介してカスタムパターンを登録できます。
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}");
}
}
この設定により、型 CreditCardNumber
の表現モデルプロパティの HAL-FORMS テンプレートプロパティが、値 [0-9]{16}
を持つ regex
フィールドを宣言します。
プロパティの HAL-FORMS オプションの定義
値が特定の値のスーパーセットと一致すると想定されるプロパティの場合、HAL-FORMS はプロパティ定義内で options
サブドキュメントを定義します。特定のプロパティで使用可能なオプションは、型のプロパティへのポインタを取得する HalFormsConfiguration
の withOptions(…)
と、PropertyMetadata
を HalFormsOptions
インスタンスに変換するクリエータ関数を介して記述できます。
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.withOptions(Order.class, "shippingMethod" metadata ->
HalFormsOptions.inline("FedEx", "DHL"));
}
}
オプション値 FedEx
および DHL
を Order.shippingMethod
プロパティから選択するオプションとして設定する方法を参照してください。あるいは、HalFormsOptions.remote(…)
は動的に値を提供する リモートリソースを指すことができます。オプション設定に関するその他の制約については、仕様 (英語) または HalFormsOptions
の Javadoc を参照してください。
4.2.2. フォーム属性の国際化
HAL-FORMS には、テンプレートのタイトルやプロパティプロンプトなど、人間が解釈するための属性が含まれています。これらは、Spring のリソースバンドルサポートと、デフォルトで Spring HATEOAS によって構成された rest-messages
リソースバンドルを使用して定義および国際化できます。
テンプレートのタイトル
テンプレートのタイトルを定義するには、パターン _templates.$affordanceName.title
を使用します。HAL-FORMS では、テンプレートが 1 つしかない場合、テンプレートの名前は default
であることに注意してください。これは、通常、アフォーダンスが説明するローカルまたは完全修飾入力型名でキーを修飾する必要があることを意味します。
_templates.default.title=Some title (1)
_templates.putEmployee.title=Create employee (2)
Employee._templates.default.title=Create employee (3)
com.acme.Employee._templates.default.title=Create employee (4)
1 | default をキーとして使用するタイトルのグローバル定義。 |
2 | 実際のアフォーダンス名をキーとして使用するタイトルのグローバル定義。アフォーダンスの作成時に明示的に定義されていない限り、これはアフォーダンスの作成時に指定されたメソッドの名前にデフォルト設定されます。 |
3 | Employee という名前のすべての型に適用される、ローカルで定義されたタイトル。 |
4 | 完全修飾型名を使用したタイトル定義。 |
実際のアフォーダンス名を使用するキーは、デフォルトのものより優先されます。 |
プロパティプロンプト
プロパティプロンプトは、Spring HATEOAS によって自動的に構成された rest-messages
リソースバンドルを介して解決することもできます。キーは、グローバル、ローカル、完全修飾で定義でき、実際のプロパティキーに連結された ._prompt
が必要です。
email
プロパティのプロンプトの定義 firstName._prompt=Firstname (1)
Employee.firstName._prompt=Firstname (2)
com.acme.Employee.firstName._prompt=Firstname (3)
1 | firstName という名前のすべてのプロパティは、宣言されている型に関係なく、レンダリングされる "Firstname" を取得します。 |
2 | Employee という名前の型の firstName プロパティは、"Firstname" と表示されます。 |
3 | com.acme.Employee の firstName プロパティには、"Firstname" のプロンプトが割り当てられます。 |
4.2.3. 完全な例
上記のすべての定義属性とカスタマイズ属性を組み合わせたコード例を見てみましょう。顧客の RepresentationModel
は次のようになります。
class CustomerRepresentation
extends RepresentationModel<CustomerRepresentation> {
String name;
LocalDate birthdate; (1)
@Pattern(regex = "[0-9]{16}") String ccn; (2)
@Email String email; (3)
}
1 | 型 LocalDate の birthdate プロパティを定義します。 |
2 | ccn が正規表現に従うことを期待しています。 |
3 | JSR-303 @Email アノテーションを使用して、email をメールとして定義します。 |
この型はドメイン型ではないことに注意してください。フィールドの潜在的に誤った値を一度に拒否できるように、無効になる可能性のある幅広い入力をキャプチャーするように意図的に設計されています。
コントローラーがそのモデルをどのように利用するかを見てみましょう。
@Controller
class CustomerController {
@PostMapping("/customers")
EntityModel<?> createCustomer(@RequestBody CustomerRepresentation payload) { (1)
// …
}
@GetMapping("/customers")
CollectionModel<?> getCustomers() {
CollectionModel<?> model = …;
CustomerController controller = methodOn(CustomerController.class);
model.add(linkTo(controller.getCustomers()).withSelfRel() (2)
.andAfford(controller.createCustomer(null)));
return ResponseEntity.ok(model);
}
}
1 | コントローラーメソッドは、POST が /customers に対して発行された場合に、リクエスト本文をバインドするために上記で定義された表現モデルを使用するように宣言されています。 |
2 | /customers への GET リクエストは、モデルを準備し、それに self リンクを追加し、さらに、POST にマップされたコントローラーメソッドを指すまさにそのリンク上でアフォーダンスを宣言します。これにより、アフォーダンスモデルが構築され、最終的にレンダリングされるメディア型に応じて、メディア型固有の形式に変換されます。 |
次に、人間がフォームにアクセスしやすくするために、いくつかのメタデータを追加しましょう。
rest-messages.properties
で宣言された追加のプロパティ。CustomerRepresentation._template.createCustomer.title=Create customer (1)
CustomerRepresentation.ccn._prompt=Credit card number (2)
CustomerRepresentation.ccn._placeholder=1234123412341234 (2)
1 | createCustomer(…) メソッドを指すことによって作成されたテンプレートの明示的なタイトルを定義します。 |
2 | CustomerRepresentation モデルの ccn プロパティのプロンプトとプレースホルダーを明示的に指定します。 |
クライアントが application/prs.hal-forms+json
の Accept
ヘッダーを使用して /customers
に GET
リクエストを発行すると、レスポンス HAL ドキュメントは HAL-FORMS ドキュメントに拡張され、次の _templates
定義が含まれます。
{
…,
"_templates" : {
"default" : { (1)
"title" : "Create customer", (2)
"method" : "post", (3)
"properties" : [ {
"name" : "name",
"required" : true,
"type" : "text" (4)
} , {
"name" : "birthdate",
"required" : true,
"type" : "date" (4)
} , {
"name" : "ccn",
"prompt" : "Credit card number", (5)
"placeholder" : "1234123412341234" (5)
"required" : true,
"regex" : "[0-9]{16}", (6)
"type" : "text"
} , {
"name" : "email",
"prompt" : "Email",
"required" : true,
"type" : "email" (7)
} ]
}
}
}
1 | default という名前のテンプレートが公開されています。その名前は、定義された唯一のテンプレートであり、仕様ではその名前を使用する必要があるため、default です。複数のテンプレートがアタッチされている場合 (追加のアフォーダンスを宣言することによって)、それらはそれぞれが指しているメソッドにちなんで名付けられます。 |
2 | テンプレートのタイトルは、リソースバンドルで定義された値から派生します。リクエストとともに送信される Accept-Language ヘッダーと可用性に応じて、異なる値が返される可能性があることに注意してください。 |
3 | method 属性の値は、アフォーダンスが派生したメソッドのマッピングから派生します。 |
4 | type 属性の値 text は、プロパティの型 String から派生します。同じことが birthdate プロパティにも当てはまりますが、結果は date になります。 |
5 | ccn プロパティのプロンプトとプレースホルダーも、リソースバンドルから派生します。 |
6 | ccn プロパティの @Pattern 宣言は、テンプレートプロパティの regex 属性として公開されます。 |
7 | email プロパティの @Email アノテーションは、対応する type 値に変換されました。 |
HAL-FORMS テンプレートは、たとえば、これらの記述から HTML フォームを自動的にレンダリングする HAL エクスプローラー [GitHub] (英語) 。
4.3. HTTP 問題の詳細
HTTP API の問題の詳細 [IETF] (英語) は、HTTP API の新しいエラーレスポンス形式を定義する必要を回避するために、HTTP レスポンスで機械可読エラーの詳細を伝えるメディア型です。
HTTP Problem Details は、エラーの詳細を HTTP クライアントに説明するための追加情報を運ぶ一連の JSON プロパティを定義します。これらのプロパティの詳細については、特に RFC ドキュメント [IETF] (英語) の関連セクションを参照してください。
Spring MVC コントローラーで Problem
メディア型 ドメイン型を使用して、このような JSON レスポンスを作成できます。
Problem
型を使用して問題の詳細を報告する @RestController
class PaymentController {
@PutMapping
ResponseEntity<?> issuePayment(@RequestBody PaymentRequest request) {
PaymentResult result = payments.issuePayment(request.orderId, request.amount);
if (result.isSuccess()) {
return ResponseEntity.ok(result);
}
String title = messages.getMessage("payment.out-of-credit");
String detail = messages.getMessage("payment.out-of-credit.details", //
new Object[] { result.getBalance(), result.getCost() });
Problem problem = Problem.create() (1)
.withType(OUT_OF_CREDIT_URI) //
.withTitle(title) (2)
.withDetail(detail) //
.withInstance(PAYMENT_ERROR_INSTANCE.expand(result.getPaymentId())) //
.withProperties(map -> { (3)
map.put("balance", result.getBalance());
map.put("accounts", Arrays.asList( //
ACCOUNTS.expand(result.getSourceAccountId()), //
ACCOUNTS.expand(result.getTargetAccountId()) //
));
});
return ResponseEntity.status(HttpStatus.FORBIDDEN) //
.body(problem);
}
}
1 | 公開されているファクトリメソッドを使用して Problem のインスタンスを作成することから始めます。 |
2 | メディア型によって定義されたデフォルトプロパティの値を定義できます。Spring の国際化機能を使用した型 URI、タイトル、詳細 (上記参照)。 |
3 | カスタムプロパティは、Map または明示的なオブジェクトを介して追加できます (以下を参照)。 |
カスタムプロパティに専用のオブジェクトを使用するには、型を宣言し、そのインスタンスを作成して入力し、これを … .withProperties(…)
経由または Problem.create(…)
経由のインスタンス作成時に Problem
インスタンスに渡します。
class AccountDetails {
int balance;
List<URI> accounts;
}
problem.withProperties(result.getDetails());
// or
Problem.create(result.getDetails());
これにより、次のようなレスポンスが得られます。
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345",
"/account/67890"]
}
4.4. コレクション +JSON
コレクション +JSON (英語) は、IANA 承認のメディア型 application/vnd.collection+json
で登録された JSON 仕様です。
コレクション +JSON (英語) は、単純なコレクションの管理とクエリをサポートするように設計された、JSON ベースの読み取り / 書き込みハイパーメディア型です。
コレクション +JSON 仕様
Collection+JSON は、単一アイテムのリソースとコレクションの両方を表す統一された方法を提供します。このメディア型を有効にするには、コードに次の構成を追加します。
@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {
}
この構成により、以下に示すように、application/vnd.collection+json
の Accept
ヘッダーを持つリクエストにアプリケーションが応答するようになります。
仕様の次の例は、単一のアイテムを示しています。
{
"collection": {
"version": "1.0",
"href": "https://example.org/friends/", (1)
"links": [ (2)
{
"rel": "feed",
"href": "https://example.org/friends/rss"
},
{
"rel": "queries",
"href": "https://example.org/friends/?queries"
},
{
"rel": "template",
"href": "https://example.org/friends/?template"
}
],
"items": [ (3)
{
"href": "https://example.org/friends/jdoe",
"data": [ (4)
{
"name": "fullname",
"value": "J. Doe",
"prompt": "Full Name"
},
{
"name": "email",
"value": "[email protected] (英語) ",
"prompt": "Email"
}
],
"links": [ (5)
{
"rel": "blog",
"href": "https://examples.org/blogs/jdoe",
"prompt": "Blog"
},
{
"rel": "avatar",
"href": "https://examples.org/images/jdoe",
"prompt": "Avatar",
"render": "image"
}
]
}
]
}
}
1 | self リンクは、ドキュメントの href 属性に格納されます。 |
2 | ドキュメントの上部の links セクションには、コレクションレベルのリンク (self リンクを除く) が含まれています。 |
3 | items セクションには、データのコレクションが含まれています。これは単一項目のドキュメントであるため、エントリは 1 つだけです。 |
4 | data セクションには、実際のコンテンツが含まれています。プロパティで構成されています。 |
5 | アイテムの個別の links . |
前のフラグメントは仕様から削除されました。Spring HATEOAS が
|
リソースのコレクションをレンダリングする場合、ドキュメントはほぼ同じですが、items
JSON 配列内にエントリごとに 1 つずつ複数のエントリが存在する点が異なります。
Spring HATEOAS は、より具体的には次のことを行います。
コレクション全体の
self
リンクを最上位のhref
属性に入れます。CollectionModel
リンク (マイナスself
) は、最上位のlinks
に配置されます。各項目レベルの
href
には、CollectionModel.content
コレクションの各エントリに対応するself
リンクが含まれます。各項目レベルの
links
には、CollectionModel.content
からの各エントリの他のすべてのリンクが含まれます。
4.5. UBER - 表現を交換するための統一基準
UBER (英語) は実験的な JSON 仕様です
UBER ドキュメント形式は、単純な状態転送とアドホックハイパーメディアベースの遷移をサポートするように設計された、最小限の読み取り / 書き込みハイパーメディア型です。
UBER 仕様
UBER は、単一アイテムのリソースとコレクションの両方を表す統一された方法を提供します。このメディア型を有効にするには、コードに次の構成を追加します。
@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {
}
この設定により、以下に示すように、アプリケーションは Accept
ヘッダー application/vnd.amundsen-uber+json
を使用してリクエストに応答するようになります。
{
"uber" : {
"version" : "1.0",
"data" : [ {
"rel" : [ "self" ],
"url" : "/employees/1"
}, {
"name" : "employee",
"data" : [ {
"name" : "role",
"value" : "ring bearer"
}, {
"name" : "name",
"value" : "Frodo"
} ]
} ]
}
}
このメディア型は、仕様自体と同様にまだ開発中です。チケットを使用して問題が発生した場合は、気軽にチケットをオープンしてください [GitHub] (英語) 。
UBER メディア型は、ライドシェアリング会社であるウーバーテクノロジーズ株式会社とは一切関係がありません。 |
4.6. ALPS - アプリケーションレベルのプロファイルセマンティクス
ALPS [IETF] (英語) は、別のリソースに関するプロファイルベースのメタデータを提供するためのメディア型です。
ALPS ドキュメントは、アプリケーションにとらわれないメディア型 (HTML、HAL、Collection+JSON、Siren など) を持つドキュメントのアプリケーションセマンティクスを説明するプロファイルとして使用できます。これにより、メディア型全体でプロファイルドキュメントの再利用性が向上します。
アルプス仕様
ALPS は特別なアクティベーションを必要としません。代わりに、Alps
レコードを「構築」し、次に示すように Spring MVC または Spring WebFlux Web メソッドからそれを返します。
Alps
レコードの作成 @GetMapping(value = "/profile", produces = ALPS_JSON_VALUE)
Alps profile() {
return Alps.alps() //
.doc(doc() //
.href("https://example.org/samples/full/doc.html") //
.value("value goes here") //
.format(Format.TEXT) //
.build()) //
.descriptor(getExposedProperties(Employee.class).stream() //
.map(property -> Descriptor.builder() //
.id("class field [" + property.getName() + "]") //
.name(property.getName()) //
.type(Type.SEMANTIC) //
.ext(Ext.builder() //
.id("ext [" + property.getName() + "]") //
.href("https://example.org/samples/ext/" + property.getName()) //
.value("value goes here") //
.build()) //
.rt("rt for [" + property.getName() + "]") //
.descriptor(Collections.singletonList(Descriptor.builder().id("embedded").build())) //
.build()) //
.collect(Collectors.toList()))
.build();
}
この例では、
PropertyUtils.getExposedProperties()
を利用して、ドメインオブジェクトの属性に関するメタデータを抽出します。
このフラグメントには、テストデータがプラグインされています。次のような JSON が生成されます。
{ "version": "1.0", "doc": { "format": "TEXT", "href": "https://example.org/samples/full/doc.html", "value": "value goes here" }, "descriptor": [ { "id": "class field [name]", "name": "name", "type": "SEMANTIC", "descriptor": [ { "id": "embedded" } ], "ext": { "id": "ext [name]", "href": "https://example.org/samples/ext/name", "value": "value goes here" }, "rt": "rt for [name]" }, { "id": "class field [role]", "name": "role", "type": "SEMANTIC", "descriptor": [ { "id": "embedded" } ], "ext": { "id": "ext [role]", "href": "https://example.org/samples/ext/role", "value": "value goes here" }, "rt": "rt for [role]" } ] }
各フィールドを「自動的に」ドメインオブジェクトのフィールドにリンクする代わりに、必要に応じて手動で記述することができます。Spring Framework のメッセージバンドルと MessageSource
インターフェースを使用することもできます。これにより、これらの値をロケール固有のメッセージバンドルに委譲し、メタデータを国際化することさえできます。
4.7. コミュニティベースのメディア型
独自のメディア型を作成できるため、追加のメディア型を作成するためのコミュニティ主導の取り組みがいくつかあります。
4.7.1. JSON:API
メディアの種類指定:
application/vnd.api+json
最新のリリース
現在のスナップショット
プロジェクトリーダー: Kai Toedter [GitHub] (英語)
<dependency>
<groupId>com.toedter</groupId>
<artifactId>spring-hateoas-jsonapi</artifactId>
<version>{see project page for current version}</version>
</dependency>
implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'
スナップショットリリースが必要な場合は、詳細についてプロジェクトページにアクセスしてください。
4.7.2. サイレン
メディアの種類指定:
application/vnd.siren+json
プロジェクトリーダー: Ingo Griebsch [GitHub] (英語)
<dependency>
<groupId>de.ingogriebsch.hateoas</groupId>
<artifactId>spring-hateoas-siren</artifactId>
<version>{see project page for current version}</version>
<scope>compile</scope>
</dependency>
implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'
4.8. カスタムメディア型の登録
Spring HATEOAS を使用すると、SPI を介してカスタムメディア型を統合できます。このような実装の構成要素は次のとおりです。
Jackson
ObjectMapper
カスタマイズの何らかの形式。最も単純なケースでは、JacksonModule
実装です。クライアント側のサポートが表現内のリンクを検出できるようにするための
LinkDiscoverer
実装。Spring HATEOAS がカスタム実装を見つけてそれを取得できるようにする、ちょっとしたインフラストラクチャ構成。
4.8.1. カスタムメディア型の構成
カスタムメディア型の実装は、アプリケーションコンテキストをスキャンして HypermediaMappingInformation
インターフェースの実装を探すことにより、Spring HATEOAS によって取得されます。各メディア型は、次の目的でこのインターフェースを実装する必要があります。
WebClient
、WebTestClient
、またはRestTemplate
インスタンスに適用されます。Spring Web MVC および Spring WebFlux コントローラーからのメディア型の提供をサポートします。
独自のメディア型を定義するのは、次のように簡単です。
@Configuration
public class MyMediaTypeConfiguration implements HypermediaMappingInformation {
@Override
public List<MediaType> getMediaTypes() {
return Collections.singletonList(MediaType.parseMediaType("application/vnd-acme-media-type")); (1)
}
@Override
public Module getJacksonModule() {
return new Jackson2MyMediaTypeModule(); (2)
}
@Bean
MyLinkDiscoverer myLinkDiscoverer() {
return new MyLinkDiscoverer(); (3)
}
}
1 | 構成クラスは、サポートするメディア型を返します。これは、サーバー側とクライアント側の両方のシナリオに当てはまります。 |
2 | getJacksonModule() をオーバーライドして、カスタムシリアライザーを提供し、メディア型固有の表現を作成します。 |
3 | また、クライアント側のさらなるサポートのためにカスタム LinkDiscoverer 実装を宣言します。 |
Jackson モジュールは通常、表現モデル型 RepresentationModel
、EntityModel
、CollectionModel
、PagedModel
の Serializer
および Deserializer
実装を宣言します。Jackson ObjectMapper
(カスタム HandlerInstantiator
など) をさらにカスタマイズする必要がある場合は、代わりに configureObjectMapper(…)
をオーバーライドできます。
以前のバージョンのリファレンスドキュメントでは、 |
4.8.2. 推奨
メディア型表現を実装するための推奨される方法は、期待される形式に一致し、そのまま Jackson によって直列化できる型階層を提供することです。RepresentationModel
用に登録された Serializer
および Deserializer
実装では、インスタンスをメディア型固有のモデル型に変換し、それらの Jackson シリアライザーを検索します。
デフォルトでサポートされているメディア型は、サードパーティの実装と同じ構成メカニズムを使用します。mediatype
パッケージ [GitHub] (英語) の実装を調べる価値があります。組み込みのメディア型の実装では、構成クラスパッケージがプライベートに保たれることに注意してください。カスタム実装では、ユーザーがアプリケーションパッケージからこれらの構成クラスをインポートできるようにするために、おそらく公開する必要があります。
5. 構成
このセクションでは、Spring HATEOAS を構成する方法について説明します。
5.1. @EnableHypermediaSupport
を使用する
RepresentationModel
サブ型をさまざまなハイパーメディア表現型の仕様に従ってレンダリングできるようにするために、@EnableHypermediaSupport
を介して特定のハイパーメディア表現形式のサポートを有効にすることができます。アノテーションは、引数として HypermediaType
列挙を受け取ります。現在、HAL [IETF] (英語) とデフォルトのレンダリングをサポートしています。アノテーションを使用すると、次のことがトリガーされます。
EntityModel
およびCollectionModel
をハイパーメディア固有の形式でレンダリングするために必要な Jackson モジュールを登録します。JSONPath がクラスパスにある場合、
LinkDiscoverer
インスタンスを自動的に登録して、プレーンな JSON 表現でrel
によってリンクを検索します (LinkDiscoverer
インスタンスの使用を参照)。デフォルトでは、エンティティリンクを有効にし、
EntityLinks
実装を自動的に取得して、オートワイヤできるDelegatingEntityLinks
インスタンスにバンドルします。ApplicationContext
内のすべてのRelProvider
実装を自動的にピックアップし、オートワイヤーできるDelegatingRelProvider
にバンドルします。ドメイン型で@Relation
と Spring MVC コントローラーを考慮するプロバイダーを登録します。EVO インフレクター [GitHub] (英語) がクラスパスにある場合、コレクションrel
値は、ライブラリに実装されている複数形化アルゴリズムを使用して導出されます ( [spis.rel- プロバイダー ] を参照)。
5.1.1. 専用 Web スタックのサポートを明示的に有効にする
デフォルトでは、@EnableHypermediaSupport
は、使用している Web アプリケーションスタックを反射的に検出し、それらに登録されている Spring コンポーネントにフックして、ハイパーメディア表現のサポートを有効にします。ただし、特定のスタックのサポートを明示的に有効にしたい場合もあります。例: Spring WebMVC ベースのアプリケーションが WebFlux の WebClient
を使用して発信リクエストを行い、そのリクエストがハイパーメディア要素で動作することが想定されていない場合、構成で WebMVC を明示的に宣言することにより、有効にする機能を制限できます。
@EnableHypermediaSupport(…, stacks = WebStack.WEBMVC)
class MyHypermediaConfiguration { … }
6. クライアント側のサポート
このセクションでは、クライアントに対する Spring HATEOAS のサポートについて説明します。
6.1. トラバーソン
Spring HATEOAS は、クライアント側のサービストラバーサル用の API を提供します。トラバーソン JavaScript ライブラリ (英語) にインスパイアされています。次の例は、その使用方法を示しています。
Map<String, Object> parameters = new HashMap<>();
parameters.put("user", 27);
Traverson traverson = new Traverson(URI.create("http://localhost:8080/api/"), MediaTypes.HAL_JSON);
String name = traverson
.follow("movies", "movie", "actor").withTemplateParameters(parameters)
.toObject("$.name");
Traverson
インスタンスを REST サーバーにポイントし、Accept
ヘッダーとして設定するメディア型を構成することで、Traverson
インスタンスをセットアップできます。次に、発見して追跡する関係名を定義できます。リレーション名は、単純な名前または JSONPath 式 ( $
で始まる) のいずれかです。
次に、サンプルはパラメーターマップを Traverson
インスタンスに渡します。パラメーターは、トラバーサル中に見つかった (テンプレート化された) URI を展開するために使用されます。走査は、最終走査の表現にアクセスすることによって終了します。前の例では、アクターの名前にアクセスするために JSONPath 式を評価します。
前の例は、トラバーサルの最も単純なバージョンです。rel
値は文字列であり、各ホップで同じテンプレートパラメーターが適用されます。
各レベルでテンプレートパラメーターをカスタマイズするためのオプションがさらにあります。次の例は、これらのオプションを示しています。
ParameterizedTypeReference<EntityModel<Item>> resourceParameterizedTypeReference = new ParameterizedTypeReference<EntityModel<Item>>() {};
EntityModel<Item> itemResource = traverson.//
follow(rel("items").withParameter("projection", "noImages")).//
follow("$._embedded.items[0]._links.self.href").//
toObject(resourceParameterizedTypeReference);
静的 rel(…)
関数は、単一の Hop
を定義する便利な方法です。.withParameter(key, value)
を使用すると、URI テンプレート変数の指定が簡単になります。
.withParameter() は、連鎖可能な新しい Hop オブジェクトを返します。.withParameter は好きなだけつなげることができます。結果は、単一の Hop 定義です。次の例は、その方法の 1 つを示しています。 |
ParameterizedTypeReference<EntityModel<Item>> resourceParameterizedTypeReference = new ParameterizedTypeReference<EntityModel<Item>>() {};
Map<String, Object> params = Collections.singletonMap("projection", "noImages");
EntityModel<Item> itemResource = traverson.//
follow(rel("items").withParameters(params)).//
follow("$._embedded.items[0]._links.self.href").//
toObject(resourceParameterizedTypeReference);
.withParameters(Map)
を使用して、パラメーターの Map
全体をロードすることもできます。
follow() は連鎖可能です。つまり、前の例に示すように、複数のホップをつなぎ合わせることができます。複数の文字列ベースの rel 値 (follow("items", "item") ) または特定のパラメーターを持つ単一のホップを配置できます。 |
6.1.1. EntityModel<T>
対 CollectionModel<T>
これまでに示した例は、Java の型消去を回避し、単一の JSON 形式のリソースを EntityModel<Item>
オブジェクトに変換する方法を示しています。しかし、\_embedded
HAL コレクションのようなコレクションを取得するとどうなるでしょうか ? 次の例に示すように、わずかに微調整するだけでこれを行うことができます。
CollectionModelType<Item> collectionModelType =
TypeReferences.CollectionModelType<Item>() {};
CollectionModel<Item> itemResource = traverson.//
follow(rel("items")).//
toObject(collectionModelType);
単一のリソースを取得する代わりに、これはコレクションを CollectionModel
に逆直列化します。
6.2. LinkDiscoverer
インスタンスの使用
ハイパーメディア対応の表現で作業する場合、一般的なタスクは、特定の関係型とのリンクを見つけることです。Spring HATEOAS は、既定の表現レンダリングまたは HAL のいずれかをすぐに使用できるように、LinkDiscoverer
インターフェースの JSONPath (英語) ベースの実装を提供します。@EnableHypermediaSupport
を使用すると、設定されたハイパーメディア型をサポートするインスタンスが Spring Bean として自動的に公開されます。
または、次のようにインスタンスをセットアップして使用することもできます。
String content = "{'_links' : { 'foo' : { 'href' : '/foo/bar' }}}";
LinkDiscoverer discoverer = new HalLinkDiscoverer();
Link link = discoverer.findLinkWithRel("foo", content);
assertThat(link.getRel(), is("foo"));
assertThat(link.getHref(), is("/foo/bar"));
6.3. WebClient インスタンスの構成
ハイパーメディアを話すように WebClient
を構成する必要がある場合は、簡単です。以下に示すように、HypermediaWebClientConfigurer
を手に入れます。
WebClient
を構成する @Bean
WebClient.Builder hypermediaWebClient(HypermediaWebClientConfigurer configurer) { (1)
return configurer.registerHypermediaTypes(WebClient.builder()); (2)
}
1 | @Configuration クラス内で、HypermediaWebClientConfigurer Bean Spring HATEOAS レジスターのコピーを取得します。 |
2 | WebClient.Builder を作成したら、configurer を使用してハイパーメディア型を登録します。 |
HypermediaWebClientConfigurer の機能 すべての適切なエンコーダーとデコーダーを WebClient.Builder に登録します。これを利用するには、ビルダーをアプリケーションのどこかに挿入し、build() メソッドを実行して WebClient を生成する必要があります。 |
Spring Boot を使用している場合は、別の方法があります: WebClientCustomizer
です。
@Bean (4)
WebClientCustomizer hypermediaWebClientCustomizer(HypermediaWebClientConfigurer configurer) { (1)
return webClientBuilder -> { (2)
configurer.registerHypermediaTypes(webClientBuilder); (3)
};
}
1 | Spring Bean を作成する場合、Spring HATEOAS の HypermediaWebClientConfigurer Bean のコピーをリクエストします。 |
2 | Java 8 ラムダ式を使用して WebClientCustomizer を定義します。 |
3 | 関数呼び出し内で、registerHypermediaTypes メソッドを適用します。 |
4 | 全体を Spring Bean として返し、Spring Boot がそれを取得して、自動構成された WebClient.Builder Bean に適用できるようにします。 |
この段階では、具体的な WebClient
が必要なときはいつでも、コードに WebClient.Builder
を挿入し、build()
を使用してください。WebClient
インスタンスは、ハイパーメディアを使用して対話できます。
6.4. WebTestClient
インスタンスの構成
ハイパーメディア対応の表現を扱う場合、一般的なタスクは WebTestClient
を使用してさまざまなテストを実行することです。
テストケースで WebTestClient
のインスタンスを構成するには、次の例を確認してください。
WebTestClient
の構成 @Test // #1225
void webTestClientShouldSupportHypermediaDeserialization() {
// Configure an application context programmatically.
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(HalConfig.class); (1)
context.refresh();
// Create an instance of a controller for testing
WebFluxEmployeeController controller = context.getBean(WebFluxEmployeeController.class);
controller.reset();
// Extract the WebTestClientConfigurer from the app context.
HypermediaWebTestClientConfigurer configurer = context.getBean(HypermediaWebTestClientConfigurer.class);
// Create a WebTestClient by binding to the controller and applying the hypermedia configurer.
WebTestClient client = WebTestClient.bindToApplicationContext(context).build().mutateWith(configurer); (2)
// Exercise the controller.
client.get().uri("http://localhost/employees").accept(HAL_JSON) //
.exchange() //
.expectStatus().isOk() //
.expectBody(new TypeReferences.CollectionModelType<EntityModel<Employee>>() {}) (3)
.consumeWith(result -> {
CollectionModel<EntityModel<Employee>> model = result.getResponseBody(); (4)
// Assert against the hypermedia model.
assertThat(model.getRequiredLink(IanaLinkRelations.SELF)).isEqualTo(Link.of("http://localhost/employees"));
assertThat(model.getContent()).hasSize(2);
});
}
1 | @EnableHypermediaSupport を使用する構成クラスを登録して、HAL サポートを有効にします。 |
2 | HypermediaWebTestClientConfigurer を使用して、ハイパーメディアサポートを適用します。 |
3 | Spring HATEOAS の TypeReferences.CollectionModelType ヘルパーを使用して CollectionModel<EntityModel<Employee>> のレスポンスを求めます。 |
4 | Spring HATEOAS 形式の「本体」を取得したら、それに対してアサート ! |
WebTestClient は不変の値型であるため、その場で変更することはできません。HypermediaWebClientConfigurer は変異したバリアントを返します。これを使用するには、それをキャプチャーする必要があります。 |
Spring Boot を使用している場合は、次のような追加のオプションがあります。
WebTestClient
の構成 @SpringBootTest
@AutoConfigureWebTestClient (1)
class WebClientBasedTests {
@Test
void exampleTest(@Autowired WebTestClient.Builder builder, @Autowired HypermediaWebTestClientConfigurer configurer) { (2)
client = builder.apply(configurer).build(); (3)
client.get().uri("/") //
.exchange() //
.expectBody(new TypeReferences.EntityModelType<Employee>() {}) (4)
.consumeWith(result -> {
// assert against this EntityModel<Employee>!
});
}
}
1 | これは、このテストクラスの WebTestClient.Builder を構成する Spring Boot のテストアノテーションです。 |
2 | Spring Boot の WebTestClient.Builder を builder および Spring HATEOAS の構成プログラムにメソッドパラメーターとして Autowire します。 |
3 | ハイパーメディアのサポートを登録するには、HypermediaWebTestClientConfigurer を使用します。 |
4 | TypeReferences を使用して EntityModel<Employee> を返したいというシグナル。 |
ここでも、前の例と同様のアサーションを使用できます。
テストケースを作成する方法は他にもたくさんあります。WebTestClient
は、コントローラー、関数、URL にバインドできます。このセクションは、そのすべてを示すことを意図したものではありません。代わりに、これは開始するためのいくつかの例を提供します。重要なことは、HypermediaWebTestClientConfigurer
を適用することにより、ハイパーメディアを処理するために WebTestClient
の任意のインスタンスを変更できることです。
6.5. RestTemplate インスタンスの構成
ハイパーメディアを話すように構成された RestTemplate
の独自のコピーを作成する場合は、HypermediaRestTemplateConfigurer
を使用できます。
RestTemplate
を自分で構成する /**
* Use the {@link HypermediaRestTemplateConfigurer} to configure a {@link RestTemplate}.
*/
@Bean
RestTemplate hypermediaRestTemplate(HypermediaRestTemplateConfigurer configurer) { (1)
return configurer.registerHypermediaTypes(new RestTemplate()); (2)
}
1 | @Configuration クラス内で、HypermediaRestTemplateConfigurer Bean Spring HATEOAS レジスターのコピーを取得します。 |
2 | RestTemplate を作成したら、configurer を使用してハイパーメディア型を適用します。 |
このパターンは、必要な RestTemplate
の任意のインスタンスに自由に適用できます。これは、登録済みの Bean を作成する場合でも、定義したサービス内でも適用できます。
Spring Boot を使用している場合は、別のアプローチがあります。
一般に、Spring Boot は、アプリケーションコンテキストで RestTemplate
Bean を登録するという概念から離れました。
さまざまなサービスと通信する場合、さまざまな資格情報が必要になることがよくあります。
RestTemplate
が基礎となる接続プールを使用する場合、追加の問題が発生します。ユーザーは、単一の Bean ではなく、異なるインスタンスを必要とすることがよくあります。
これを補うために、Spring Boot は RestTemplateBuilder
を提供します。この自動構成された Bean により、RestTemplate
インスタンスの作成に使用されるさまざまな Bean を定義できます。RestTemplateBuilder
Bean を要求し、その build()
メソッドを呼び出してから、最終設定 (資格情報やその他の詳細など) を適用します。
ハイパーメディアベースのメッセージコンバーターを登録するには、コードに以下を追加します。
@Bean (4)
RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { (1)
return restTemplate -> { (2)
configurer.registerHypermediaTypes(restTemplate); (3)
};
}
1 | Spring Bean を作成する場合、Spring HATEOAS の HypermediaRestTemplateConfigurer Bean のコピーをリクエストします。 |
2 | Java 8 ラムダ式を使用して RestTemplateCustomizer を定義します。 |
3 | 関数呼び出し内で、registerHypermediaTypes メソッドを適用します。 |
4 | 全体を Spring Bean として返し、Spring Boot がそれを取得して、自動構成された RestTemplateBuilder に適用できるようにします。 |
この段階では、具体的な RestTemplate
が必要なときはいつでも、コードに RestTemplateBuilder
を挿入し、build()
を使用してください。RestTemplate
インスタンスは、ハイパーメディアを使用して対話できます。