Mikalai Parafeniuk
Mikalai Parafeniuk

Reputation: 1356

Intercept constructors during rebase in ByteBuddy

What I try to do

I am trying to rebase and rename class to intercept it's constructor with Bytebuddy 1.6.7

Motivation

I am working on SAAS system, where user can provide annotated java classes, system should instrument them before storing or launching. I have developed pipeline which performs instrumentation I need, but it can be used in two places.

First module instruments classes and stores them without loading. It uses class name as "component" id, so I don't want to make a subclass of instrumented type to avoid unnecessary suffixes in class name. That's why I want to use rebase.

Another module instruments already loaded classes and uses them right away and doesn't care about class names. In this module I'd like to reuse code from first module and additionally change name of instrumented type before loading.

Code

Full working example is here.

I am wondering, why the following method works for inner classes, but not for root classes.

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();
}

If I provide root class, I get the following exception:

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)

Workarounds

Workaround 1

As I mentioned before, I can avoid class if class is not loaded:

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();
}

Workaround 2

Also I was able to achieve goal by following steps: Rebase with rename, make. Then create SimpleClassLoader, which contains just generated bytes for new class name. Intercept constructor

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);
}

Investigation

I have inspected ByteBuddy code, and possibly found code, which causes first test to fail.

When MethodRebaseResolver is constructed in RebaseDynamicTypeBuilder here we got 0 instrumentedMethods in resulting methodRebaseResolver. Seems, that RebasableMatcher during matching has single value in instrumentedMethodTokens:

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

but matched against the following target

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

Tokens are different, because they have different receiver type and constructor is not being instrumented.

Question

Finally, the question: am I doing something conceptually wrong if want use rebase+rename. Possibly this is a bug?

Upvotes: 3

Views: 732

Answers (1)

Rafael Winterhalter
Rafael Winterhalter

Reputation: 44032

You are right, you found a bug that I have just fixed. With inner classes, the renaming resolution was slightly different what made it work.

As for your problem, chaining the resolution is probably the best idea. A better approach would however be to use a Java agent. This way, you can guarantee that you do not load a class prematurely.

Upvotes: 1

Related Questions