El ClassCastException
peut se produire si la même classe a été chargée par plusieurs classloaders différents et que des instances de ces classes sont partagées entre eux.
Considérons l'exemple de hiérarchie suivant.
SystemClassloader <--- AppClassloader <--+--- Classloader1
|
+--- Classloader2
Je pense qu'en général, les points suivants sont vrais, mais il est possible d'écrire des classloaders personnalisés qui s'en écartent.
- Les instances des classes chargées par SystemClassloader sont accessibles dans n'importe lequel des contextes du classloader.
- Les instances des classes chargées par l'AppClassloader sont accessibles dans n'importe lequel des contextes du classloader.
- Les instances des classes chargées par Classloader1 ne sont pas accessibles par Classloader2.
- Les instances des classes chargées par Classloader2 ne sont pas accessibles par Classloader1.
Comme nous l'avons mentionné, un scénario courant où cela se produit est celui des déploiements d'applications web où, en général, l'AppClassloader ressemble beaucoup au classpath configuré dans l'appserver et où les Classloader1 et Classloader2 représentent les classpaths des applications web déployées individuellement.
Si plusieurs applications web déploient les mêmes JARs/classes, alors l'option ClassCastException
peut se produire s'il existe un mécanisme permettant aux applications web de partager des objets tels qu'un cache ou une session partagée.
Un autre scénario similaire peut se produire si les classes sont chargées par l'application Web et que les instances de ces classes sont stockées dans la session ou le cache de l'utilisateur. Si la web app est redéployée, ces classes sont rechargées par un nouveau classloader et toute tentative d'accès aux objets de la session ou du cache entraînera cette exception.
Une méthode pour éviter ce problème en production est de déplacer les JARs plus haut dans la hiérarchie du classloader. Ainsi, au lieu d'inclure le même JAR dans chaque application Web, il est préférable de les inclure dans le classpath de l'appserver. De cette façon, les classes ne sont chargées qu'une seule fois et sont accessibles à toutes les applications Web.
Une autre méthode pour éviter cela est d'opérer uniquement sur les interfaces que les objets partagés. Les interfaces doivent alors être chargées plus haut dans la hiérarchie du chargeur de classes, mais pas les classes elles-mêmes. Votre exemple de récupération de l'objet depuis le cache serait le même, mais l'interface C1
serait remplacée par une interface qui C1
met en œuvre.
Vous trouverez ci-dessous un exemple de code qui peut être exécuté indépendamment pour recréer ce scénario. Ce n'est pas le plus concis et il y a certainement de meilleures façons de l'illustrer, mais il lève l'exception pour les raisons mentionnées ci-dessus.
Sur a.jar
package les deux classes suivantes, A
et MyRunnable
. Ceux-ci sont chargés plusieurs fois par deux classloaders indépendants.
package classloadertest;
public class A {
private String value;
public A(String value) {
this.value = value;
}
@Override
public String toString() {
return "<A value=\"" + value + "\">";
}
}
Et
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());
}
}
Indépendamment des classes ci-dessus, le programme suivant est exécuté. Il ne doit pas partager un classpath avec les classes ci-dessus pour s'assurer qu'elles sont chargées à partir du fichier 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);
}
}
En exécutant cette opération, le résultat suivant s'affiche :
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)
J'ai mis la source sur GitHub également.