Полгода назад я работал на проекте, в котором был специфический случай, когда один и тот же класс мог быть загружен разными загрузчиками классов, и в этом случае мы получили очевидный ClassCastException.
Каковы решения для такого случая? Ответы из StackOverflow:
Через интерфейсы и классы, которые загруженные общим загрузчиком классов
Это невозможно
Reflection
Сериализация / десериализация
Исходя из этого лучшим решением является 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
Английская версия статьи здесь.
apangin
Отличный способ выстрелить себе в ногу! Это ни разу не решает проблему class cast, а лишь дурит JVM, обходя небезопасным способом верификацию. Даже если этот хак иногда и работает в интерпретаторе, то в скомпилированном коде, во время GC или в других пограничных случаях может запросто привести к крашу JVM.
В этом ответе и в комментариях к нему я уже объяснял, почему такие фокусы делать нельзя.