0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コンピューターシステム株式会社Advent Calendar 2024

Day 14

【Spring Initializr 読解】initializr-web: ProjectMetadataController.java 編

Posted at

【Spring Initializr 読解】initializr 特有の資材 の続編です。

この記事では、 initializr のサブモジュールである、 initializr-web についてコードを追っていきます。

読み始める起点を探す

コードを追っていく上でいい感じのエントリーポイント(処理の起点)を探します。
今回は、 http://localhost:8080/ で実行されるメソッドの起点をまず探します。

spring-webcontroller パッケージの ProjectMetadataController クラスにて、 @GetMapping でルートパスが指定されているメソッドがあるので、そこを目当てに見てみます。すると、4つほど候補がありました。

ProjectMetadataController.java
	@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 で実行します。

image.png

起動できたら、ブラウザで http://localhost:8080/ にアクセスします。
すると、 initializr-webProjectMetadataController.serviceCapabilitiesHal にブレークされました。

image.png

ということで、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" で実行していることがわかります。

ProjectMetadataController.java
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 形式で返す。

コードの流れ

  1. String appUrl = generateAppUrl();

アプリケーションのベース URL を生成する。ローカルで実行した場合は、http://localhost:8080 となる。通常は、Spring Initializr がデプロイされている環境のベース URL になるはず。

  1. InitializrMetadata metadata = this.metadataProvider.get();

メタデータプロバイダーから現在の設定情報を取得する。依存関係、バージョン、パッケージング形式など。

  1. String content = getJsonMapper(version).write(metadata, appUrl);

指定されたバージョンの JSON マッパーを使用して、メタデータを JSON 文字列に変換します。

  1. 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"
  }
}
  1. VSCode では、ブレークポイントを貼らなくても launch.jsonstopOnEntry: true でアプリ起動時に最初の行で自動的にブレークしてくる機能がありますが、 Spring も @GetMapping の最初の行で自動的にブレークしてくると楽ですね。(願望)

0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?