【Spring Initializr 読解】initializr 特有の資材 の続編です。
この記事では、 initializr のサブモジュールである、 initializr-web
についてコードを追っていきます。
読み始める起点を探す
コードを追っていく上でいい感じのエントリーポイント(処理の起点)を探します。
今回は、 http://localhost:8080/ で実行されるメソッドの起点をまず探します。
spring-web
の controller
パッケージの ProjectMetadataController
クラスにて、 @GetMapping
でルートパスが指定されているメソッドがあるので、そこを目当てに見てみます。すると、4つほど候補がありました。
@GetMapping(path = { "/", "/metadata/client" }, produces = "application/hal+json")
public ResponseEntity<String> serviceCapabilitiesHal() {
return serviceCapabilitiesFor(InitializrMetadataVersion.V2_1, HAL_JSON_CONTENT_TYPE);
}
@GetMapping(path = { "/", "/metadata/client" }, produces = { "application/vnd.initializr.v2.2+json" })
public ResponseEntity<String> serviceCapabilitiesV22() {
return serviceCapabilitiesFor(InitializrMetadataVersion.V2_2);
}
@GetMapping(path = { "/", "/metadata/client" },
produces = { "application/vnd.initializr.v2.1+json", "application/json" })
public ResponseEntity<String> serviceCapabilitiesV21() {
return serviceCapabilitiesFor(InitializrMetadataVersion.V2_1);
}
@GetMapping(path = { "/", "/metadata/client" }, produces = "application/vnd.initializr.v2+json")
public ResponseEntity<String> serviceCapabilitiesV2() {
return serviceCapabilitiesFor(InitializrMetadataVersion.V2);
}
@GetMapping
で複数パス指定できるのは知らなかったですね。
あと、produces(クライアントから送信される HTTP ヘッダーの Accept
)でメソッドを分割しているコードを始めてみました。
Spring はパスや HTTP メソッドのほかに、Header や パラメータ、Content-Type 別にハンドラとなるメソッドを分けられるそうです。
ただ、Accept 未指定ですと、どのメソッドが呼ばれるか自信がないので、ブレークポイントを貼り、デバッグ実行して確認してみます。1
では、 ServiceApplication
を Debug で実行します。
起動できたら、ブラウザで http://localhost:8080/ にアクセスします。
すると、 initializr-web
の ProjectMetadataController.serviceCapabilitiesHal
にブレークされました。
ということで、Accept が未指定の場合、 Accept: application/hal+json
のメソッドが選択されるようです。
この辺りの挙動を調べてみると、Accept が未指定(media type が */*
とみなされる)の場合、より具体的(単純に文字列が長いもの)に media type が指定されているメソッドが優先的に適用されるそうです。ただ、 Spring HATEOAS を使っている場合、HATEOAS でサポートされている media type が優先されるため、 hal+
で指定されているメソッドが選択されたようです。
この当たりはドキュメントには特に記載が見当たらず、ソースコードを追ってみました。
HypermediaMappingInformationComparator.java にそういう処理が入っています。
EnableHypermediaSupport.java は HATEOAS でサポートしている media type です。
クライアントに Response を返すまでの処理の流れを追う
上記で見つけた起点から処理を追っていくと、以下のメソッドに対して、version="application/vnd.initializr.v2.1+json", contentType="application/hal+json"
で実行していることがわかります。
private ResponseEntity<String> serviceCapabilitiesFor(InitializrMetadataVersion version,
MediaType contentType) {
String appUrl = generateAppUrl();
InitializrMetadata metadata = this.metadataProvider.get();
String content = getJsonMapper(version).write(metadata, appUrl);
return ResponseEntity.ok()
.contentType(contentType)
.eTag(createUniqueId(content))
.varyBy("Accept")
.cacheControl(determineCacheControlFor(metadata))
.body(content);
}
このメソッドの実装を要約してみると以下のようになります。
全体的な処理の説明
Spring Initializr が提供する機能(dependencies、Java versions、Spring Boot versions など)のメタデータを JSON 形式で返す。
コードの流れ
String appUrl = generateAppUrl();
アプリケーションのベース URL を生成する。ローカルで実行した場合は、http://localhost:8080
となる。通常は、Spring Initializr がデプロイされている環境のベース URL になるはず。
InitializrMetadata metadata = this.metadataProvider.get();
メタデータプロバイダーから現在の設定情報を取得する。依存関係、バージョン、パッケージング形式など。
String content = getJsonMapper(version).write(metadata, appUrl);
指定されたバージョンの JSON マッパーを使用して、メタデータを JSON 文字列に変換します。
-
ResponseEntity.ok()
以降の部分では、HTTP レスポンスを構築しています
-
.contentType(contentType)
: レスポンスの Content-Type を設定 -
.eTag(createUniqueId(content))
: コンテンツの一意性を示す ETag を設定(キャッシュの最適化に使用) -
.varyBy("Accept")
: クライアントの Accept ヘッダーに応じてレスポンスが変わることを示す -
.cacheControl(determineCacheControlFor(metadata))
: キャッシュ制御の設定 -
.body(content)
: レスポンスボディに JSON 文字列を設定
以下、 http://localhost:8080/
で取得できた JSON を載せておきます。
Response(JSON)
{
"_links": {
"maven-project": {
"href": "http://localhost:8080/starter.zip?type=maven-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"gradle-project": {
"href": "http://localhost:8080/starter.zip?type=gradle-project{&dependencies,packaging,javaVersion,language,bootVersion,groupId,artifactId,version,name,description,packageName}",
"templated": true
},
"dependencies": {
"href": "http://localhost:8080/dependencies{?bootVersion}",
"templated": true
}
},
"dependencies": {
"type": "hierarchical-multi-select",
"values": [
{
"name": "Web",
"values": [
{
"id": "web",
"name": "Web",
"description": "Servlet web application with Spring MVC and Tomcat"
}
]
}
]
},
"type": {
"type": "action",
"default": "maven-project",
"values": [
{
"id": "maven-project",
"name": "Maven Project",
"description": "Generate a Maven based project archive",
"action": "/starter.zip",
"tags": {
"build": "maven",
"format": "project"
}
},
{
"id": "gradle-project",
"name": "Gradle Project",
"description": "Generate a Gradle based project archive",
"action": "/starter.zip",
"tags": {
"build": "gradle",
"format": "project"
}
}
]
},
"packaging": {
"type": "single-select",
"default": "jar",
"values": [
{
"id": "jar",
"name": "Jar"
},
{
"id": "war",
"name": "War"
}
]
},
"javaVersion": {
"type": "single-select",
"default": "17",
"values": [
{
"id": "17",
"name": "17"
},
{
"id": "11",
"name": "11"
},
{
"id": "1.8",
"name": "8"
}
]
},
"language": {
"type": "single-select",
"default": "java",
"values": [
{
"id": "java",
"name": "Java"
},
{
"id": "kotlin",
"name": "Kotlin"
},
{
"id": "groovy",
"name": "Groovy"
}
]
},
"bootVersion": {
"type": "single-select",
"default": "3.4.0.RELEASE",
"values": [
{
"id": "3.4.1.BUILD-SNAPSHOT",
"name": "3.4.1 (SNAPSHOT)"
},
{
"id": "3.4.0.RELEASE",
"name": "3.4.0"
},
{
"id": "3.3.7.BUILD-SNAPSHOT",
"name": "3.3.7 (SNAPSHOT)"
},
{
"id": "3.3.6.RELEASE",
"name": "3.3.6"
},
{
"id": "3.2.12.RELEASE",
"name": "3.2.12"
},
{
"id": "3.1.12.RELEASE",
"name": "3.1.12"
},
{
"id": "3.0.13.RELEASE",
"name": "3.0.13"
},
{
"id": "2.7.18.RELEASE",
"name": "2.7.18"
}
]
},
"groupId": {
"type": "text",
"default": "org.acme"
},
"artifactId": {
"type": "text",
"default": "demo"
},
"version": {
"type": "text",
"default": "0.0.1-SNAPSHOT"
},
"name": {
"type": "text",
"default": "demo"
},
"description": {
"type": "text",
"default": "Demo project for Spring Boot"
},
"packageName": {
"type": "text",
"default": "org.acme.demo"
}
}
-
VSCode では、ブレークポイントを貼らなくても
launch.json
のstopOnEntry: true
でアプリ起動時に最初の行で自動的にブレークしてくる機能がありますが、 Spring も@GetMapping
の最初の行で自動的にブレークしてくると楽ですね。(願望) ↩