Как стать автором
Обновить

Инстанцировать не инстанциируемое

Время на прочтение4 мин
Количество просмотров13K
Идея этого топика родилась из этого комментария.

Итак имеем: класс без конструкторов, необходимо создать экземпляр этого класса. На 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. В проекте есть тест который показывает все описанные попытки инстанцировать объект.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
А вы используете багогенерацию в продакшене?
9.74% Да, генерируем байткод19
13.33% Да, генерируем исходный код26
17.95% Да, код генерируется сторонними инструментами (JSP, scripting, etc.)35
74.36% Нет, у нас баги ручной работы145
Проголосовали 195 пользователей. Воздержались 102 пользователя.
Теги:
Хабы:
Всего голосов 18: ↑18 и ↓0+18
Комментарии16

Публикации

Истории

Работа

Java разработчик
322 вакансии

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
19 сентября
CDI Conf 2024
Москва
20 – 22 сентября
BCI Hack Moscow
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
24 сентября
Astra DevConf 2024
МоскваОнлайн
25 сентября
Конференция Yandex Scale 2024
МоскваОнлайн
28 – 29 сентября
Конференция E-CODE
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн