Конструкторы перехвата во время перебазирования в ByteBuddy

Что я пытаюсь сделать

Я пытаюсь переименовать и переименовать класс, чтобы перехватить его конструктор с Bytebuddy 1.6.7

мотивация

Я работаю над системой SAAS, где пользователь может предоставлять аннотированные Java-классы, система должна их инструктировать перед сохранением или запуском. Я разработал конвейер, который выполняет необходимые мне инструменты, но его можно использовать в двух местах.

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

Другой модуль уже загружает классы и использует их сразу и не заботится об именах классов. В этом модуле я хотел бы повторно использовать код из первого модуля и дополнительно изменить имя инструментированного типа перед загрузкой.

Код

Полный рабочий пример здесь.

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

private static void rebaseConstructorSimple(Class<?> clazz) throws InstantiationException, IllegalAccessException {
    new ByteBuddy()
            .rebase(clazz)
            .name(clazz.getName() + "Rebased")
            .constructor(ElementMatchers.any())
            .intercept(SuperMethodCall.INSTANCE.andThen(
                    MethodDelegation.to(new ConstructorInterceptor()
                    )))
            .make()
            .load(clazz.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
            .getLoaded()
            .newInstance();
}

Если я предоставлю корневой класс, я получу следующее исключение:

Exception in thread "main" java.lang.IllegalStateException: Cannot call super (or default) method for public OuterClassRebased()
    at net.bytebuddy.implementation.SuperMethodCall$Appender.apply(SuperMethodCall.java:97)
    at net.bytebuddy.implementation.bytecode.ByteCodeAppender$Compound.apply(ByteCodeAppender.java:134)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyCode(TypeWriter.java:614)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyBody(TypeWriter.java:603)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining$RedefinitionClassVisitor$CodePreservingMethodVisitor.visitCode(TypeWriter.java:3912)
    at net.bytebuddy.jar.asm.MethodVisitor.visitCode(Unknown Source)
    at net.bytebuddy.jar.asm.ClassReader.b(Unknown Source)
    at net.bytebuddy.jar.asm.ClassReader.accept(Unknown Source)
    at net.bytebuddy.jar.asm.ClassReader.accept(Unknown Source)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining.create(TypeWriter.java:2894)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:1612)
    at net.bytebuddy.dynamic.scaffold.inline.RebaseDynamicTypeBuilder.make(RebaseDynamicTypeBuilder.java:200)
    at net.bytebuddy.dynamic.scaffold.inline.AbstractInliningDynamicTypeBuilder.make(AbstractInliningDynamicTypeBuilder.java:92)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:2560)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase$Delegator.make(DynamicType.java:2662)
    at TestConstructorInterceptor.rebaseConstructorSimple(TestConstructorInterceptor.java:35)
    at TestConstructorInterceptor.main(TestConstructorInterceptor.java:89)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

обходные

Обходной путь 1

Как я упоминал ранее, я могу избежать класса, если класс не загружен:

public static void rebaseConstructorNotLoaded(String classPath, String className) throws Exception {
    ClassFileLocator.ForFolder folderClassLoader = new ClassFileLocator.ForFolder(new File(classPath));
    TypePool typePool = TypePool.Default.ofClassPath();
    TypeDescription typeDescription = typePool.describe(className).resolve();

    new ByteBuddy()
            .rebase(typeDescription, folderClassLoader)
            .constructor(ElementMatchers.any())
            .intercept(
                    SuperMethodCall.INSTANCE.andThen(
                            MethodDelegation.to(new ConstructorInterceptor())))
            .make()
            .load(TestConstructorInterceptor.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
            .getLoaded()
            .newInstance();
}

Обходной путь 2

Также мне удалось достичь цели, выполнив следующие действия: Перебазировать с переименовать, сделать. Затем создайте SimpleClassLoader, который содержит только что сгенерированные байты для нового имени класса. Конструктор перехвата

public static void rebaseWithIntermediateMake(Class<?> clazz) throws IllegalAccessException, InstantiationException {
    DynamicType.Unloaded<?> unloaded = new ByteBuddy()
            .rebase(clazz)
            .name(clazz.getName() + "Rebased")
            .make();
    ClassFileLocator dynamicTypesLocator = getClassFileLocatorForDynamicTypes(unloaded);
    TypeDescription typeDescription = unloaded.getTypeDescription();
    new ByteBuddy()
            .rebase(typeDescription, dynamicTypesLocator)
            .constructor(ElementMatchers.any())
            .intercept(
                    SuperMethodCall.INSTANCE.andThen(
                            MethodDelegation.to(new ConstructorInterceptor())))
            .make()
            .load(clazz.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
            .getLoaded()
            .newInstance();
}

private static ClassFileLocator getClassFileLocatorForDynamicTypes(DynamicType.Unloaded<?> unloaded) {
    Map<TypeDescription, byte[]> allTypes = unloaded.getAllTypes();
    Map<String, byte[]> nameByteMap = new HashMap<>();
    for (Map.Entry<TypeDescription, byte[]> entry : allTypes.entrySet()) {
        nameByteMap.put(entry.getKey().getName(), entry.getValue());
    }
    return new ClassFileLocator.Simple(nameByteMap);
}

изучение

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

Когда MethodRebaseResolver создается в RebaseDynamicTypeBuilder, мы получили 0 инструментализированных методов в результирующем methodRebaseResolver. Кажется, что RebasableMatcher во время сопоставления имеет единственное значение в instrumentedMethodTokens:

MethodDescription.Token{name='<init>', modifiers=1, typeVariableTokens=[],
 returnType=void, parameterTokens=[], exceptionTypes=[], annotations=[],
 defaultValue=null, receiverType=class net.bytebuddy.dynamic.TargetType}

но соответствует следующей цели

MethodDescription.Token{name='<init>', modifiers=1, typeVariableTokens=[],
 returnType=void, parameterTokens=[], exceptionTypes=[], annotations=[],
 defaultValue=null, receiverType=class OuterClass}

Токены разные, потому что у них другой тип приемника и конструктор не инструментируется.

Вопрос

Наконец, вопрос: делаю ли я что-то концептуально неправильно, если хочу использовать rebase + rename. Возможно, это ошибка?

1 ответ

Решение

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

Что касается вашей проблемы, вероятно, лучшая идея - объединение в цепочку. Однако лучшим подходом будет использование агента Java. Таким образом, вы можете гарантировать, что вы не загрузите класс преждевременно.

Другие вопросы по тегам