これClassCastException
は、同じクラスが複数の異なるクラスローダーによってロードされ、クラスのインスタンスがそれらの間で共有されている場合に発生する可能性があります。
次の階層の例を考えてみましょう。
SystemClassloader <--- AppClassloader <--+--- Classloader1
|
+--- Classloader2
一般に、次のことは正しいと思いますが、これから逸脱するカスタムクラスローダーを作成できます。
- SystemClassloader によってロードされたクラスのインスタンスは、どのクラスローダー コンテキストでもアクセスできます。
- AppClassloader によってロードされたクラスのインスタンスは、どのクラスローダー コンテキストでもアクセスできます。
- Classloader1 によってロードされたクラスのインスタンスは、Classloader2 からアクセスできません。
- Classloader2 によってロードされたクラスのインスタンスは、Classloader1 からアクセスできません。
前述のように、これが発生する一般的なシナリオは、一般的に言えば AppClassloader が appserver で構成されたクラスパスに非常に似ている Web アプリのデプロイであり、Classloader1 と Classloader2 は個別にデプロイされた Web アプリのクラスパスを表します。
複数の Web アプリが同じ JAR/クラスをデプロイするClassCastException
場合、Web アプリがキャッシュや共有セッションなどのオブジェクトを共有するメカニズムがある場合に発生する可能性があります。
これが発生する可能性がある別の同様のシナリオは、クラスが Web アプリによって読み込まれ、これらのクラスのインスタンスがユーザー セッションまたはキャッシュに格納されている場合です。Web アプリが再デプロイされると、これらのクラスは新しいクラスローダーによって再ロードされ、セッションまたはキャッシュからオブジェクトにアクセスしようとすると、この例外がスローされます。
本番環境でこの問題を回避する方法の 1 つは、JAR をクラスローダー階層の上位に移動することです。そのため、各 Web アプリに同じ JAR を含める代わりに、これらをアプリサーバーのクラスパスに含める方がうまくいく場合があります。これにより、クラスは 1 回だけ読み込まれ、すべての Web アプリからアクセスできるようになります。
これを回避するもう 1 つの方法は、オブジェクトを共有しているインターフェイスでのみ操作することです。次に、インターフェースをクラスローダー階層の上位にロードする必要がありますが、クラス自体はロードしません。キャッシュからオブジェクトを取得する例は同じですが、C1
クラスは実装するインターフェースに置き換えられますC1
。
以下は、このシナリオを再現するために個別に実行できるサンプル コードです。これは最も簡潔ではなく、確かにそれを説明するためのより良い方法があるかもしれませんが、上記の理由で例外をスローします.
パッケージにはa.jar
、次の 2 つのクラスA
とMyRunnable
. これらは、2 つの独立したクラスローダーによって複数回ロードされます。
package classloadertest;
public class A {
private String value;
public A(String value) {
this.value = value;
}
@Override
public String toString() {
return "<A value=\"" + value + "\">";
}
}
と
package classloadertest;
import java.util.concurrent.ConcurrentHashMap;
public class MyRunnable implements Runnable {
private ConcurrentHashMap<String, Object> cache;
private String name;
public MyRunnable(String name, ConcurrentHashMap<String, Object> cache) {
this.name = name;
this.cache = cache;
}
@Override
public void run() {
System.out.println("Run " + name + ": running");
// Set the object in the cache
A a = new A(name);
cache.putIfAbsent("key", a);
// Read the object from the cache which may be differed from above if it had already been set.
A cached = (A) cache.get("key");
System.out.println("Run " + name + ": cache[\"key\"] = " + cached.toString());
}
}
上記のクラスとは別に、次のプログラムを実行します。上記のクラスが JAR ファイルから確実にロードされるように、クラスパスを上記のクラスと共有してはなりません。
package classloadertest;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.ConcurrentHashMap;
public class Main {
public static void run(String name, ConcurrentHashMap<String, Object> cache) throws Exception {
// Create a classloader using a.jar as the classpath.
URLClassLoader classloader = URLClassLoader.newInstance(new URL[] { new File("a.jar").toURI().toURL() });
// Instantiate MyRunnable from within a.jar and call its run() method.
Class<?> c = classloader.loadClass("classloadertest.MyRunnable");
Runnable r = (Runnable)c.getConstructor(String.class, ConcurrentHashMap.class).newInstance(name, cache);
r.run();
}
public static void main(String[] args) throws Exception {
// Create a shared cache.
ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<String, Object>();
run("1", cache);
run("2", cache);
}
}
これを実行すると、次の出力が表示されます。
Run 1: running
Run 1: cache["key"] = <A value="1">
Run 2: running
Exception in thread "main" java.lang.ClassCastException: classloadertest.A cannot be cast to classloadertest.A
at classloadertest.MyRunnable.run(MyRunnable.java:23)
at classloadertest.Main.run(Main.java:16)
at classloadertest.Main.main(Main.java:24)
ソースもGitHubに上げておきます。