はじめに (対象読者・この記事でわかること)
この記事は、JavaでWebアプリケーションを開発する中で「複数のWARファイル間で共通のクラスを使いたい」と悩んでいるエンジニアを対象にしています。特に、TomcatやWildFlyなどのアプリケーションサーバーを使っていて、同じライブラリをWARごとにパッケージングするとデプロイが重くなったり、メモリを圧迫したりしている方に最適です。
この記事を読むことで、サーバー側で共通ライブラリを配置する方法、ClassLoaderの仕組み、そして「ClassNotFoundException」や「LinkageError」を回避するためのベストプラクティスが身につきます。サンプルコードと設定例を交えて解説するので、すぐに実践できます。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法とパッケージの概念 - MavenまたはGradleでWARファイルをビルドした経験 - TomcatまたはWildFlyへのデプロイ経験
なぜWAR間でクラスを共有したいのか
Java EE(現Jakarta EE)の世界では、WARファイルは「アプリケーションごとに独立させる」という思想のもと、必要なクラスをすべて内包する構造になっています。しかし、実務では以下のような悩ましい状況に遭遇します。
- 社内で標準化した認証ライブラリ(社内認証SDK)が50個のマイクロサービスで使われており、バージョンアップのたびに50個のWARを再ビルド・再デプロイするのが現実的でない
- 巨大な機械学習モデル(約200 MB)を含むユーティリティを使うために、各WARに同じJARを内包するとデプロイ時間が5分を超える
- ヒープサイズを1 GB確保しているのに、同じライブラリが10個のWARに読み込まれてメモリを圧迫し、GCが頻発してレスポンスタイムが劣化
これらの課題を解決するため、「アプリケーションサーバー側で共通ライブラリを配置し、複数のWARから参照させる」手法が必要になります。ただし、この方法には「どのClassLoaderがクラスを読み込むか」という、一見地味で実は最も重要なポイントが潜んでいます。
サーバー側で共通ライブラリを配置する実践手順
以下では、Tomcat 10.1系とWildFly 31系を例に、共通ライブラリの配置方法から、トラブルシューティングまでをステップバイステップで解説します。
ステップ1:Tomcatで共通ライブラリを配置する
Tomcatには「lib」ディレクトリが存在し、ここに配置したJARは共通クラスローダ(Common ClassLoader)によって読み込まれます。すべてのWebアプリケーション(WAR)から参照可能です。
- まず、共有したいライブラリ(例:
company-auth-sdk-2.3.0.jar)を$CATALINA_HOME/libにコピーします。 - 次に、各WARの
WEB-INF/libから同じライブラリを除外します。Mavenであればprovidedスコープを使います。
Xml<dependency> <groupId>com.example</groupId> <artifactId>company-auth-sdk</artifactId> <version>2.3.0</version> <scope>provided</scope> </dependency>
context.xmlでdelegate="true"を設定すると、Tomcatが親クラスローダを優先する(Java SEの委譲モデルに従う)ため、意図しない再読み込みを防げます。
Xml<Context delegate="true"> <!-- 他の設定 --> </Context>
ステップ2:WildFlyでモジュールとして登録する
WildFlyは「モジュール」という概念を持っており、JARを$JBOSS_HOME/modules以下に配置して宣言的に依存関係を解決します。
- ディレクトリを作成します(パスがモジュール名になるので注意)。
$JBOSS_HOME/modules/com/example/company/auth-sdk/main/
- 同ディレクトリに
module.xmlを作成し、依存関係を記述します。
Xml<module name="com.example.company.auth-sdk" xmlns="urn:jboss:module:1.9"> <resources> <resource-root path="company-auth-sdk-2.3.0.jar"/> </resources> <dependencies> <module name="javax.api"/> <module name="org.slf4j"/> </dependencies> </module>
- アプリケーションの
WEB-INF/jboss-deployment-structure.xmlで依存を宣言します。
Xml<jboss-deployment-structure> <deployment> <dependencies> <module name="com.example.company.auth-sdk"/> </dependencies> </deployment> </jboss-deployment-structure>
ステップ3:マルチリリースJAR(MRJAR)に対応する
Java 9以降、マルチリリースJAR(MRJAR)が登場し、サーバーのJavaバージョンに応じて最適なクラスが読み込まれます。Tomcat 9.0以降、WildFly 15以降はMRJARをサポートしていますが、lib以下に配置した際の挙動が細かく異なるため注意が必要です。
TomcatではMETA-INF/versions/9/以下のクラスが正しく認識されますが、WildFlyではmodule.xmlに<property name="jboss.api"value="private"/>`を追加しないと、MRJAR機能が無効化されることがあります。
ハマった点:ClassCastExceptionとLinkageError
複数のWARで共通ライブラリを使っていると、「同じクラスなのにClassCastExceptionが発生」という不可解な現象に遭遇します。これは、異なるクラスローダで同じクラスを読み込んでいるためです。
例えば、WAR-Aで取得したUserPrincipalをWAR-Bに渡してキャストしようとすると、クラスローダが異なるため「異なる型として扱われ」例外がスローされます。これを解決するには、「クラスを識別するのはFQCN(完全修飾クラス名)とクラスローダの両方」であることを理解し、以下の対策を講じます。
- シリアライズ/デシリアライズを経由する
- インタフェースを共有し、実装クラスは隠蔽する
Thread.currentThread().getContextClassLoader()を一時的に書き換えてからロードする(TCCLハック)
解決策:TCCLハックの実装例
以下は、TCCL(Thread Context ClassLoader)を一時的に書き換えて、共通ライブラリのクラスを安全に読み込むユーティリティです。
Javapublic final class ClassLoaderUtil { private ClassLoaderUtil() {} public static <T> T createInstance(Class<T> iface, String implClassName) { ClassLoader original = Thread.currentThread().getContextClassLoader(); try { // 共通ライブラリをロードしたクラスローダを取得 ClassLoader common = iface.getClassLoader(); Thread.currentThread().setContextClassLoader(common); @SuppressWarnings("unchecked") Class<? extends T> impl = (Class<? extends T>) Class.forName(implClassName, true, common); return impl.getDeclaredConstructor().newInstance(); } catch (Exception ex) { throw new IllegalStateException("Cannot create instance: " + implClassName, ex); } finally { Thread.currentThread().setContextClassLoader(original); } } }
このユーティリティを使うことで、WAR間で共通のインタフェースを通じてクラスを受け渡しつつ、実装クラスは共有ライブラリ側に隠蔽できます。結果として、ClassCastExceptionを回避しながらメモリ効率も改善します。
まとめ
本記事では、Javaで複数のWARファイル間でクラスを共有する方法と、ClassLoaderの落とし穴を解説しました。
- Tomcatでは
$CATALINA_HOME/libに、WildFlyではmodules配下にJARを配置して、共通ライブラリとして参照可能にする - Maven/Gradleの
providedスコープを使い、WARに同じJARを内包しないようにビルドを調整する - 異なるクラスローダで同じクラスを読み込むと
ClassCastExceptionが発生するため、TCCLハックやインタフェース隠蔽で回避する
この記事を通して、デプロイ時間の短縮、メモリ使用量の削減、そしてライブラリ更新時の作業工数削減が実現できるでしょう。今後は、Java 9以降のモジュールシステム(JPMS)と、アプリケーションサーバーのモジュール機能の連携についても深掘りして解説する予定です。
参考資料
- Apache Tomcat 10 Class Loader How-To
- WildFly Documentation - Class Loading in WildFly
- Javaのクラスローダ ~ 仕組みからTomcat・JIG・Springの挙動まで
