FilterRegistrationBeanのaddUrlPatternsでパスパラメータ含むURLのワイルドカード指定は出来ない

# この記事の概要
- SpringでFilterRegistrationBeanを使い、リクエストの前処理を記述することは多い
- FilterRegistrationBeanは「[addUrlPatterns](https://spring.pleiades.io/spring-boot/api/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.html#addUrlPatterns(java.lang.String...))」というメソッドでフィルタを適用するエンドポイントを絞ることが出来る
- しかし、addUrlPatternsでの指定では柔軟なワイルドカードは指定できない
    - 例：`/hoge/*/piyo`と指定し、`/hoge/some-path/piyo`で適用、といったことは出来ない
    - この挙動はサーブレットの仕様による

以下で詳しく解説します

# まずはURL指定なしでFilterを適用する
下記のようなControllerを用意します。

```kotlin:SampleController.kt
@RestController
@RequestMapping("/sample")
class SampleController {
    @GetMapping("/")
    fun index(): String {
        return "index"
    }

    @GetMapping("/{sampleId}/detail/")
    fun detail(@PathVariable("sampleId") sampleId: String): String {
        return "sampleId is ${sampleId}";
    }

    @GetMapping("/hoge/")
    fun hoge(): String {
        return "sample hoge"
    }
}
```

次に、動作確認用のFilterを用意します。

```kotlin:SampleFilter.kt
class SampleFilter: OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        println("SampleFilter is called from ${request.requestURI}")
        filterChain.doFilter(request, response)
    }
}
```

```kotlin:SampleFilterConfig.kt
@Configuration
class SampleFilterConfig {

    @Bean
    fun sampleFilter(): FilterRegistrationBean<SampleFilter> {
        val filterRegistrationBean = FilterRegistrationBean<SampleFilter>(SampleFilter())
        return filterRegistrationBean
    }
}
```

上記を起動し、動作確認します。今は`SampleFilterConfig.kt`で特にURL指定を何もしていないため、3つのエンドポイント全てでFilterが適用されることが期待値です。

```
curl localhost:8080/sample/
curl localhost:8080/sample/123/detail/
curl localhost:8080/sample/hoge/
```

それぞれ実行してみると、期待値通りFilterも適用されていそうです。

![スクリーンショット 2025-05-31 1.52.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/728813/94f2fd9f-ee70-4c42-bcfd-42a4c8ddba55.png)

# addUrlPatternsを指定してみる

ここで、`/sample/{sampleId}/detail/`のエンドポイントでのみFilterを適用したくなったとします。

ざっと調べてみると、URLパターンにはワイルドカードが使えるらしいです。そのため、下記のように指定してみましょう。

```kotlin:SampleFilterConfig.kt
@Configuration
class SampleFilterConfig {

    @Bean
    fun sampleFilter(): FilterRegistrationBean<SampleFilter> {
        val filterRegistrationBean = FilterRegistrationBean<SampleFilter>(SampleFilter())
        // 下記を追加
        filterRegistrationBean.addUrlPatterns(
            "/sample/*/detail/"
        )
        // 追加ここまで
        return filterRegistrationBean
    }
}
```

すると、**コンソールに何も出力されません！** Filterの処理にブレークポイントを張ると分かりますが、SampleFilterの処理が呼ばれていないことが分かります。

ちなみに、他にも適用されてくれそうな`/sample/**/detail/`や`/sample/{sampleId}/detail/`といった指定をしても同様に適用されません。

ここで、下記のように`/sample/*`と指定してみましょう。

```kotlin:SampleFilterConfig.kt
@Configuration
class SampleFilterConfig {

    @Bean
    fun sampleFilter(): FilterRegistrationBean<SampleFilter> {
        val filterRegistrationBean = FilterRegistrationBean<SampleFilter>(SampleFilter())
        // 下記を変更
        filterRegistrationBean.addUrlPatterns(
            "/sample/*"
        )
        // 変更ここまで
        return filterRegistrationBean
    }
}
```

すると、全ての`/sample/`以下のエンドポイントでFilterが適用されます。

![スクリーンショット 2025-05-31 2.02.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/728813/293b2141-9fbc-4f3e-a83c-c670ebfeef2f.png)

下記のような挙動になりそうなことが分かります。
- 末尾のワイルドカード指定は効く
- パスの途中でワイルドカード指定をしても、効かない

# なぜこんな挙動になるのか？
結論、サーブレットの仕様です。

[addUrlPatternsのドキュメント](https://spring.pleiades.io/spring-boot/api/java/org/springframework/boot/web/servlet/AbstractFilterRegistrationBean.html#addUrlPatterns(java.lang.String...))を見ると

> サーブレット仕様で定義されているように、フィルターが登録される URL パターンを追加します。

と記載されています。この分だけで読み取るのはちょっとキツイですが笑。

サーブレット仕様の邦訳が存在し、「第12章　要求のサーブレットへのマッピング（Mapping Requests to Servlets）」にこのURLマッピングの仕様が載っています。105pあたりです。

https://www.cresc.co.jp/tech/java/Servlet_Specifications/Servlet_Specification_3_0_Japanese.pdf

https://www.cresc.co.jp/index.php/tech_info/

↑翻訳してくださった会社

サーブレット仕様を該当箇所の記述は下記です。

> - ‘/’文字で始まり‘/*’サフックスで終わる文字列がパス・マッピングのために使われる。
> - ‘*.’プレフィックスで始まる文字列は拡張子マッピングに使われる。
> - 空の文字列("")は特別の URL パタンで、正確にそのアプリケーションのコンテキスト・ルートにマップする、言い換えれば http://host:port/<contextroot>/の要求を意味する。この場合パス情報は’/’でサーブレット・パスとコンテキスト・パスは空の文字列(““)である。
> - ’/’文字のみを含む文字列はそのアプリケーションの「デフォルト」サーブレットを示す。この場合のサーブレット・パスは要求 URI からコンテキスト・パスを引いたもので、パス情報はnull となる。
> - その他の総ての文字列は正確なマッチングのためにのみ使われる。

つまり、サーブレット仕様では`/hoge/*/piyo`といったような末尾以外のワイルドカードは扱っていないのです。この場合、`/hoge/*/piyo`というパスに対してのみ適用することになります。



# 参考リンク

https://github.com/yamayamaKo/spring_filter_train

https://qiita.com/suke_masa/items/7bdfab8e974931afdac5

https://spring.pleiades.io/spring-security/reference/servlet/architecture.html

https://spring.pleiades.io/spring-boot/api/java/org/springframework/boot/web/servlet/FilterRegistrationBean.html