Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancing a class using Byte Buddy resulted in the generation of unexpected bytecode. #1758

Open
sweetcczhang opened this issue Jan 22, 2025 · 2 comments
Assignees
Labels

Comments

@sweetcczhang
Copy link

sweetcczhang commented Jan 22, 2025

Questtion description:

The problem is like this. When a class implements an interface with default methods, enhancing it with Byte Buddy may result in the generation of bytecode that does not meet expectations.

Version info:

the byte-buddy version is : 1.15.11
JDK is: 1.8

Test case:

To reproduce the issue, a simple test case in below.

A interface code:

public interface JedisCommands {

    Long expire(String key, long seconds);


    default String setex(String key, int seconds, String value) {
        return setex(key, (long) seconds, value);
    }

    String setex(String key, long seconds, String value);
}

this interface JedisCommands has three method and one method setex(String, int, String) has a default implements .

the class implements the JedisCommands interface

    public class Jedis implements JedisCommands {

    @Override
    public Long expire(String key, long seconds) {
        return 100L;
    }

    @Override
    public String setex(String key, long seconds, String value) {
        return "test";
    }
}

A method delegation defines:

public class InstMethodsInterWithOverrideArgs {

    @RuntimeType
    public Object intercept(@This Object obj,
                            @AllArguments Object[] allArguments,
                            @Origin Method method,
                            @Morph OverrideCallable zuper
    ) throws Throwable {
        System.out.println(obj.getClass().toString());

        return null;
    }
}

the OverrideCallable define:

public interface OverrideCallable {
    Object call(Object[] args);
}

the byte-buddy config:

public static void test2() throws Exception {
        TypePool typePool = TypePool.Default.ofSystemLoader();
        DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .rebase(typePool.describe("org.example.Jedis").resolve(), ClassFileLocator.ForClassLoader.ofSystemLoader())
                .method(named("setex").or(named("expire")))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Morph.Binder.install(OverrideCallable.class))
                        .to(new InstMethodsInterWithOverrideArgs())
                )
                .name("ProxyJedis")
                .make();

        DynamicType.Loaded<?> type = dynamicType.load(App.class.getClassLoader());

        try {
            dynamicType.saveIn(new File(App.class.getResource("/").getPath()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        Object jedis = type.getLoaded().newInstance();

//        jedis.setex("zcc", 1L, "zcc");

        Method setex = jedis.getClass().getMethod("setex", String.class, int.class, String.class);
        Object invoke = setex.invoke(jedis, "zcc", 1, "zcc");
        System.out.println(invoke);
    }

I save the byte code in file and named ProxyJedis:

Image

Question:

I can't understand why was the method setex(String,int,String) is delegated to toString method instead of intercept method.

@raphw
Copy link
Owner

raphw commented Jan 23, 2025

There is indeed a little bug when it comes to default methods. It should now work with the master version of Byte Buddy. You can fortunately work-around this limitation with a little trick, simply define a second method in the interceptor:

@RuntimeType
public Object interceptDefault(@This Object obj,
                        @AllArguments Object[] allArguments,
                        @Origin Method method,
                        @Morph(defaultMethod = true) OverrideCallable zuper
) throws Throwable {
    System.out.println(obj.getClass().toString());

    return null;
}

Simply delegate to the original interceptor from it.

@raphw raphw self-assigned this Jan 23, 2025
@raphw raphw added the question label Jan 23, 2025
@sweetcczhang
Copy link
Author

Thank you for your explanation. And I hava another question。It also has default methods, but it not generate unexpected byte code instead of throw a exception.

and the interface like this:

public interface JedisCommands {
    default Long expire(String key, int seconds) {
        return expire(key, (long) seconds);
    }

    Long expire(String key, long seconds);


    String setex(String key, long seconds, String value);

}

the class implements the JedisCommands interface like this:

public class Jedis implements JedisCommands {

    @Override
    public Long expire(String key, long seconds) {
        return 100L;
    }

    @Override
    public String setex(String key, long seconds, String value) {
        return "test";
    }
}

The rest of the code is the same as the one above. and when I compile the code. it throw the exception like this:

Exception in thread "main" java.lang.IllegalArgumentException: None of [protected void java.lang.Object.finalize() throws java.lang.Throwable, public final void java.lang.Object.wait() throws java.lang.InterruptedException, public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException, public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException, public boolean java.lang.Object.equals(java.lang.Object), public java.lang.String java.lang.Object.toString(), public native int java.lang.Object.hashCode(), public final native java.lang.Class java.lang.Object.getClass(), protected native java.lang.Object java.lang.Object.clone() throws java.lang.CloneNotSupportedException, public final native void java.lang.Object.notify(), public final native void java.lang.Object.notifyAll(), public java.lang.Object org.example.InstMethodsInterWithOverrideArgs.intercept(java.lang.Object,java.lang.Object[],java.lang.reflect.Method,org.example.OverrideCallable) throws java.lang.Throwable] allows for delegation from public java.lang.Long org.example.JedisCommands.expire(java.lang.String,int)
	at net.bytebuddy.implementation.bind.MethodDelegationBinder$Processor.bind(MethodDelegationBinder.java:1099)
	at net.bytebuddy.implementation.MethodDelegation$Appender.apply(MethodDelegation.java:1349)
	at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyCode(TypeWriter.java:732)
	at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod$WithBody.applyBody(TypeWriter.java:717)
	at net.bytebuddy.dynamic.scaffold.TypeWriter$MethodPool$Record$ForDefinedMethod.apply(TypeWriter.java:624)
	at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining$WithFullProcessing$RedefinitionClassVisitor.onVisitEnd(TypeWriter.java:5340)
	at net.bytebuddy.utility.visitor.MetadataAwareClassVisitor.visitEnd(MetadataAwareClassVisitor.java:325)
	at net.bytebuddy.jar.asm.ClassVisitor.visitEnd(ClassVisitor.java:395)
	at net.bytebuddy.jar.asm.ClassReader.accept(ClassReader.java:749)
	at net.bytebuddy.utility.AsmClassReader$Default.accept(AsmClassReader.java:132)
	at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining.create(TypeWriter.java:4034)
	at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:2246)
	at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase$UsingTypeWriter.make(DynamicType.java:4085)
	at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:3769)
	at org.example.App.test2(App.java:69)
	at org.example.App.main(App.java:24)

My question is, why is the default setex method generating unexpected bytecode, whereas the default expire method is throwing an exception?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants