60

这是一道面试题。

面试结束了,但这个问题还在我的脑海里。

我不能问面试官,因为我没有得到这份工作。

设想:

  • 将 C1 类的对象放入带有键“a”的缓存中

后期代码:

C1 c1FromCache = (C1) cache.get("a");

此代码引发 ClassCastException。

原因可能是什么?

我说是因为其他人用相同的键放置了另一个对象,所以覆盖了它。我被告知没有,想想其他可能性。

我说可能定义类 C1 的 jar 在此节点上不可用(不确定这是否会导致类强制转换或 ClassNotFoundException,但我现在正在寻找任何线索。然后我说可能是错误的类版本?他们说所有节点中都存在相同的 C1 类 jar)。

编辑/添加询问 get 是否抛出 ClassCast 但被告知没有。之后我告诉他我解决此类问题的措施是放入一个测试 jsp,该 jsp 将模仿这些操作并在异常之后放置更好的日志记录(堆栈跟踪)。这是问题的第二部分(如果在生产中发生这种情况,为什么以及你会怎么做)

还有其他人对为什么缓存获取会导致强制转换问题有任何想法吗?

4

4 回答 4

52

一个原因可能是插入对象的代码部分使用的类加载器与检索它的代码不同。
不能将类的实例强制转换为由不同类加载器加载的同一类。

对编辑的回应:

如果在生产中发生这种情况,你会怎么做?

这通常发生在读取和插入模块都包含相同的 jar 包时C1
由于大多数容器首先尝试父类加载器,然后是本地类加载器(Parent first策略),因此解决问题的常见方法是加载与插入和读取模块最近的公共父级中的类。
如果将包含C1该类的模块移动到父模块,则强制两个子模块从父模块获取该类,从而消除任何类加载器差异。

于 2013-04-18T20:15:59.360 回答
32

ClassCastException如果同一个类被多个不同的类加载器加载并且类的实例在它们之间共享,则可能会发生这种情况。

考虑以下示例层次结构。

SystemClassloader <--- AppClassloader <--+--- Classloader1
                                         |
                                         +--- Classloader2

我认为总的来说以下是正确的,但可以编写偏离此的自定义类加载器。

  • SystemClassloader 加载的类的实例可以在任何类加载器上下文中访问。
  • AppClassloader 加载的类的实例可以在任何类加载器上下文中访问。
  • Classloader1 加载的类的实例不能被 Classloader2 访问。
  • Classloader2 加载的类的实例不能被 Classloader1 访问。

如前所述,发生这种情况的常见场景是 Web 应用程序部署,一般来说 AppClassloader 非常类似于在应用程序服务器中配置的类路径,然后 Classloader1 和 Classloader2 代表单独部署的 Web 应用程序的类路径。

如果多个 Web 应用程序部署相同的 JAR/类,那么ClassCastException如果 Web 应用程序有任何机制可以共享对象(例如缓存或共享会话),则可能会发生这种情况。

另一种可能发生这种情况的类似情况是,如果类由 Web 应用程序加载,并且这些类的实例存储在用户会话或缓存中。如果重新部署 Web 应用程序,则这些类将由新的类加载器重新加载,并且尝试从会话或缓存访问对象将引发此异常。

在生产环境中避免此问题的一种方法是将 JAR 在类加载器层次结构中向上移动。因此,与其在每个 Web 应用程序中包含相同的 JAR,不如将它们包含在应用程序服务器的类路径中可能会更好。通过这样做,类只加载一次,并且可供所有 Web 应用程序访问。

避免这种情况的另一种方法是仅对共享对象的接口进行操作。然后需要在类加载器层次结构中将接口加载到更高的位置,但类本身不需要。您从缓存中获取对象的示例将是相同的,但C1该类将替换为C1实现的接口。

下面是一些可以独立运行以重新创建此场景的示例代码。它不是最简洁的,当然可能有更好的方法来说明它,但由于上述原因,它确实抛出了异常。

a.jar包以下两个类,AMyRunnable. 它们由两个独立的类加载器多次加载。

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 上

于 2013-04-21T04:10:57.103 回答
3

最后,有人破解了String intern表的字符串"a"

在此处查看如何完成的示例。

于 2013-04-21T20:21:28.613 回答
0

好吧,也许是因为 C1 是一个抽象类,并且 get 函数还返回在返回之前被强制转换为 C1 的对象(当然是 C1 的子类)?

于 2013-04-21T05:01:20.100 回答