Error [java.lang.NoClassDefFoundError: javax/servlet/Filter]

NoClassDefFoundError: javax/servlet/Filter の直し方(自分は jakarta に移行したのに、依存が javax を要求する)

FIX SUMMARY verified
Applies when
eclipse-temurin:17-jdkspring-web / spring-context (Spring 5's GenericFilterBean implements javax.servlet.Filter; Spring 6's implements jakarta.servlet.Filter — the app itself references neither)running a jakarta.servlet application that still has a pre-Jakarta library on the classpath — it compiles when nothing in your own source statically references the old library's types

Verified: reproduced in eclipse-temurin:17-jdk, then the java.lang.NoClassDefFoundError: javax/servlet/Filter signature was gone after the fix (exit 0).

jakarta.servlet へ移行して、自分のコードから javax.* の import を全部消した。コンパイルも通る。 それでも実行時に次のエラーで落ちることがあります。

Exception in thread "main" java.lang.NoClassDefFoundError: javax/servlet/Filter
	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
	...
	at java.base/java.lang.Class.forName(Class.java:377)
	at App.main(App.java:3)
Caused by: java.lang.ClassNotFoundException: javax.servlet.Filter
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
	... 21 more

javax.servlet.Filter は、自分のコードが1文字も参照していないクラスです。

直し方の要点を先に

要求している側のライブラリを、Jakarta 世代に対応した版へ上げます。

<!-- 旧:Spring 5 系。GenericFilterBean が javax.servlet.Filter を実装している -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>6.1.15</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>6.1.15</version>
</dependency>

関連する Spring の module は揃えて上げます(片方だけ上げると版が混在します)。Spring Boot を使っているなら、 個別の版を指定するのではなく Boot / BOM が管理する世代へ上げるのが筋です。上の 6.1.15 はこの記事が 検証で通した一点であって、恒久的に固定する版という意味ではありません。利用中の BOM と、Servlet / JDK の baseline に合う世代を選んでください。

javax.servlet-api を足して黙らせない

依存に javax.servlet-api を足すと、クラスは見つかるようになります(実測:この記事の最小構成に javax.servlet-api:4.0.1 を足すと終了コード 0 でロードが通ります)。それでも移行にはなりません。 javax.servlet.Filterjakarta.servlet.Filter無関係な型なので、Spring 5 のフィルタは javax.servlet.Filter の実装のままです。Jakarta 世代のコンテナが呼ぶのは jakarta.servlet の型なので、 そのフィルタはフィルタチェーンに参加できません(ロードは通るのに機能しない、という形に問題が移ります)。

なぜコンパイルが通るのに実行時に落ちるのか

javax.servlet.Filter を要求しているのは、自分のコードではなく、依存しているライブラリです。 Spring 5 の OncePerRequestFilter は、superclass の GenericFilterBean を介して javax.servlet.Filter を 実装していますOncePerRequestFilterGenericFilterBean を継承し、GenericFilterBean が interface の Filter を実装する、という連鎖です)。Spring 6 の同じ連鎖では、GenericFilterBeanjakarta.servlet.Filter を実装します。

  • 落ちるのはクラスをロードした瞬間です。 JVM が OncePerRequestFilter を定義するには、その superclass と、 そこが実装する interface を解決する必要があります。classpath には jakarta.servlet-api しか無いので、 javax.servlet.Filter を解決できずに落ちます。
  • この最小構成でコンパイルが通るのは、クラス名を文字列で渡しているからです。 再現コードは Class.forName("org.springframework.web.filter.OncePerRequestFilter") と書いており、Spring の型を ソース上で参照していません。javac が解決するのは、ソースが参照・継承・実装する型です。 OncePerRequestFilter を継承した自作フィルタを書けば、javax.servlet.Filter が無い時点でコンパイルが落ちます ——つまり「実行時まで露見しない」のは、そのクラスを静的に参照していない経路に限った話です。

同じアプリ(依存は jakarta.servlet:jakarta.servlet-api:6.0.0)で、Spring の版だけを変えた実測です。

Spring Web / Context結果
5.3.39NoClassDefFoundError: javax/servlet/Filter(終了コード 1)
6.1.15成功(終了コード 0)

アプリのソースは2回とも同一で、変えたのは Spring の版だけです。

この時間差が、次の形で表面化します。

  • 自分のコードの移行が済んでも終わらない。 import を全部書き換えてビルドが緑になっても、依存が旧世代のままなら 実行時に落ちます。ビルドの緑は、移行の完了を意味しません。
  • CI がユニットテストしか回していないと、デプロイ先で初めて落ちる。 そのクラスをロードする経路を通ったときだけ 例外になるので、ロードしないテストは緑のまま通ります。

直っていないのはどのライブラリか

エラーメッセージは「javax.servlet.Filter が無い」としか言いません。要求している側の名前は、そこには出ません。

スタックトレースにも、要求元のクラス名が出るとは限りません。 上のトレースがその例です——フレームは クラスローダの内部と App.main だけで、OncePerRequestFilter はどこにも現れません。ロードに失敗したクラスの コードは一度も実行されないので、フレームには残らないためです。Caused by の上を見れば要求元が分かる」とは 限らない、ということです。

代わりに、次の順で当たります。

  • ロードの起点を見る。 トレースのアプリ側フレーム(上の例なら App.main)は、どこでロードが始まったかを 示します。実際のアプリなら、そこに Spring の起動やフィルタ登録のフレームが並ぶので、経路の手掛かりになります。 上の最小構成では、ロード対象は Class.forName に渡した文字列そのものなので最初から分かっています。
  • 依存ツリーから探す。 mvn dependency:tree で、Jakarta 世代に上げ忘れているライブラリを探します。 名前空間の世代はライブラリごとに上がるので、1つでも旧世代が残っていると、その1つが要求します。
  • 要求している jar を直接特定する。 どの jar が javax.servlet を参照しているかは、jdeps などで classpath 上の jar を調べると分かります。トレースに名前が出ない場合はこちらが確実です。

切り分け(うまくいかないとき)

  • コンパイルの時点で package javax.persistence does not exist が出る:それは自分のコードがまだ古い名前空間を 参照しています。実行時まで届いていません。 package javax.persistence does not exist の直し方(Spring Boot 3) を参照してください。
  • package javax.mail does not exist が出る:依存ので名前空間が変わる罠です(Maven 座標が先に jakarta へ改名されている)。 package javax.mail does not exist の直し方 を参照してください。
  • 落ちているのが javax/xml/bind/JAXBException:それは Jakarta EE の改名ではなく、JDK が JAXB の同梱をやめた話です (Java 11 / JEP 320)。同じ NoClassDefFoundError でも、直し方は依存を足すことです。 NoClassDefFoundError: javax/xml/bind/JAXBException の直し方 を参照してください。
  • ライブラリに Jakarta 対応版が無い:そのライブラリを置き換えるか、移行を待つ判断になります。javax.servlet-api を 足す回避は、上に書いたとおりロードは通るがフィルタが機能しない形に問題を移すだけです。
  • 上げたら別のクラスで同じ形の例外が出た:世代はライブラリごとなので、旧世代が他にも残っています。 同じ手順で要求元を特定します。

検証環境

  • eclipse-temurin:17-jdk の中で Maven と java を実行(依存の取得にのみネットワークを使用)
  • 再現:jakarta.servlet:jakarta.servlet-api:6.0.0 を依存に持つアプリで、Spring Web / Context 5.3.39 を classpath に置き、 OncePerRequestFilter をロードすると NoClassDefFoundError: javax/servlet/FilterCaused by: ClassNotFoundException: javax.servlet.Filter)で終了コード 1
  • 修正:Spring Web / Context を 6.1.15 へ上げると、終了コード 0・シグネチャ消滅

再現から修正までは errfix の検証ハーネスが機械的に確認しています。reproduce と fix の差は Spring の版だけで、 アプリのソースは同一です(jakarta.servlet 側も変えていません)。

ケースは最小化のため、Class.forName("org.springframework.web.filter.OncePerRequestFilter") でクラスをロードしています。 実際のアプリでこの例外に当たるのは、Spring がフィルタを実体化する起動時など、そのクラスがロードされる経路を 通ったときです。Class.forName は「ロードした瞬間に superclass と、そこが実装する interface の解決が走る」という 機構を、最小の構成で証明するための形です。本文に載せたスタックトレースはこの最小構成の実測で、フレームが クラスローダ内部と App.main だけになるのはそのためです(実アプリなら、そこに Spring 側のフレームが並びます)。 ケースが spring-context も明示しているのは、spring-web だけでは OncePerRequestFilter のロードに必要なクラスが 揃わないためで、これも実測して確かめました。

javax.servlet-api を足すと「ロードは通る」ことも実測しました——同じ最小構成に javax.servlet-api:4.0.1 を 足すと終了コード 0 になります。上でこの回避を勧めないのは、エラーが消えないからではなく、 消えた先が「フィルタがチェーンに参加しない」状態だからです。各版・各構成の結果はケースの verification/probes.txt に記録しています。

検証(machine-verified)

この修正は eclipse-temurin:17-jdk のバージョン固定コンテナ内で再現し、修正後に java.lang.NoClassDefFoundError: javax/servlet/Filter のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs jvm/jakarta-migration-transitive-javax
● reproduce java.lang.NoClassDefFoundError: javax/servlet/Filter present ✓
● apply fix exit 0
● re-run java.lang.NoClassDefFoundError: javax/servlet/Filter gone ✓
PASS verified · eclipse-temurin:17-jdk · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。