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


Каковы решения для такого случая? Ответы из StackOverflow:


  1. Через интерфейсы и классы, которые загруженные общим загрузчиком классов
  2. Это невозможно
  3. Reflection
  4. Сериализация / десериализация

Исходя из этого лучшим решением является 1 ответ, также такое решение было реализовано в Tomcat (вы можете прочитать подробную статью здесь).


Но в нашем случае не все классы наследовали интерфейсы или классы (сторонние библиотеки), что делать в таком случае?


И я случайно обнаружил, что это возможно с JNI, используя простой нативный (C++) метод:


JNIEXPORT jobject JNICALL Java_<your class and method name>
  (JNIEnv *env, jobject thisObject, jobject externalObject){
    return externalFoo;
  }

Пример приложения(POC)


Допустим, нам нужно cast Foo между различными загрузчиками классов:


import java.util.Objects;

public class Foo {

    private final String value;

    public Foo(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Foo that = (Foo) o;
        return Objects.equals(value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }

    @Override
    public String toString() {
        return "Foo{" +
                "value='" + value + '\'' +
                '}';
    }

}

ClassCastUtils с нативным методом:


public class ClassCastUtils {

    public static native Foo cast(Object externalFoo);

}

ClassCastUtils.h header:


/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class ClassCastUtils */

#ifndef _Included_ClassCastUtils
#define _Included_ClassCastUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     ClassCastUtils
 * Method:    cast
 * Signature: (Ljava/lang/Object;)LFoo;
 */
JNIEXPORT jobject JNICALL Java_ClassCastUtils_cast
  (JNIEnv *, jobject, jobject);

#ifdef __cplusplus
}
#endif
#endif

ClassCastUtils.cpp c C++ имплементацией:


#include "ClassCastUtils.h"

JNIEXPORT jobject JNICALL Java_ClassCastUtils_cast
  (JNIEnv *env, jobject thisObject, jobject externalFoo){
    return externalFoo;
  }

ByteArrayClassLoader простой загрузчик классов чтобы перезагрузить Foo класс:


import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

public class ByteArrayClassLoader extends URLClassLoader {

    private final String reloadClassName;
    private final byte reloadClassBytes[];

    public ByteArrayClassLoader(Class reloadClass) {
        super(new URL[]{}, null);
        this.reloadClassName = reloadClass.getName();
        this.reloadClassBytes = classToBytes(reloadClass);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (reloadClassName.equals(name)) {
            return defineClass(name, reloadClassBytes, 0, reloadClassBytes.length);
        } else {
            return super.loadClass(name);
        }
    }

    private byte[] classToBytes(Class clazz) {
        try {
            String className = clazz.getName();
            String classAsPath = className.replace('.', '/') + ".class";
            InputStream inputStream = clazz.getClassLoader().getResourceAsStream(classAsPath);
            return inputStream.readAllBytes();
        } catch (Exception e) {
            throw new RuntimeException("Unable to read class's bytes", e);
        }
    }

}

Main наш POC:


import java.lang.reflect.Constructor;

public class Main {

    static {
        System.load("<library name>");
    }

    public static void main(String[] args) throws Exception {       
        ByteArrayClassLoader byteArrayClassLoader = new ByteArrayClassLoader(Foo.class);

        Class<?> externalFooClass = byteArrayClassLoader.loadClass(Foo.class.getName());
        Constructor<?> externalFooConstructor = externalFooClass.getConstructor(String.class);

        Object externalFoo = externalFooConstructor.newInstance("external Foo value");

        System.out.println("\nCast external Foo to Foo via native method\n");
        Foo castedExternalFoo = ClassCastUtils.cast(externalFoo);

        Foo foo = new Foo("Foo value");

        System.out.println("Casted external Foo: " + castedExternalFoo);
        System.out.println("Foo: " + foo);

        System.out.println("\nFoo class 'isAssignableFrom' casted external Foo class :"
                                   + foo.getClass().isAssignableFrom(castedExternalFoo.getClass()));
        System.out.println("Casted external Foo class 'isAssignableFrom' Foo class :"
                                   + castedExternalFoo.getClass().isAssignableFrom(foo.getClass()));

        System.out.println("\nFoo 'instanceof' Foo: " + (foo instanceof Foo));
        System.out.println("External Foo 'instanceof' Foo: " + (castedExternalFoo instanceof Foo));

        try {
            System.out.println("\nDirectly cast external Foo to Foo");
            Foo directCastedExternalFoo = (Foo) externalFoo;
        } catch (ClassCastException e) {
            System.out.println("Caught 'ClassCastException' as expected: "
                                       + e.getMessage());
        }
    }

}

Компилируем


java:


javac *.java

c++:


#MacOS:
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin ClassCastUtils.cpp -o ClassCastUtils.o
g++ -dynamiclib -o <library name>.dylib ClassCastUtils.o -lc

#Linux:
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux ClassCastUtils.cpp -o ClassCastUtils.o
g++ -shared -fPIC -o <library name>.so ClassCastUtils.o -lc

#Windows:
g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 ClassCastUtils.cpp -o ClassCastUtils.o
g++ -shared -o <library name>.dll ClassCastUtils.o -Wl,--add-stdcall-alias

Запускаем


java Main

Получаем вывод в консоль:


Cast external Foo to Foo via native method

Casted external Foo: Foo{value='external Foo value'}
Foo: Foo{value='Foo value'}

Foo class 'isAssignableFrom' casted external Foo class: false
Casted external Foo class 'isAssignableFrom' Foo class: false

Foo 'instanceof' Foo: true
External Foo 'instanceof' Foo: false

Directly cast external Foo to Foo
Caught 'ClassCastException' as expected: class Foo cannot be cast to class Foo (Foo is in unnamed module of loader ByteArrayClassLoader @65e579dc; Foo is in unnamed module of loader 'app')

Обратите внимание что ByteArrayClassLoader использует inputStream.readAllBytes(), поэтому необходима Java 9 или более поздняя версия.
Но это можно переписать например для Java 8.

Как это работает?


Вероятно это связано с нарушением security через Java Native Interface:


Java's Security policies simply do not apply to native code called via JNI, so obviously the native code can violate them at will.

Подводные камни


Из результата мы видим, что casted Foo не isAssignableFrom класса Foo.
Поэтому всегда нужно приводить такие обьекты:


Object externalFoo = ...
Foo castedExternalFoo = ClassCastUtils.cast(externalFoo);
List<Foo> list = new ArrayList<>();
// That's ok
list.add(castedExternalFoo);
// That's not ok, throws ClassCastException 
Foo failedFoo = list.get(0);
// That's ok
Foo foo = ClassCastUtils.cast(list.get(0));

Sources


Source code на Github
Live demo на Repl


Английская версия статьи здесь.

Комментарии (1)


  1. apangin
    11.11.2019 17:47

    Отличный способ выстрелить себе в ногу! Это ни разу не решает проблему class cast, а лишь дурит JVM, обходя небезопасным способом верификацию. Даже если этот хак иногда и работает в интерпретаторе, то в скомпилированном коде, во время GC или в других пограничных случаях может запросто привести к крашу JVM.

    В этом ответе и в комментариях к нему я уже объяснял, почему такие фокусы делать нельзя.