【SpringBoot】プログラムの中からAPIエンドポイントの情報を取得する

# はじめに

## 対象の方

- SpringBoot（SpringMVC／spring-boot-starter-web）を使ってる人
- アプリで使用可能な API エンドポイントの情報を取りたい人

サンプルは Kotlin で記述していますが、Java でも同等のことが実現可能かと思います。

## 忙しい人向け

RequestMappingHandlerMapping の Bean を引き込んで getHandlerMethods() を叩けば取れます。

# 取得方法

具体的にどのようなことをすればタイトルのようなことが実現できるか見ていきます。

## 前準備

**RequestMappingHandlerMapping**クラスのインスタンスを DI などでコンテナから取得します。

このクラスは Spring のコンテナに Bean として登録されていますので、
コンストラクタインジェクション等で容易にインスタンスを確保することが出来ます。

こんなかんじ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199230/0023f340-43be-8bb9-06b0-c1f6fe9edc72.png)

## エンドポイント一覧の取得

**RequestMappingHandlerMapping#getHandlerMethods**を呼び出すことで、API エンドポイントの一覧を取得できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199230/4ee6c421-107e-289b-d568-9a823410844d.png)

# おまけ

## 戻り値

getHandlerMethods()の戻り値は**RequestMappingInfo**クラスと**HandlerMethod**クラスの Map です。
それぞれどのような情報を持っているかを見ていきます。

### HandlerMethod

呼び出し先の Controller クラスに定義されている関数の情報が収められています。
下記のデバッグウィンドウに表示されている通り、関数の参照（リフレクションで取得できる Method クラス）や関数が定義されている Controller クラスの情報等が取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199230/6cf02ece-15ee-6a34-592a-1897d79296e4.png)

### RequestMappingInfo

エンドポイントのパス情報と対応している HTTP メソッドの種別などが収められています。
下記のデバッグウィンドウに表示されている通り、RequestMapping アノテーションに設定されているパス（/api/user）と、fetchUser()に設定されている HTTP メソッド（GET、HEAD）等が取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199230/1396b4a1-fa95-2b2c-a4ed-ad464dbf8a02.png)

### Key と Value の組み合わせで表現されるもの

「この HTTP メソッドとパスを持つリクエストは、この関数で処理を行う」…を表現します。
RequestMappingHandlerMapping クラスの名前の通り、リクエストとのマッピングですね。
下記の関数定義部分とデバッグウィンドウを見比べてみてください。Controller に定義されている関数およびそれらに付与されているアノテーションの組み合わせが Key-Value で表現されているのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199230/c760ae6f-d86b-8540-7cb6-c3e31040a574.png)

## 実用例

デバッグ目的のほかに、実際に使えそうな例を考えてみました。
CORSの設定をイイカンジに行ってくれるFilterを書くのに使えそう…？

```kotlin
  /**
   * [HttpServletRequest.getRequestURI]と一致するエンドポイントを列挙し、そのエンドポイントが求めているHTTPメソッドの情報を
   * "Access-Control-Allow-Methods"ヘッダとして[response]に設定する。
   * 同一のパスを持つがHTTPメソッドごとに関数が分かれているようなケースにおいては、それらの関数が要求するHTTPメソッドすべてを収集し、
   * "Access-Control-Allow-Methods"の設定値とする。
   * また、Preflight requestに対応するため、上記で検出したHTTPメソッド一覧に加えて[RequestMethod.OPTIONS]を無条件で追加する。
   *
   * @param request 受信したリクエスト
   * @param response ヘッダ設定対象のレスポンス
   * @see findMappingInfosByPath
   */
  private fun applyAllowMethods(request: HttpServletRequest, response: HttpServletResponse) {
    val supportRequestMethods = findMappingInfosByPath(request).flatMap { it.methodsCondition.methods }
    val allowMethodsText = (supportRequestMethods + listOf(RequestMethod.OPTIONS))
      .toSet()
      .joinToString(", ") { it.name }
    response.setHeader("Access-Control-Allow-Methods", allowMethodsText)
  }

  /**
   * [HttpServletRequest.getRequestURI]と一致（[RequestMappingInfo.getDirectPaths]に含まれるかどうかで判断）するパスを持つ
   * エンドポイントのマッピング情報を検索するシーケンスを作成する。
   *
   * @param request 受信したリクエスト
   * @return 見つかったマッピング情報を返すシーケンス
   */
  private fun findMappingInfosByPath(request: HttpServletRequest): Sequence<RequestMappingInfo> {
    return requestMappingHandlerMapping.handlerMethods.keys.asSequence().filter { info ->
      info.directPaths.contains(request.requestURI)
    }
  }
```

全体像は下記のGistに置いてあります。
https://gist.github.com/samunohito/6000650cffeaf5a317400df1e76a6d62

# 参考

- [RequestMappingHandlerMapping (Spring Framework 5.3.22 API)](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.html)
- [RequestMappingHandlerMapping.java](https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java)
- [AbstractHandlerMethodMapping (Spring Framework 5.3.22 API](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.html)
- [AbstractHandlerMethodMapping.java](https://github.com/spring-projects/spring-framework/blob/main/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java)