Идея этого топика родилась из этого комментария.
Итак имеем: класс без конструкторов, необходимо создать экземпляр этого класса. На Java такого сделать нельзя, так что придется напрямую манипулировать байткодом. Идея состоит в том, чтобы вызвать NEW, но при этом не вызывать <init>. Но тут есть трудность, спецификация JVM говорит что так делать нельзя,
Я взял ASM и начал эксперименты.
Попытка сделать «в лоб»
Для начала попытаемся реализовать идею напрямую, генерируем код метода который должен инстанцировать объект без конструктора:
MethodVisitor methodVisitor = classVisitor.visitMethod(ACC_PUBLIC | ACC_STATIC,
FACTORY_METHOD_NAME,
Type.getMethodDescriptor(Type.getObjectType(classToInstantiateInternalName)),
null,
null);
methodVisitor.visitCode();
methodVisitor.visitTypeInsn(NEW, classToInstantiateInternalName);
if (invokeObjectConstructor) {
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", Type.getMethodDescriptor(Type.VOID_TYPE), false);
}
methodVisitor.visitInsn(ARETURN);
methodVisitor.visitMaxs(1, 0);
methodVisitor.visitEnd();
Есть два варианта генерации, вообще без вызова <init> или с вызовом <init> от другого класса (в данном случае Object). Как и ожидалось это не сработало, в обоих случаях получаем VerifyError, в первом случае с сообщением «Expecting to find object/array on stack» (тот самый type uninitialized), во втором «Call to wrong initialization method».
Где-то я это уже видел
Если почитать описание того как работает сериализация, то там сказано что конструктор класса не вызывается, вызывается только конструктор по умолчанию первого суперкласса не реализующего Serializable. Это именно то что нам нужно.
Если покопаться в исходниках ObjectStreamClass, то можно найти магию в виде sun.reflect.ReflectionFactory.newConstructorForSerialization(). Попытаемся ее использовать:
Constructor<Object> objCons = Object.class.getConstructor();
Constructor<?> c = sun.reflect.ReflectionFactory.getReflectionFactory().newConstructorForSerialization(noConstructorClass, objCons);
Object instance = c.newInstance();
logger.info("Instance: {}", instance);
И это сработало!
Импортозамещение
Осталось понять, как это делает ReflectionFactory и попытаться воспроизвести импортную технологию в отечественных реалиях. Покопавшись в исходниках OpenJDK можно понять, что там в итоге генерируется специальный класс который и делает всю грязную работу. Немного пошаманив с исходниками OpenJDK получаем байткод и видим «фигу», там тот же самый код что и у нас, но тут он работает а у нас нет.
public class sun.reflect.GeneratedSerializationConstructorAccessor1 extends sun.reflect.SerializationConstructorAccessorImpl { public sun.reflect.GeneratedSerializationConstructorAccessor1(); Code: 0: aload_0 1: invokespecial #36 // Method sun/reflect/SerializationConstructorAccessorImpl."<init>":()V 4: return public java.lang.Object newInstance(java.lang.Object[]) throws java.lang.reflect.InvocationTargetException; Code: 0: new #6 // class com/example/test/classes/NoConstructor 3: dup 4: aload_1 5: ifnull 24 8: aload_1 9: arraylength 10: sipush 0 13: if_icmpeq 24 16: new #22 // class java/lang/IllegalArgumentException 19: dup 20: invokespecial #29 // Method java/lang/IllegalArgumentException."<init>":()V 23: athrow 24: invokespecial #12 // Method java/lang/Object."<init>":()V 27: areturn 28: invokespecial #42 // Method java/lang/Object.toString:()Ljava/lang/String; 31: new #22 // class java/lang/IllegalArgumentException 34: dup_x1 35: swap 36: invokespecial #32 // Method java/lang/IllegalArgumentException."<init>":(Ljava/lang/String;)V 39: athrow 40: new #24 // class java/lang/reflect/InvocationTargetException 43: dup_x1 44: swap 45: invokespecial #35 // Method java/lang/reflect/InvocationTargetException."<init>":(Ljava/lang/Throwable;)V 48: athrow Exception table: from to target type 0 24 28 Class java/lang/ClassCastException 0 24 28 Class java/lang/NullPointerException 24 27 40 Class java/lang/Throwable }
Там много мусора связанного с обработкой исключений и проверкой аргументов, но реально работающие строки это 0, 3, 24, 27
В попытках разобраться открываем sun.reflect.SerializationConstructorAccessorImpl и видим там:
Java serialization (in java.io) expects to be able to instantiate a class and invoke a no-arg constructor of that class's first non-Serializable superclass. This is not a valid operation according to the VM specification; one can not (for classes A and B, where B is a subclass of A) write «new B; invokespecial A()» without getting a verification error.
In all other respects, the bytecode-based reflection framework can be reused for this purpose. This marker class was originally known to the VM and verification disabled for it and all subclasses, but the bug fix for 4486457 necessitated disabling verification for all of the dynamically-generated bytecodes associated with reflection. This class has been left in place to make future debugging easier.
Вот он, источник сильного колдунства ReflectionFactory.
Теперь дело за малым, наследуемся от этого класса (кстати баг 4486457, рекомендует наследоваться от MagicAccessorImpl) и пробуем провернуть то же самое. И все работает, причем работают оба варианта как с вызовом <init> от Object так и вообще без него. Так что похоже, что type uninitialized это понятие валидатора байткода, а не реально существующая сущность.
Весь код доступен на Bitbucket. В проекте есть тест который показывает все описанные попытки инстанцировать объект.
Автор: LSD