Что еще может бросить ClassCastException в Java?

Это вопрос интервью.

Интервью окончено, но этот вопрос все еще у меня в голове.

Я не могу спросить интервьюера, так как я не получил работу.

Сценарий:

  • положить объект класса C1 в кеш с ключом "a"

Позже код:

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

Этот код вызывает исключение ClassCastException.

Какие могут быть причины?

Я сказал, потому что кто-то другой положил другой объект с тем же ключом и переписал его. Мне сказали нет, подумайте о других возможностях.

Я сказал, что, возможно, jar, определяющий класс C1, не был доступен на этом узле (не уверен, приведет ли это к приведению класса или к исключению ClassNotFoundException, но я теперь ухватился за любое преимущество. Тогда я сказал, что, возможно, неправильная версия класса? Они сказали, что один и тот же сосуд класса C1 есть во всех узлах).

Редактировать / Добавить Спросил, бросил ли get ClassCast, но ему сказали нет. после этого я сказал ему, что мои действия по решению такой проблемы будут состоять в том, чтобы добавить тестовый jsp, который будет имитировать действия и поставить лучшую запись (трассировку стека) после исключения. это была вторая часть вопроса (почему и что бы вы сделали, если бы это произошло в производстве)

У кого-нибудь еще есть идеи о том, почему получение кэша может привести к проблеме приведения?

4 ответа

Решение

Одной из причин может быть то, что часть кода, вставляющая объект, использует загрузчик классов, отличный от кода, извлекающего его.
Экземпляр класса не может быть приведен к тому же классу, загруженному другим загрузчиком классов.

Ответ на редактирование:

Что бы вы сделали, если бы это произошло в производстве?

Обычно это происходит, когда каждый из модулей чтения и вставки содержит один и тот же сосуд, содержащий C1,
Поскольку большинство контейнеров сначала пробуют родительский загрузчик классов, а затем локальный загрузчик классов (стратегия " Родитель первый"), общее решение проблемы состоит в том, чтобы вместо этого загрузить класс в ближайшем общем родителе для модулей вставки и чтения.
Если вы переместите модуль, содержащий C1 При подключении к родительскому модулю вы заставляете оба подмодуля получать класс от родительского модуля, удаляя все различия загрузчика классов.

ClassCastException может произойти, если один и тот же класс был загружен несколькими различными загрузчиками классов, и экземпляры классов распределяются между ними.

Рассмотрим следующий пример иерархии.

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

Я думаю, что в целом верно следующее, но могут быть написаны пользовательские загрузчики классов, которые отклоняются от этого.

  • Экземпляры классов, загруженных SystemClassloader, доступны в любом из контекстов загрузчика классов.
  • Экземпляры классов, загруженных AppClassloader, доступны в любом из контекстов загрузчика классов.
  • Экземпляры классов, загруженных Classloader1, не доступны Classloader2.
  • Экземпляры классов, загруженных Classloader2, не доступны Classloader1.

Как уже упоминалось, распространенным сценарием, в котором это происходит, являются развертывания веб-приложений, когда, вообще говоря, AppClassloader очень похож на classpath, настроенный в appserver, а затем Classloader1 и Classloader2 представляют пути к классам развернутых по отдельности веб-приложений.

Если несколько веб-приложений развертывают одни и те же JAR / классы, тогда ClassCastException может возникнуть, если у веб-приложений есть какой-либо механизм для совместного использования объектов, таких как кэш или общий сеанс.

Другой подобный сценарий, в котором это может произойти, - это если классы загружаются веб-приложением, а экземпляры этих классов хранятся в пользовательском сеансе или кэше. Если веб-приложение повторно развернуто, эти классы перезагружаются новым загрузчиком классов, и попытка получить доступ к объектам из сеанса или кэша вызовет это исключение.

Одним из способов избежать этой проблемы в Production является перемещение JAR-файлов выше в иерархии загрузчиков классов. Поэтому вместо включения одного и того же JAR-файла в каждое веб-приложение может быть лучше включить их в путь к классам сервера приложений. При этом классы загружаются только один раз и доступны всем веб-приложениям.

Другой способ избежать этого - это работать только на интерфейсах, которыми пользуются общие объекты. Затем интерфейсы должны быть загружены выше в иерархии загрузчика классов, но сами классы этого не делают. Ваш пример получения объекта из кэша будет таким же, но 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.

И наконец, кто-то взломал Stringintern стол для строки "a",

Смотрите пример того, как это можно сделать здесь.

Ну, может быть, потому что C1 является абстрактным классом, а функция get также возвращает объект (конечно, подкласса C1), который был приведен к C1 перед возвратом?

Другие вопросы по тегам