Что еще может вызвать ClassCastException в java?

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

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

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

Сценарий:

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

Более поздний код:

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

Этот код генерирует исключение ClassCastException.

В чем могут быть причины?

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

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

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

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

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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

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

Как уже упоминалось, распространенный сценарий, когда это происходит, - это развертывание веб-приложений, где, как правило, AppClassloader очень похож на путь к классам, настроенный на сервере приложений, а затем 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.

Ответ 3

И, наконец, кто-то взломал таблицу String intern для строки "a".

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

Ответ 4

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