Доступ к закрытым внутренним классам в Java ASM

У меня есть класс, который содержит несколько внутренних классов. Я хотел бы создать дополнительные внутренние классы, которые взаимодействуют с частными внутренними классами времени компиляции, используя библиотеку ASM. Мой код выглядит так:

public class Parent {

  public void generateClass() {
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    cw.visit(49, Opcodes.ACC_PUBLIC, "Parent$OtherChild", null,
             Type.getInternalName(Child.class), new String[]{});
    // .. generate the class
    byte[] bytes = cw.toByteArray();
    Class<?> genClass = myClassLoader.defineClass("Parent$OtherChild", bytes);
  }

  private static class Child {
  }

}

Как показано, простым примером взаимодействия является наследование - я пытаюсь объединить класс OtherChild, который расширяет закрытый внутренний класс Child. Я получаю это сообщение об ошибке, пока загрузчик классов проверяет определение класса:

IllegalAccessError: class Parent$OtherChild cannot access its superclass Parent$Child

Есть ли способ генерировать внутренние классы, которые могут взаимодействовать с другими частными внутренними классами? Вы можете предположить, что это выполняется из "безопасной зоны", где доступен внутренний класс.

благодарю вас

1 ответ

Решение

Правило, что внутренние и внешние классы могут получить доступ к своим private members - это чисто языковая конструкция Java, которая не отражается в проверках доступа JVM. Когда в Java 1.1 были представлены внутренние классы, они были введены таким образом, что не требовали изменений в JVM. С точки зрения JVM, вложенные классы - это обычные (верхнего уровня) классы с некоторой дополнительной игнорируемой метаинформацией.

Когда объявлен внутренний класс private Это обычный уровень доступа к классу "по умолчанию" или пакет-приватный. Когда это объявлено protected, это будет public на уровне JVM.

Когда вложенные классы получают доступ друг к другу private Поля или методы, компилятор будет генерировать синтетические вспомогательные методы с доступом к частному пакету в целевом классе, обеспечивая требуемый доступ.

Таким образом, с точки зрения JVM, вы пытаетесь создать подкласс частного класса пакета, а доллар в имени - это просто обычный символ имени. Сгенерированный класс имеет соответствующее квалифицированное имя, но вы пытаетесь определить его в другом загрузчике классов, поэтому JVM считает эти пакеты не идентичными во время выполнения, несмотря на их идентичное имя.

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

Class<?> genClass = myClassLoader.defineClass("Parent$OtherChild", bytes);

в

Method m=ClassLoader.class.getDeclaredMethod(
    "defineClass", String.class, byte[].class, int.class, int.class);
m.setAccessible(true);
Class<?> genClass=(Class<?>)m.invoke(
    Child.class.getClassLoader(), "Parent$OtherChild", bytes, 0, bytes.length);

В качестве альтернативы, вы можете объявить Child как protected, Так как это public класс на низком уровне, тогда он будет доступен другим загрузчикам классов.

Обратите внимание, что в обоих случаях вы создали не новый внутренний класс, а просто класс с именем Parent$OtherChild расширение внутреннего класса. Единственное отличие - это метаинформация о взаимоотношениях между внешним и внутренним классами, но если вы добавите этот атрибут в свой сгенерированный класс, утверждая, что это был внутренний класс Parent может случиться так, что он будет отклонен верификатором, потому что метаинформация Parent не упоминает о существовании внутреннего класса OtherChild, Это единственное место, где JVM может взглянуть на этот атрибут.

Но кроме отражения отчетов о внутренних классовых отношениях, в любом случае нет никакой функциональной разницы между классами верхнего уровня и вложенными классами. Как уже говорилось, классы на самом деле не имеют уровней доступа protected ни private и для всех остальных обращений к членам вам все равно придется самостоятельно генерировать необходимый код. Если вы не можете изменить код существующих классов Parent или же Parent$Child Вы не можете получить доступ к их private члены, для которых эти синтетические методы доступа еще не существуют...


Начиная с Java 9, существует стандартный способ определения нового класса в доступном контексте, что делает подход "Отражение с переопределением доступа", показанный выше, устаревшим для этого варианта использования, например, следующие работы:

public class Parent {
    public void generateClass() {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        String superType = Type.getInternalName(Child.class);
        cw.visit(49, Opcodes.ACC_PUBLIC, "Parent$OtherChild", null, superType, null);
        MethodVisitor mv = cw.visitMethod(0, "<init>", "()V", null, null);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, superType, "<init>", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(-1, -1);
        mv.visitEnd();
        // etc
        byte[] bytes = cw.toByteArray();
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        try {
            Class<?> genClass = lookup.defineClass(bytes);
            Child ch = (Child)
                lookup.findConstructor(genClass, MethodType.methodType(void.class))
                      .invoke();
            System.out.println(ch);
        } catch(Throwable ex) {
            Logger.getLogger(Parent.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    private static class Child {
        Child() {}
    }
}

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

@Test
    public void changeToPublic() throws Exception {
        String className = "com.github.asm.Parent$Child";
        ClassReader classReader = new ClassReader(className);
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
        ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM6, classWriter) {

            @Override
            public void visitInnerClass(String name, String outerName, String innerName, int access) {
                super.visitInnerClass(name, outerName, innerName, Modifier.PUBLIC);
            }

            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
                super.visit(version, Modifier.PUBLIC, name, signature, superName, interfaces);
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                return super.visitMethod(Modifier.PUBLIC, name, descriptor, signature, exceptions);
            }
        };
        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
        byte[] bytes = classWriter.toByteArray();
        ClassLoaderUtils.defineClass(getClass().getClassLoader(), className, bytes);
        new Parent().generateClass();
    }
Другие вопросы по тегам