このプロジェクトは、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 型参照を新しいものに置き換えます。

例 1: 移行スクリプトのサンプルアプリケーション
$ ./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 の基本とその基本的なドメインの抽象化について説明します。

ハイパーメディアの基本的な考え方は、リソースの表現をハイパーメディア要素で豊かにすることです。その最も単純な形式はリンクです。それらは、クライアントが特定のリソースに移動できることを示します。関連リソースのセマンティクスは、いわゆるリンク関係で定義されます。HTML ファイルのヘッダーですでにこれを見たことがあるかもしれません:

例 2: HTML ドキュメント内のリンク
<link href="theme.css" rel="stylesheet" type="text/css" />

ご覧のとおり、リンクはリソース theme.css を指しており、それがスタイルシートであることを示しています。リンクには、リソースが指すメディア型などの追加情報が含まれていることがよくあります。ただし、リンクの基本的な構成要素は、その参照と関係です。

Spring HATEOAS を使用すると、不変の Link 値型を介してリンクを操作できます。そのコンストラクターはハイパーテキスト参照とリンク関係の両方を取り、後者はデフォルトで IANA リンク関係 self に設定されています。後者についてはリンク関係を参照してください。

例 3: リンクの使用
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 に変換できます。変数の名前だけを知る必要があります。

例 4: テンプレート化された 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");
1Link インスタンスは、テンプレート化されていることを示します。つまり、URI テンプレートが含まれています。
2 テンプレートに含まれるパラメーターを公開します。
3 パラメーターの拡張が可能です。

URI テンプレートは手動で作成でき、後でテンプレート変数を追加できます。

例 5: URI テンプレートの操作
UriTemplate template = UriTemplate.of("/{segment}/something")
  .with(new TemplateVariable("parameter", VariableType.REQUEST_PARAM);

assertThat(template.toString()).isEqualTo("/{segment}/something{?parameter}");

ターゲットリソースと現在のリソースの関連を示すには、いわゆるリンク関連が使用されます。Spring HATEOAS は、String ベースのインスタンスを簡単に作成するための LinkRelation 型を提供します。

Internet Assigned Numbers Authority には、定義済みの一連のリンク関係 (英語) が含まれています。それらは IanaLinkRelations 経由で参照できます。

例 6: IANA リンク関係の使用
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 のコレクションのコンテナーであり、モデルに追加するための便利なメソッドがあります。モデルは、ハイパーメディア要素が表現でどのように見えるかを定義するさまざまなメディア型 フォーマットに後でレンダリングできます。詳細については、メディアの種類を参照してください。

例 7: RepresentationModel クラス階層
diagram classes

RepresentationModel を操作するデフォルトの方法は、そのサブクラスを作成して、表現に含まれると想定されるすべてのプロパティを含め、そのクラスのインスタンスを作成し、プロパティを設定し、リンクで強化することです。

例 8: サンプル表現モデル型
class PersonModel extends RepresentationModel<PersonModel> {

  String firstname, lastname;
}

RepresentationModel.add(…) が自身のインスタンスを返せるようにするには、一般的な自己型指定が必要です。モデル型は次のように使用できるようになりました。

例 9: 人物表現モデルの使用
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 ヘッダーを送信した場合、レスポンスは次のようになります。

例 10: 人物表現モデル用に生成された HAL 表現
{
  "_links" : {
    "self" : {
      "href" : "https://myhost/people/42"
    }
  },
  "firstname" : "Dave",
  "lastname" : "Matthews"
}

2.4.1. アイテムのリソース表現モデル

単一のオブジェクトまたは概念に基づくリソースの場合、便利な EntityModel 型が存在します。コンセプトごとにカスタムモデル型を作成する代わりに、既存の型を再利用して、そのインスタンスを EntityModel にラップすることができます。

例 11: EntityModel を使用して既存のオブジェクトをラップする
Person person = new Person("Dave", "Matthews");
EntityModel<Person> model = EntityModel.of(person);

2.4.2. コレクションのリソース表現モデル

概念的にコレクションであるリソースの場合、CollectionModel を使用できます。その要素は、単純なオブジェクトまたは RepresentationModel インスタンスのいずれかになります。

例 12: 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. サーバー側のサポート

これでドメインボキャブラリの準備は整いましたが、主な課題が残っています。それは、脆弱性の低いメソッドで 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 を提供するようになりました。次の例は、その方法を示しています。

import static org.sfw.hateoas.server.mvc.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);

メソッドを指すリンクを作成したり、ダミーのコントローラーメソッド呼び出しを作成したりすることもできます。最初のアプローチは、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&param=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 でのみその非複合スタイルを導入したためです。今日ゼロから始めた場合、デフォルトでそのスタイルを使用し、ユーザーが明示的に複合スタイルを選択できるようにするのではなく、複合スタイルを明示的に選択できるようにするでしょう。

TODO

3.3. アフォーダンス

環境のアフォーダンスは、それが提供するものです... 善悪を問わず、環境が提供または提供するものです。動詞の「余裕」は辞書にありますが、名詞の「アフォーダンス」はありません。それを作りました。

— ジェームズ・ J ・ギブソン
視覚に対する生態学的アプローチ (126 ページ)

REST ベースのリソースは、データだけでなくコントロールも提供します。柔軟なサービスを形成するための最後の要素は、さまざまなコントロールの使用方法に関する詳細なアフォーダンスです。アフォーダンスはリンクに関連付けられているため、Spring HATEOAS は、必要な数の関連メソッドをリンクにアタッチするための API を提供します。Spring MVC コントローラーメソッド (詳細については Spring MVC でのリンクの構築を参照) を指定してリンクを作成できるのと同じように、…

次のコードは、セルフリンクを取得し、さらに 2 つのアフォーダンスを関連付ける方法を示しています。

例 13: アフォーダンスを 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 セルフリンクを作成します。
2updateEmployee メソッドを self リンクに関連付けます。
3partiallyUpdateEmployee メソッドを self リンクに関連付けます。

.andAffordance(afford(…​)) を使用すると、コントローラーのメソッドを使用して PUT および PATCH 操作を GET 操作に接続できます。上記の関連メソッドが次のようになっていると想像してください。

例 14: PUT /employees/{id} に対応する updateEmpoyee 方式
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee( //
    @RequestBody EntityModel<Employee> employee, @PathVariable Integer id)
例 15: 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 を使用して実現できます。

例 16: 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 を登録します。

アフォーダンスは、一般的なアフォーダンスメタデータを特定の表現に変換するメディア型固有のアフォーダンスモデルによって支えられています。そのメタデータの公開を制御する方法の詳細については、メディアの種類セクションのアフォーダンスのセクションを確認してください。

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 として簡単に宣言できます。

例 17: ForwardedHeaderFilter の登録
@Bean
ForwardedHeaderFilter forwardedHeaderFilter() {
    return new ForwardedHeaderFilter();
}

これにより、すべての X-Forwarded- …  ヘッダーを処理するサーブレットフィルターが作成されます。そして、それをサーブレットハンドラーに適切に登録します。

Spring WebFlux アプリケーションの場合、対応するリアクティブは ForwardedHeaderTransformer です。

例 18: ForwardedHeaderTransformer の登録
@Bean
ForwardedHeaderTransformer forwardedHeaderTransformer() {
    return new ForwardedHeaderTransformer();
}

これにより、リアクティブ Web リクエストを変換し、X-Forwarded- …  ヘッダーを処理する関数が作成されます。そして、WebFlux に正しく登録します。

上記のように構成すると、X-Forwarded- …  ヘッダーを渡すリクエストは、生成されたリンクに反映されたものを確認します。

例 19: X-Forwarded- …  ヘッダーを使用したリクエスト
curl -v localhost:8080/employees \
    -H 'X-Forwarded-Proto: https' \
    -H 'X-Forwarded-Host: example.com' \
    -H 'X-Forwarded-Port: 9001'
例 20: それらのヘッダーを考慮するために生成されたリンクを含む対応するレスポンス
{
  "_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"
    }
  }
}
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 を構成に挿入します。
2API を使用して、コントローラークラスの代わりにエンティティ型を使用してリンクを作成します。

ご覧のとおり、OrderController を明示的に参照しなくても、Order インスタンスを管理するリソースを参照できます。

3.5.2. EntityLinks API の詳細

基本的に、EntityLinks を使用すると、LinkBuilder および Link インスタンスを構築して、エンティティ型のリソースをコレクションおよびアイテムにすることができます。linkFor …  で始まるメソッドは、LinkBuilder インスタンスを生成し、追加のパスセグメントやパラメーターなどで拡張および拡張できます。linkTo で始まるメソッドは、完全に準備された Link インスタンスを生成します。

コレクションリソースの場合はエンティティ型を提供するだけで十分ですが、アイテムリソースへのリンクには識別子を指定する必要があります。これは通常、次のようになります。

例 21: アイテムリソースへのリンクを取得する
entityLinks.linkToItemResource(order, order.getId());

これらのメソッド呼び出しを繰り返していることに気付いた場合は、識別子抽出ステップを再利用可能な Function にプルして、さまざまな呼び出しで再利用できます。

Function<Order, Object> idExtractor = Order::getId; (1)

entityLinks.linkToItemResource(order, idExtractor); (2)
1 識別子の抽出は、フィールドまたは定数に保持できるように外部化されます。
2 エクストラクターを使用したリンクルックアップ。
TypedEntityLinks

コントローラーの実装はエンティティ型ごとにグループ化されることが多いため、コントローラークラス全体で同じ抽出関数 (詳細については EntityLinks API の詳細を参照) を使用していることに気付くことがよくあります。エクストラクターを提供する TypedEntityLinks インスタンスを一度取得することで、識別子抽出ロジックをさらに一元化できるため、実際のルックアップで抽出を処理する必要がまったくなくなります。

例 22: 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)
  }
}
1EntityLinks インスタンスを注入します。
2 特定の識別子抽出関数を使用して Order インスタンスを検索することを示します。
3 唯一の Order インスタンスに基づいてアイテムリソースリンクを検索します。

3.5.3. SPI としての EntityLinks

@EnableHypermediaSupport によって作成された EntityLinks インスタンスの型は DelegatingEntityLinks であり、ApplicationContext で Bean として使用可能な他のすべての EntityLinks 実装を取得します。これはプライマリ Bean として登録されているため、一般的に EntityLinks を注入するときは常に唯一の注入候補になります。ControllerEntityLinks はセットアップに含まれるデフォルトの実装ですが、ユーザーは自由に独自の実装を実装および登録できます。これらを EntityLinks インスタンスでインジェクションに使用できるようにするには、実装を Spring Bean として登録するだけです。

例 23: カスタム EntityLinks 実装の宣言
@Configuration
class CustomEntityLinksConfiguration {

  @Bean
  MyEntityLinks myEntityLinks(…) {
    return new MyEntityLinks(…);
  }
}

このメカニズムの拡張性の例は、Spring Data REST の RepositoryEntityLinks [GitHub] (英語) です。これは、リポジトリマッピング情報を使用して、Spring Data リポジトリによってサポートされるリソースを指すリンクを作成します。同時に、他の型のリソース用の追加のルックアップメソッドも公開します。これらを利用したい場合は、明示的に RepositoryEntityLinks を注入するだけです。

3.6. 代表モデルアセンブラ

エンティティから表現モデルへのマッピングは複数の場所で使用する必要があるため、それを担当する専用のクラスを作成することは理にかなっています。変換には非常にカスタムな手順が含まれていますが、定型的な手順もいくつか含まれています。

  1. モデルクラスのインスタンス化

  2. レンダリングされるリソースを指す 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") が表示されます。
2URI はプロセッサーによって提供されました。

この例は非常に単純ですが、次のことが簡単にできます。

  • WebMvcLinkBuilder または WebFluxLinkBuilder を使用して、PaymentController への動的リンクを構築します。

  • 状態によって駆動される他のリンク (例: cancelamend) を条件付きで追加するために必要なサービスを注入します。

  • Spring Security などの横断的サービスを活用して、現在のユーザーのコンテキストに基づいてリンクを追加、削除、修正します。

また、この例では、PaymentProcessor は提供された EntityModel<Order> を変更します。また、それを別のオブジェクトに置き換える力もあります。API では、戻り値の型が入力型と同じである必要があることに注意してください。

3.7.1. 空のコレクションモデルの処理

RepresentationModel インスタンスに対して呼び出す RepresentationModelProcessor インスタンスの適切なセットを見つけるために、呼び出し元のインフラストラクチャは、登録されている RepresentationModelProcessor のジェネリクス宣言の詳細な分析を実行します。CollectionModel インスタンスの場合、これには基になるコレクションの要素のインスペクションが含まれます。これは、実行時に唯一のモデルインスタンスがジェネリクス情報を公開しないためです (Java の型消去のため)。つまり、デフォルトでは、RepresentationModelProcessor インスタンスは空のコレクションモデルに対して呼び出されません。インフラストラクチャがペイロード型を正しく推測できるようにするには、空の CollectionModel インスタンスを最初から明示的なフォールバックペイロード型で初期化するか、CollectionModel.withFallbackType(…) を呼び出して登録します。詳細については、コレクションのリソース表現モデルを参照してください。

3.8.  LinkRelationProvider API の使用

リンクを作成するときは、通常、リンクに使用する関係型を決定する必要があります。ほとんどの場合、関係型は (ドメイン) 型に直接関連付けられています。詳細なアルゴリズムをカプセル化して、LinkRelationProvider API の背後にある関係型を検索し、単一およびコレクションリソースの関係型を決定できるようにします。リレーション型を検索するアルゴリズムは次のとおりです。

  1. 型に @Relation のアノテーションが付けられている場合、アノテーションで構成された値を使用します。

  2. そうでない場合は、コレクション rel の追加された List に加えて、大文字ではない単純なクラス名がデフォルトになります。

  3. EVO インフレクター [GitHub] (英語) JAR がクラスパスにある場合、複数形化アルゴリズムによって提供される単一リソース rel の複数形を使用します。

  4.  @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 を提供しています。これらはその基本的な仮定です:

  1. HAL 表現は、表現に含まれるドメインフィールドを構築する任意のオブジェクト (エンティティ) によってサポートできます。

  2. 表現は、任意のオブジェクトまたは HAL 表現自体 (つまり、ネストされた埋め込みとリンクを含む) のいずれかである、さまざまな埋め込みドキュメントによって強化できます。

  3. 特定の 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 つ以上のリンクを持つ特定のリンク関係の場合、仕様は表現に関して明確です。

例 24: 1 つのリレーションに 2 つのリンクが関連付けられた HAL ドキュメント
{
  "_links": {
    "item": [
      { "href": "https://myhost/cart/42" },
      { "href": "https://myhost/inventory/12" }
    ]
  },
  "customer": "Dave Matthews"
}

しかし、特定のリレーションに対してリンクが 1 つしかない場合、仕様はあいまいです。それを単一のオブジェクトまたは単一項目の配列としてレンダリングできます。

デフォルトでは、Spring HATEOAS は最も簡潔なアプローチを使用し、次のような単一リンクの関係をレンダリングします。

例 25: オブジェクトとしてレンダリングされた 1 つのリンクを含む HAL ドキュメント
{
  "_links": {
    "item": { "href": "https://myhost/inventory/12" }
  },
  "customer": "Dave Matthews"
}

一部のユーザーは、HAL を使用するときに配列とオブジェクトを切り替えたくない場合があります。彼らはこの型のレンダリングを好みます:

例 26: 配列としてレンダリングされた 1 つのリンクを含む HAL
{
  "_links": {
    "item": [{ "href": "https://myhost/inventory/12" }]
  },
  "customer": "Dave Matthews"
}

このポリシーをカスタマイズする場合は、アプリケーション構成に HalConfiguration Bean を挿入するだけです。複数の選択肢があります。

例 27: グローバル HAL シングルリンクレンダリングポリシー
@Bean
public HalConfiguration globalPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinks(RenderSingleLinks.AS_ARRAY); (1)
}
1 すべての単一リンク関係を配列としてレンダリングすることにより、Spring HATEOAS のデフォルトをオーバーライドします。

特定のリンク関係のみをオーバーライドしたい場合は、次のように HalConfiguration Bean を作成できます。

例 28: リンク関係ベースの HAL シングルリンクレンダリングポリシー
@Bean
public HalConfiguration linkRelationBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          IanaLinkRelations.ITEM, RenderSingleLinks.AS_ARRAY) (1)
      .withRenderSingleLinksFor( //
          LinkRelation.of("prev"), RenderSingleLinks.AS_SINGLE); (2)
}
1item リンク関係を常に配列としてレンダリングします。
2 リンクが 1 つしかない場合、prev リンク関係をオブジェクトとしてレンダリングします。

これらのいずれもニーズに合わない場合は、Ant スタイルのパスパターンを使用できます。

例 29: パターンベースの HAL シングルリンクレンダリングポリシー
@Bean
public HalConfiguration patternBasedPolicy() {
  return new HalConfiguration() //
      .withRenderSingleLinksFor( //
          "http*", RenderSingleLinks.AS_ARRAY); (1)
}
1http で始まるすべてのリンク関係を配列としてレンダリングします。
パターンベースのアプローチでは、Spring の AntPathMatcher を使用します。

これらすべての HalConfiguration ウィザーを組み合わせて、1 つの包括的なポリシーを形成できます。驚きを避けるために、API を広範囲にテストしてください。

4.1.3. リンクタイトルの国際化

HAL は、そのリンクオブジェクトの title 属性を定義します。これらのタイトルは、クライアントが UI で直接使用できるように、Spring のリソースバンドルの抽象化と rest-messages という名前のリソースバンドルを使用して設定できます。このバンドルは自動的にセットアップされ、HAL リンクの直列化中に使用されます。

リンクのタイトルを定義するには、キーテンプレート _links.$relationName.title を次のように使用します。

例 30: サンプル rest-messages.properties
_links.cancel.title=Cancel order
_links.payment.title=Proceed to checkout

これにより、次の HAL 表現が生成されます。

例 31: リンクタイトルが定義されたサンプル 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 仕様

このメディア型を有効にするには、コードに次の構成を追加します。

例 32: HAL-FORMS 対応アプリケーション
@Configuration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class HalFormsApplication {

}

クライアントが application/prs.hal-forms+json で Accept ヘッダーを提供するときはいつでも、次のようなことが期待できます。

例 33: HAL-FORMS サンプルドキュメント
{
  "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 では、入力型と出力型のモデル型を整形し、アノテーションを使用することで、カスタマイズできます。

各テンプレートには、次の属性が定義されています。

表 1: テンプレート属性
属性 説明

contentType

サーバーが受信すると予想されるメディアの種類。指すコントローラーメソッドが @RequestMapping(consumes = " … ") 属性を公開する場合、またはアフォーダンスの設定時にメディア型が明示的に定義された場合にのみ含まれます。

method

テンプレートを送信するときに使用する HTTP メソッド。

target

フォームの送信先のターゲット URI。アフォーダンスターゲットが宣言されたリンクと異なる場合にのみレンダリングされます。

title

テンプレートを表示するときの人間が読めるタイトル。

properties

フォームで送信するすべてのプロパティ (以下を参照)。

各プロパティは、定義された次の属性を取得します。

表 2: プロパティ属性
属性 説明

readOnly

プロパティに setter メソッドがない場合は、true に設定します。それが存在する場合は、アクセサーまたはフィールドで明示的に Jackson の @JsonProperty(Access.READ_ONLY) を使用します。デフォルトではレンダリングされないため、デフォルトで false になります。

regex

フィールドまたは型のいずれかで JSR-303 の @Pattern アノテーションを使用してカスタマイズできます。後者の場合、パターンはその特定の型として宣言されたすべてのプロパティに使用されます。デフォルトではレンダリングされません。

required

JSR-303 の @NotNull を使用してカスタマイズできます。デフォルトではレンダリングされないため、デフォルトで false になります。メソッドとして PATCH を使用するテンプレートでは、すべてのプロパティが自動的に不要に設定されます。

max

プロパティに許可される最大値。JSR-303 の @Size、Hibernate Validator の @Range または JSR-303 の @Max および @DecimalMax アノテーションから派生。

maxLength

プロパティに許可される最大長の値。Hibernate Validator の @Length アノテーションから派生。

min

プロパティに許可される最小値。JSR-303 の @Size、Hibernate Validator の @Range または JSR-303 の @Min および @DecimalMin アノテーションから派生します。

minLength

プロパティに許可される最小の長さの値。Hibernate Validator の @Length アノテーションから派生。

options

フォームを送信するときに値を選択するオプション。詳細については、プロパティの HAL-FORMS オプションの定義を参照してください。

prompt

フォーム入力をレンダリングするときに使用する、ユーザーが読み取り可能なプロンプト。詳細については、プロパティプロンプトを参照してください。

placeholder

予想される形式の例を示す、ユーザーが読み取り可能なプレースホルダー。定義する方法はプロパティプロンプトに従いますが、サフィックス _placeholder を使用します。

type

明示的な @InputType アノテーション、JSR-303 検証アノテーション、またはプロパティの型から派生した HTML 入力型。

手動でアノテーションを付けることができない型については、アプリケーションコンテキストに存在する 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 であることに注意してください。これは、通常、アフォーダンスが説明するローカルまたは完全修飾入力型名でキーを修飾する必要があることを意味します。

例 34: HAL-FORMS テンプレートのタイトルの定義
_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)
1default をキーとして使用するタイトルのグローバル定義。
2 実際のアフォーダンス名をキーとして使用するタイトルのグローバル定義。アフォーダンスの作成時に明示的に定義されていない限り、これはアフォーダンスの作成時に指定されたメソッドの名前にデフォルト設定されます。
3Employee という名前のすべての型に適用される、ローカルで定義されたタイトル。
4 完全修飾型名を使用したタイトル定義。
実際のアフォーダンス名を使用するキーは、デフォルトのものより優先されます。
プロパティプロンプト

プロパティプロンプトは、Spring HATEOAS によって自動的に構成された rest-messages リソースバンドルを介して解決することもできます。キーは、グローバル、ローカル、完全修飾で定義でき、実際のプロパティキーに連結された ._prompt が必要です。

例 35: email プロパティのプロンプトの定義
firstName._prompt=Firstname (1)
Employee.firstName._prompt=Firstname (2)
com.acme.Employee.firstName._prompt=Firstname (3)
1firstName という名前のすべてのプロパティは、宣言されている型に関係なく、レンダリングされる "Firstname" を取得します。
2Employee という名前の型の firstName プロパティは、"Firstname" と表示されます。
3com.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 プロパティを定義します。
2ccn が正規表現に従うことを期待しています。
3JSR-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)
1createCustomer(…) メソッドを指すことによって作成されたテンプレートの明示的なタイトルを定義します。
2CustomerRepresentation モデルの 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)
      } ]
    }
  }
}
1default という名前のテンプレートが公開されています。その名前は、定義された唯一のテンプレートであり、仕様ではその名前を使用する必要があるため、default です。複数のテンプレートがアタッチされている場合 (追加のアフォーダンスを宣言することによって)、それらはそれぞれが指しているメソッドにちなんで名付けられます。
2 テンプレートのタイトルは、リソースバンドルで定義された値から派生します。リクエストとともに送信される Accept-Language ヘッダーと可用性に応じて、異なる値が返される可能性があることに注意してください。
3method 属性の値は、アフォーダンスが派生したメソッドのマッピングから派生します。
4type 属性の値 text は、プロパティの型 String から派生します。同じことが birthdate プロパティにも当てはまりますが、結果は date になります。
5ccn プロパティのプロンプトとプレースホルダーも、リソースバンドルから派生します。
6ccn プロパティの @Pattern 宣言は、テンプレートプロパティの regex 属性として公開されます。
7email プロパティの @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 レスポンスを作成できます。

Spring HATEOAS' 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());

これにより、次のようなレスポンスが得られます。

サンプルの HTTP 問題の詳細レスポンス
{
  "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 は、単一アイテムのリソースとコレクションの両方を表す統一された方法を提供します。このメディア型を有効にするには、コードに次の構成を追加します。

例 36: コレクション + JSON 対応アプリケーション
@Configuration
@EnableHypermediaSupport(type = HypermediaType.COLLECTION_JSON)
public class CollectionJsonApplication {

}

この構成により、以下に示すように、application/vnd.collection+json の Accept ヘッダーを持つリクエストにアプリケーションが応答するようになります。

仕様の次の例は、単一のアイテムを示しています。

例 37: コレクション+ JSON 単品例
{
  "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"
          }
        ]
      }
    ]
  }
}
1self リンクは、ドキュメントの href 属性に格納されます。
2 ドキュメントの上部の links セクションには、コレクションレベルのリンク (self リンクを除く) が含まれています。
3items セクションには、データのコレクションが含まれています。これは単一項目のドキュメントであるため、エントリは 1 つだけです。
4data セクションには、実際のコンテンツが含まれています。プロパティで構成されています。
5 アイテムの個別の links.

前のフラグメントは仕様から削除されました。Spring HATEOAS が EntityModel をレンダリングすると、次のようになります。

  • self リンクをドキュメントの href 属性とアイテムレベルの href 属性の両方に挿入します。

  • モデルの残りのリンクをトップレベルの links とアイテムレベルの links の両方に配置します。

  • EntityModel からプロパティを抽出し、… に変換します。

リソースのコレクションをレンダリングする場合、ドキュメントはほぼ同じですが、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 は、単一アイテムのリソースとコレクションの両方を表す統一された方法を提供します。このメディア型を有効にするには、コードに次の構成を追加します。

例 38: UBER+JSON 対応アプリケーション
@Configuration
@EnableHypermediaSupport(type = HypermediaType.UBER)
public class UberApplication {

}

この設定により、以下に示すように、アプリケーションは Accept ヘッダー application/vnd.amundsen-uber+json を使用してリクエストに応答するようになります。

例 39: UBER サンプルドキュメント
{
  "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 メソッドからそれを返します。

例 40: 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 が生成されます。

例 41: ALPS 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

Maven 座標
<dependency>
    <groupId>com.toedter</groupId>
    <artifactId>spring-hateoas-jsonapi</artifactId>
    <version>{see project page for current version}</version>
</dependency>
Gradle 座標
implementation 'com.toedter:spring-hateoas-jsonapi:{see project page for current version}'

スナップショットリリースが必要な場合は、詳細についてプロジェクトページにアクセスしてください。

4.7.2. サイレン

Maven 座標
<dependency>
    <groupId>de.ingogriebsch.hateoas</groupId>
    <artifactId>spring-hateoas-siren</artifactId>
    <version>{see project page for current version}</version>
    <scope>compile</scope>
</dependency>
Gradle 座標
implementation 'de.ingogriebsch.hateoas:spring-hateoas-siren:{see project page for current version}'

4.8. カスタムメディア型の登録

Spring HATEOAS を使用すると、SPI を介してカスタムメディア型を統合できます。このような実装の構成要素は次のとおりです。

  1. Jackson ObjectMapper カスタマイズの何らかの形式。最も単純なケースでは、Jackson Module 実装です。

  2. クライアント側のサポートが表現内のリンクを検出できるようにするための LinkDiscoverer 実装。

  3. Spring HATEOAS がカスタム実装を見つけてそれを取得できるようにする、ちょっとしたインフラストラクチャ構成。

4.8.1. カスタムメディア型の構成

カスタムメディア型の実装は、アプリケーションコンテキストをスキャンして HypermediaMappingInformation インターフェースの実装を探すことにより、Spring HATEOAS によって取得されます。各メディア型は、次の目的でこのインターフェースを実装する必要があります。

  • WebClientWebTestClient、または 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 構成クラスは、サポートするメディア型を返します。これは、サーバー側とクライアント側の両方のシナリオに当てはまります。
2getJacksonModule() をオーバーライドして、カスタムシリアライザーを提供し、メディア型固有の表現を作成します。
3 また、クライアント側のさらなるサポートのためにカスタム LinkDiscoverer 実装を宣言します。

Jackson モジュールは通常、表現モデル型 RepresentationModelEntityModelCollectionModelPagedModel の Serializer および Deserializer 実装を宣言します。Jackson ObjectMapper (カスタム HandlerInstantiator など) をさらにカスタマイズする必要がある場合は、代わりに configureObjectMapper(…) をオーバーライドできます。

以前のバージョンのリファレンスドキュメントでは、MediaTypeConfigurationProvider インターフェースの実装と spring.factories への登録についてメンションされていました。これは必要ありません。この SPI は、Spring HATEOAS によって提供されるすぐに使えるメディア型にのみ使用されます。HypermediaMappingInformation インターフェースを実装し、それを Spring Bean として登録するだけで十分です。

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 を明示的に宣言することにより、有効にする機能を制限できます。

例 42: 特定の Web スタックのハイパーメディアサポートを明示的にアクティブ化する
@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 に逆直列化します。

ハイパーメディア対応の表現で作業する場合、一般的なタスクは、特定の関係型とのリンクを見つけることです。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 を手に入れます。

例 43: 自分で WebClient を構成する
@Bean
WebClient.Builder hypermediaWebClient(HypermediaWebClientConfigurer configurer) { (1)
 return configurer.registerHypermediaTypes(WebClient.builder()); (2)
}
1@Configuration クラス内で、HypermediaWebClientConfigurer Bean Spring HATEOAS レジスターのコピーを取得します。
2WebClient.Builder を作成したら、configurer を使用してハイパーメディア型を登録します。
HypermediaWebClientConfigurer の機能 すべての適切なエンコーダーとデコーダーを WebClient.Builder に登録します。これを利用するには、ビルダーをアプリケーションのどこかに挿入し、build() メソッドを実行して WebClient を生成する必要があります。

Spring Boot を使用している場合は、別の方法があります: WebClientCustomizer です。

例 44: Spring Boot に設定させる
@Bean (4)
WebClientCustomizer hypermediaWebClientCustomizer(HypermediaWebClientConfigurer configurer) { (1)
    return webClientBuilder -> { (2)
        configurer.registerHypermediaTypes(webClientBuilder); (3)
    };
}
1Spring Bean を作成する場合、Spring HATEOAS の HypermediaWebClientConfigurer Bean のコピーをリクエストします。
2Java 8 ラムダ式を使用して WebClientCustomizer を定義します。
3 関数呼び出し内で、registerHypermediaTypes メソッドを適用します。
4 全体を Spring Bean として返し、Spring Boot がそれを取得して、自動構成された WebClient.Builder Bean に適用できるようにします。

この段階では、具体的な WebClient が必要なときはいつでも、コードに WebClient.Builder を挿入し、build() を使用してください。WebClient インスタンスは、ハイパーメディアを使用して対話できます。

6.4. WebTestClient インスタンスの構成

ハイパーメディア対応の表現を扱う場合、一般的なタスクは WebTestClient を使用してさまざまなテストを実行することです。

テストケースで WebTestClient のインスタンスを構成するには、次の例を確認してください。

例 45: Spring HATEOAS を使用する場合の 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 サポートを有効にします。
2HypermediaWebTestClientConfigurer を使用して、ハイパーメディアサポートを適用します。
3Spring HATEOAS の TypeReferences.CollectionModelType ヘルパーを使用して CollectionModel<EntityModel<Employee>> のレスポンスを求めます。
4Spring HATEOAS 形式の「本体」を取得したら、それに対してアサート !
WebTestClient は不変の値型であるため、その場で変更することはできません。HypermediaWebClientConfigurer は変異したバリアントを返します。これを使用するには、それをキャプチャーする必要があります。

Spring Boot を使用している場合は、次のような追加のオプションがあります。

例 46: 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 のテストアノテーションです。
2Spring Boot の WebTestClient.Builder を builder および Spring HATEOAS の構成プログラムにメソッドパラメーターとして Autowire します。
3 ハイパーメディアのサポートを登録するには、HypermediaWebTestClientConfigurer を使用します。
4TypeReferences を使用して EntityModel<Employee> を返したいというシグナル。

ここでも、前の例と同様のアサーションを使用できます。

テストケースを作成する方法は他にもたくさんあります。WebTestClient は、コントローラー、関数、URL にバインドできます。このセクションは、そのすべてを示すことを意図したものではありません。代わりに、これは開始するためのいくつかの例を提供します。重要なことは、HypermediaWebTestClientConfigurer を適用することにより、ハイパーメディアを処理するために WebTestClient の任意のインスタンスを変更できることです。

6.5. RestTemplate インスタンスの構成

ハイパーメディアを話すように構成された RestTemplate の独自のコピーを作成する場合は、HypermediaRestTemplateConfigurer を使用できます。

例 47: 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 レジスターのコピーを取得します。
2RestTemplate を作成したら、configurer を使用してハイパーメディア型を適用します。

このパターンは、必要な RestTemplate の任意のインスタンスに自由に適用できます。これは、登録済みの Bean を作成する場合でも、定義したサービス内でも適用できます。

Spring Boot を使用している場合は、別のアプローチがあります。

一般に、Spring Boot は、アプリケーションコンテキストで RestTemplate Bean を登録するという概念から離れました。

  • さまざまなサービスと通信する場合、さまざまな資格情報が必要になることがよくあります。

  • RestTemplate が基礎となる接続プールを使用する場合、追加の問題が発生します。

  • ユーザーは、単一の Bean ではなく、異なるインスタンスを必要とすることがよくあります。

これを補うために、Spring Boot は RestTemplateBuilder を提供します。この自動構成された Bean により、RestTemplate インスタンスの作成に使用されるさまざまな Bean を定義できます。RestTemplateBuilder Bean を要求し、その build() メソッドを呼び出してから、最終設定 (資格情報やその他の詳細など) を適用します。

ハイパーメディアベースのメッセージコンバーターを登録するには、コードに以下を追加します。

例 48: Spring Boot に設定させる
@Bean (4)
RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { (1)
    return restTemplate -> { (2)
        configurer.registerHypermediaTypes(restTemplate); (3)
    };
}
1Spring Bean を作成する場合、Spring HATEOAS の HypermediaRestTemplateConfigurer Bean のコピーをリクエストします。
2Java 8 ラムダ式を使用して RestTemplateCustomizer を定義します。
3 関数呼び出し内で、registerHypermediaTypes メソッドを適用します。
4 全体を Spring Bean として返し、Spring Boot がそれを取得して、自動構成された RestTemplateBuilder に適用できるようにします。

この段階では、具体的な RestTemplate が必要なときはいつでも、コードに RestTemplateBuilder を挿入し、build() を使用してください。RestTemplate インスタンスは、ハイパーメディアを使用して対話できます。