这是一个面试问题。
面试结束了,但这个问题仍在我的脑海里。
我不能问面试官,因为我没有得到这份工作。
情境:
后期代码:
C1 c1FromCache = (C1) cache.get("a");
此代码抛出ClassCastException。
原因是什么?
我之所以说是因为其他人将另一个对象放在同一个键上,因此覆盖了它。我被告知不,想想其他可能性。
我说也许jar定义类C1在这个节点上不可用(不确定这是否会导致类强制转换或ClassNotFoundException,但我现在正在抓住任何领导。然后我说可能错误的类版本?他们说所有节点都有相同的C1类罐。)
编辑/添加询问获取是否正在抛出ClassCast,但被告知没有。在那之后我告诉他我解决这个问题的行动是放入一个模拟动作的测试jsp,并在异常之后放置更好的日志记录(堆栈跟踪)。这是问题的第二部分(为什么以及如果在生产中发生了什么会怎么做)
有没有其他人对缓存获取会导致投射问题的原因有任何想法?
答案 0 :(得分:52)
一个原因可能是插入对象的代码部分使用了与检索它的代码不同的类加载器。
类的实例不能转换为由不同类加载器加载的同一个类。
对编辑的回复:
如果在生产中发生这种情况你会怎么做?
这通常发生在读取和插入模块都包含相同的包含C1
的jar时
由于大多数容器首先尝试父类加载器,然后是本地类加载器(父第一策略),因此问题的常见解决方案是将类加载到最接近的公共父级中以插入和读取模块。
如果将包含C1
类的模块移动到父模块,则强制两个子模块从父模块中获取类,从而删除任何类加载器差异。
答案 1 :(得分:31)
如果多个不同的类加载器加载了相同的类,并且在它们之间共享类的实例,则可能发生ClassCastException
。
考虑以下示例层次结构。
SystemClassloader <--- AppClassloader <--+--- Classloader1
|
+--- Classloader2
我认为通常以下情况属实,但可以编写自定义类加载器,这可能会偏离这一点。
如上所述,发生这种情况的常见情况是Web应用程序部署,其中通常说AppClassloader非常类似于appserver中配置的类路径,然后Classloader1和Classloader2代表单独部署的Web应用程序的类路径。
如果多个Web应用程序部署相同的JAR /类,那么如果Web应用程序有任何共享对象(如缓存或共享会话)的机制,则可能会出现ClassCastException
。
另一种类似的情况是,如果Web应用程序加载了这些类,并且这些类的实例存储在用户会话或缓存中。如果重新部署了Web应用程序,那么这些类将由新的类加载器重新加载,并尝试从会话或缓存中访问对象将引发此异常。
在Production中避免此问题的一种方法是将JAR在类加载器层次结构中向上移动。因此,不是在每个Web应用程序中包含相同的JAR,而是将它们包含在appserver的类路径中可能会更好。通过这样做,类只加载一次,并且可以被所有Web应用程序访问。
避免这种情况的另一种方法是仅在共享对象的接口上操作。接口然后需要在类加载器层次结构中更高的位置加载,但类本身不需要。从缓存中获取对象的示例是相同的,但C1
类将被C1
实现的接口替换。
下面是一些可以独立运行以重新创建此方案的示例代码。它不是最简洁的,当然可能有更好的方式来说明它,但它确实抛出了上述原因的例外。
在a.jar
包中包含以下两个类A
和MyRunnable
。这些由两个独立的类加载器多次加载。
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上。
答案 2 :(得分:3)
最后,有人攻击了String
intern
表中的字符串"a"
。
查看如何完成here的示例。
答案 3 :(得分:0)
那么也许是因为C1是一个抽象类,并且get函数也返回对象(当然是C1的子类),它在返回之前被转换为C1?