リクエストマッピング

このセクションでは、アノテーション付きコントローラーのリクエストマッピングについて説明します。

@RequestMapping

@RequestMapping アノテーションを使用して、リクエストをコントローラーメソッドにマッピングできます。URL、HTTP メソッド、リクエストパラメーター、ヘッダー、メディア型で一致するさまざまな属性があります。クラスレベルで使用して共有マッピングを表現したり、メソッドレベルで使用して特定のエンドポイントマッピングに絞り込んだりできます。

@RequestMapping の HTTP メソッド固有のショートカットバリアントもあります。

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

ショートカットはカスタムアノテーションであり、ほとんどのコントローラーメソッドは、デフォルトですべての HTTP メソッドに一致する @RequestMapping を使用するのではなく、特定の HTTP メソッドにマップする必要があるためです。共有マッピングを表現するには、クラスレベルで @RequestMapping が引き続き必要です。

@RequestMapping は、同じ要素 (クラス、インターフェース、メソッド) で宣言されている他の @RequestMapping アノテーションと組み合わせて使用することはできません。同じ要素で複数の @RequestMapping アノテーションが検出された場合、警告がログに記録され、最初のマッピングのみが使用されます。これは、@GetMapping@PostMapping などの合成 @RequestMapping アノテーションにも当てはまります。

次の例には、型およびメソッドレベルのマッピングがあります。

  • Java

  • Kotlin

@RestController
@RequestMapping("/persons")
class PersonController {

	@GetMapping("/{id}")
	public Person getPerson(@PathVariable Long id) {
		// ...
	}

	@PostMapping
	@ResponseStatus(HttpStatus.CREATED)
	public void add(@RequestBody Person person) {
		// ...
	}
}
@RestController
@RequestMapping("/persons")
class PersonController {

	@GetMapping("/{id}")
	fun getPerson(@PathVariable id: Long): Person {
		// ...
	}

	@PostMapping
	@ResponseStatus(HttpStatus.CREATED)
	fun add(@RequestBody person: Person) {
		// ...
	}
}

URI パターン

@RequestMapping メソッドは、URL パターンを使用してマッピングできます。2 つの選択肢があります。

  • PathPattern —  PathContainer としても事前に解析された URL パスと照合される事前に解析されたパターン。このソリューションは Web 使用向けに設計されており、エンコーディングとパスパラメーターを効果的に処理し、効率的に一致します。

  • AntPathMatcher — 文字列パターンを文字列パスと照合します。これは、Spring 構成でもクラスパス、ファイルシステム、その他の場所のリソースを選択するために使用される元のソリューションです。これは効率が悪く、文字列パスの入力は、URL のエンコーディングやその他の課題を効果的に処理するための課題です。

PathPattern は、Web アプリケーションに推奨されるソリューションであり、Spring WebFlux で唯一の選択肢です。バージョン 5.3 から Spring MVC での使用が有効になり、バージョン 6.0 からデフォルトで有効になります。パスマッチングオプションのカスタマイズについては、MVC 設定を参照してください。

PathPattern は、AntPathMatcher と同じパターン構文をサポートします。さらに、パスの末尾の 0 個以上のパスセグメントに一致するキャプチャーパターン ( {*spring} など) もサポートします。また、PathPattern は、複数のパスセグメントに一致する ** の使用をパターンの末尾でのみ許可するように制限します。これにより、特定のリクエストに最適な一致パターンを選択するときに、多くのあいまいさが排除されます。完全なパターン構文については、PathPattern (Javadoc) および AntPathMatcher (Javadoc) を参照してください。

いくつかのパターン例:

  • "/resources/ima?e.png" - パスセグメントの 1 文字に一致

  • "/resources/*.png" - パスセグメントの 0 個以上の文字に一致

  • "/resources/**" - 複数のパスセグメントに一致

  • "/projects/{project}/versions" - パスセグメントを一致させ、変数としてキャプチャーする

  • "/projects/{project:[a-z]+}/versions" - 変数を正規表現に一致させてキャプチャーする

キャプチャーされた URI 変数は @PathVariable でアクセスできます。例:

  • Java

  • Kotlin

@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
	// ...
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
	// ...
}

次の例に示すように、クラスおよびメソッドレベルで URI 変数を宣言できます。

  • Java

  • Kotlin

@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

	@GetMapping("/pets/{petId}")
	public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
		// ...
	}
}
@Controller
@RequestMapping("/owners/{ownerId}")
class OwnerController {

	@GetMapping("/pets/{petId}")
	fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
		// ...
	}
}

URI 変数は適切な型に自動的に変換されるか、TypeMismatchException が発生します。単純型(intlongDate など)はデフォルトでサポートされており、他のデータ型のサポートを登録できます。型変換および DataBinder を参照してください。

URI 変数に明示的に名前を付けることができますが (たとえば、@PathVariable("customId"))、名前が同じで、コードが -parameters コンパイラーフラグでコンパイルされている場合は、その詳細を省略できます。

構文 {varName:regex} は、{varName:regex} の構文を持つ正規表現で URI 変数を宣言します。例: URL "/spring-web-3.0.5.jar" を指定すると、次のメソッドは名前、バージョン、ファイル拡張子を抽出します。

  • Java

  • Kotlin

@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
	// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) {
	// ...
}

URI パスパターンには、PropertySourcesPlaceholderConfigurer をローカル、システム、環境、その他のプロパティソースに対して使用することにより、起動時に解決される ${…​} プレースホルダーを埋め込むこともできます。これを使用して、たとえば、外部設定に基づいてベース URL をパラメーター化できます。

パターン比較

複数のパターンが URL に一致する場合は、最適なものを選択する必要があります。これは、解析された PathPattern の使用が有効になっているかどうかに応じて、次のいずれかで実行されます。

どちらも、より具体的なパターンを上位にしてパターンを並べ替えるのに役立ちます。URI 変数の数 (1 としてカウント)、単一のワイルドカード (1 としてカウント)、および二重ワイルドカード (2 としてカウント) の数が少ないほど、パターンはより具体的になります。スコアが等しい場合、より長いパターンが選択されます。同じスコアと長さの場合、ワイルドカードよりも多くの URI 変数を含むパターンが選択されます。

デフォルトのマッピングパターン(/**)はスコアリングから除外され、常に最後にソートされます。また、プレフィックスパターン(/public/** など)は、二重ワイルドカードを持たない他のパターンよりも特定性が低いと見なされます。

詳細については、上記のパターンコンパレータへのリンクをたどってください。

サフィックスマッチ

5.3 以降、デフォルトで Spring MVC は .* サフィックスパターンマッチングを実行しなくなりました。/person にマップされたコントローラーも暗黙的に /person.* にマップされます。結果として、パス拡張は、レスポンスのためにリクエストされたコンテンツ型を解釈するために使用されなくなりました (たとえば、/person.pdf/person.xml など)。

ブラウザーが一貫して解釈するのが困難な Accept ヘッダーを送信するために使用した場合、この方法でファイル拡張子を使用する必要がありました。現時点では、もはや必要ではなく、Accept ヘッダーを使用することをお勧めします。

時間の経過とともに、ファイル名拡張子の使用はさまざまな点で問題があることが証明されています。URI 変数、パスパラメーター、URI エンコードを使用してオーバーレイすると、あいまいさが生じる可能性があります。URL ベースの認可とセキュリティ(詳細については次のセクションを参照)についての推論もより困難になります。

5.3 より前のバージョンでパス拡張機能の使用を完全に無効にするには、次のように設定します。

"Accept" ヘッダー以外でコンテンツ型をリクエストする方法があると、たとえばブラウザーで URL を入力するときなどに便利です。パス拡張子の安全な代替手段は、クエリパラメーター戦略を使用することです。ファイル拡張子を使用する必要がある場合は、ContentNegotiationConfigurer の mediaTypes プロパティを通じて明示的に登録された拡張子のリストに制限することを検討してください。

サフィックスマッチと RFD

反射ファイルダウンロード(RFD)攻撃は、レスポンスに反映されるリクエスト入力(クエリパラメーターや URI 変数など)に依存するという点で XSS に似ています。ただし、JavaScript を HTML に挿入する代わりに、ブラウザーを切り替えてダウンロードを実行し、後でダブルクリックしたときにレスポンスを実行可能なスクリプトとして処理することに依存します。

Spring MVC では、@ResponseBody および ResponseEntity メソッドが危険にさらされます。これは、クライアントが URL パス拡張を介してリクエストできるさまざまなコンテンツ型をレンダリングできるためです。サフィックスパターンマッチングを無効にし、コンテンツネゴシエーションにパス拡張を使用すると、リスクは低下しますが、RFD 攻撃を防ぐには不十分です。

RFD 攻撃を防ぐために、Spring MVC は、レスポンス本文をレンダリングする前に、固定された安全なダウンロードファイルを提案する Content-Disposition:inline;filename=f.txt ヘッダーを追加します。これは、URL パスに、コンテンツネゴシエーションに対して安全として許可されておらず、明示的に登録されていないファイル拡張子が含まれている場合にのみ行われます。ただし、URL をブラウザーに直接入力すると、副作用が生じる可能性があります。

多くの一般的なパス拡張は、デフォルトで安全として許可されています。カスタム HttpMessageConverter 実装を使用するアプリケーションは、コンテンツネゴシエーションのファイル拡張子を明示的に登録して、それらの拡張子に Content-Disposition ヘッダーが追加されないようにすることができます。コンテンツタイプを参照してください。

RFD に関連する追加の推奨事項については、CVE-2015-5211 (英語) を参照してください。

消費可能なメディア型

次の例に示すように、リクエストの Content-Type に基づいてリクエストマッピングを絞り込むことができます。

  • Java

  • Kotlin

@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
	// ...
}
1consumes 属性を使用して、コンテンツ型ごとにマッピングを絞り込みます。
@PostMapping("/pets", consumes = ["application/json"]) (1)
fun addPet(@RequestBody pet: Pet) {
	// ...
}
1consumes 属性を使用して、コンテンツ型ごとにマッピングを絞り込みます。

consumes 属性は否定表現もサポートします。たとえば、!text/plain は text/plain 以外のコンテンツ型を意味します。

クラスレベルで共有 consumes 属性を宣言できます。ただし、他のほとんどのリクエストマッピング属性とは異なり、クラスレベルで使用する場合、メソッドレベルの consumes 属性はクラスレベルの宣言を継承するのではなくオーバーライドします。

MediaType は、APPLICATION_JSON_VALUE や APPLICATION_XML_VALUE などの一般的に使用されるメディア型に定数を提供します。

生産可能なメディア型

次の例に示すように、Accept リクエストヘッダーとコントローラーメソッドが生成するコンテンツ型のリストに基づいて、リクエストマッピングを絞り込むことができます。

  • Java

  • Kotlin

@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
	// ...
}
1produces 属性を使用して、コンテンツ型ごとにマッピングを絞り込みます。
@GetMapping("/pets/{petId}", produces = ["application/json"]) (1)
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
	// ...
}
1produces 属性を使用して、コンテンツ型ごとにマッピングを絞り込みます。

メディア型は文字セットを指定できます。否定式がサポートされています。たとえば、!text/plain は、"text/plain" 以外のすべてのコンテンツ型を意味します。

クラスレベルで共有 produces 属性を宣言できます。ただし、他のほとんどのリクエストマッピング属性とは異なり、クラスレベルで使用する場合、メソッドレベルの produces 属性はクラスレベルの宣言を継承するのではなくオーバーライドします。

MediaType は、APPLICATION_JSON_VALUE や APPLICATION_XML_VALUE などの一般的に使用されるメディア型に定数を提供します。

パラメーター、ヘッダー

リクエストパラメーターの条件に基づいて、リクエストマッピングを絞り込むことができます。リクエストパラメーターの存在(myParam)、パラメーターの不在(!myParam)、特定の値(myParam=myValue)をテストできます。次の例は、特定の値をテストする方法を示しています。

  • Java

  • Kotlin

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
	// ...
}
1myParam が myValue と等しいかどうかのテスト。
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
	// ...
}
1myParam が myValue と等しいかどうかのテスト。

次の例に示すように、リクエストヘッダー条件で同じものを使用することもできます。

  • Java

  • Kotlin

@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
	// ...
}
1myHeader が myValue と等しいかどうかのテスト。
@GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
	// ...
}
1myHeader が myValue と等しいかどうかのテスト。
Content-Type および Accept をヘッダー条件と一致させることができますが、代わりに consumesproduces を使用することをお勧めします。

HTTP HEAD, OPTIONS

@GetMapping (および @RequestMapping(method=HttpMethod.GET))は、リクエストマッピングのために HTTP HEAD を透過的にサポートします。コントローラーのメソッドを変更する必要はありません。jakarta.servlet.http.HttpServlet で適用されるレスポンスラッパーは、Content-Length ヘッダーが(実際にレスポンスに書き込まずに)書き込まれたバイト数に設定されるようにします。

デフォルトでは、HTTP OPTIONS は、Allow レスポンスヘッダーを、一致する URL パターンを持つすべての @RequestMapping メソッドにリストされている HTTP メソッドのリストに設定することによって処理されます。

HTTP メソッド宣言のない @RequestMapping の場合、Allow ヘッダーは GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS に設定されます。コントローラーメソッドは、サポートされている HTTP メソッドを常に宣言する必要があります(たとえば、HTTP メソッド固有のバリアント(@GetMapping@PostMapping など)を使用する)。

@RequestMapping メソッドを明示的に HTTP HEAD および HTTP OPTIONS にマップできますが、これは一般的なケースでは必要ありません。

カスタムアノテーション

Spring MVC は、リクエストマッピングのための構成済みアノテーションの使用をサポートしています。それらは、それ自体が @RequestMapping でメタアノテーションが付けられたアノテーションであり、@RequestMapping 属性のサブセット (またはすべて) をより狭く、より具体的な目的で再宣言するように構成されています。

@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping は、合成されたアノテーションの例です。これらが提供されるのは、おそらく、デフォルトですべての HTTP メソッドに一致する @RequestMapping を使用するのではなく、ほとんどのコントローラーメソッドを特定の HTTP メソッドにマップする必要があるためです。合成されたアノテーションを実装する方法の例が必要な場合は、それらがどのように宣言されているかを確認してください。

@RequestMapping は、同じ要素 (クラス、インターフェース、メソッド) で宣言されている他の @RequestMapping アノテーションと組み合わせて使用することはできません。同じ要素で複数の @RequestMapping アノテーションが検出された場合、警告がログに記録され、最初のマッピングのみが使用されます。これは、@GetMapping@PostMapping などの合成 @RequestMapping アノテーションにも当てはまります。

Spring MVC は、カスタムリクエストマッチングロジックを使用したカスタムリクエストマッピング属性もサポートしています。これは、RequestMappingHandlerMapping のサブクラス化と getCustomMethodCondition メソッドのオーバーライドを必要とするより高度なオプションであり、カスタム属性をチェックして独自の RequestCondition を返すことができます。

明示的な登録

ハンドラーメソッドをプログラムで登録できます。これは、動的な登録や、異なる URL での同じハンドラーの異なるインスタンスなどの高度なケースに使用できます。次の例では、ハンドラーメソッドを登録します。

  • Java

  • Kotlin

@Configuration
public class MyConfig {

	@Autowired
	public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
			throws NoSuchMethodException {

		RequestMappingInfo info = RequestMappingInfo
				.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)

		Method method = UserHandler.class.getMethod("getUser", Long.class); (3)

		mapping.registerMapping(info, handler, method); (4)
	}
}
1 ターゲットハンドラーとコントローラーのハンドラーマッピングを挿入します。
2 リクエストマッピングメタデータを準備します。
3 ハンドラーメソッドを取得します。
4 登録を追加します。
@Configuration
class MyConfig {

	@Autowired
	fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
		val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
		val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
		mapping.registerMapping(info, handler, method) (4)
	}
}
1 ターゲットハンドラーとコントローラーのハンドラーマッピングを挿入します。
2 リクエストマッピングメタデータを準備します。
3 ハンドラーメソッドを取得します。
4 登録を追加します。

@HttpExchange

@HttpExchange の主な目的は、生成されたプロキシを使用して HTTP クライアントコードを抽象化することですが、そのようなアノテーションが配置される HTTP インターフェースは、クライアントとサーバーの使用に中立的な契約です。クライアントコードを簡素化することに加えて、HTTP インターフェースはサーバーがクライアントアクセス用に API を公開する便利なメソッドである場合もあります。これはクライアントとサーバー間の結合の増加につながり、特にパブリック API にとっては良い選択ではないことがよくありますが、内部 API にとってはまさにゴールとなる可能性があります。これは Spring Cloud で一般的に使用されるアプローチであり、コントローラークラスでのサーバー側の処理のために @RequestMapping の代替として @HttpExchange がサポートされるのはこのためです。

例:

  • Java

  • Kotlin

@HttpExchange("/persons")
interface PersonService {

	@GetExchange("/{id}")
	Person getPerson(@PathVariable Long id);

	@PostExchange
	void add(@RequestBody Person person);
}

@RestController
class PersonController implements PersonService {

	public Person getPerson(@PathVariable Long id) {
		// ...
	}

	@ResponseStatus(HttpStatus.CREATED)
	public void add(@RequestBody Person person) {
		// ...
	}
}
@HttpExchange("/persons")
interface PersonService {

	@GetExchange("/{id}")
	fun getPerson(@PathVariable id: Long): Person

	@PostExchange
	fun add(@RequestBody person: Person)
}

@RestController
class PersonController : PersonService {

	override fun getPerson(@PathVariable id: Long): Person {
		// ...
	}

	@ResponseStatus(HttpStatus.CREATED)
	override fun add(@RequestBody person: Person) {
		// ...
	}
}

@HttpExchange と @RequestMapping には違いがあります。@RequestMapping はパスパターン、HTTP メソッドなどによって任意の数のリクエストにマップできますが、@HttpExchange は具体的な HTTP メソッド、パス、コンテンツ型を使用して単一のエンドポイントを宣言します。

メソッドパラメーターと戻り値については、通常、@HttpExchange は @RequestMapping が行うメソッドパラメーターのサブセットをサポートします。特に、サーバー側固有のパラメーター型は除外されます。詳細は @HttpExchange@RequestMapping の一覧を参照してください。

@HttpExchange は、クライアント側の @RequestMapping(headers={}) と同様に "name=value" -like ペアを受け入れる headers() パラメーターもサポートします。サーバー側では、これは @RequestMapping がサポートする完全な構文に拡張されます。