Sergey Polyakov
Sergey Polyakov

Reputation: 41

How to remove/shrink 'import some.clazz.SomeClass;' statement by means of bytecode manipulation library/framework in Java?

I have the following class:

    package some.clazz.client;

    import some.clazz.SomeClass;

    public class SomeClassClient {
        ...
        public SomeClass getProc();
        ...
    }

I've removed/shrunk/deleted this getProc() Java method from SomeClassClient class bytecode by using new MemberRemoval().stripMethods(ElementMatcher); ByteBuddy transformation in net.bytebuddy:byte-buddy-maven-plugin Maven Plugin. But import some.clazz.SomeClass; statement is still present and shown by CFR Java Decompiler!

There are no any another reference to SomeClass class in SomeClassClient class.

How can I remove this import statement from bytecode (really I'm assuming it's located in constant pool)? Because I'm still getting ClassNotFoundException when trying to use 'SomeClassClient' class.

My class

public class MethodsRemover implements net.bytebuddy.build.Plugin {
    ...
    @Override
    public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
                                        TypeDescription typeDescription,
                                        ClassFileLocator classFileLocator) {
        try{
            return builder.visit(new MemberRemoval().stripMethods(
                ElementMatchers.any().and(
                    isAnnotatedWith(Transient.class)
                    .and(
                        t -> {
                            log.info(
                                "ByteBuddy transforming class: {}, strip method: {}",
                                typeDescription.getName(),
                                t
                            );
                            return true;
                        }
                    )
                ).or(
                    target -> Arrays.stream(STRIP_METHODS).anyMatch(
                        m -> {
                            Class<?> methodReturnType = getMethodReturnType(m);
                            String methodName = getMethodName(m);
                            Class<?>[] methodParameters = getMethodParameters(m);
                            return
                                isPublic()
                                .and(returns(
                                    isVoid(methodReturnType)
                                        ? is(TypeDescription.VOID)
                                        : isSubTypeOf(methodReturnType)
                                ))
                                .and(named(methodName))
                                .and(isNoParams(m)
                                    ? takesNoArguments()
                                    : takesArguments(methodParameters)
                                )
                                .and(t -> {
                                    log.info(
                                        "ByteBuddy transforming class: {}, strip method: {}",
                                        typeDescription.getName(),
                                        t
                                    );
                                    return true;
                                }).matches(target)
                            ;
                        }
                    )
                )
            ));
            ...
}

I've added the following EntryPoint and configured it in bytebuddy plugin to use:

public static class EntryPoint implements net.bytebuddy.build.EntryPoint {
    private net.bytebuddy.build.EntryPoint typeStrategyEntryPoint = Default.REDEFINE;

    public EntryPoint() {
    }

    public EntryPoint(net.bytebuddy.build.EntryPoint typeStrategyEntryPoint) {
        this.typeStrategyEntryPoint = typeStrategyEntryPoint;
    }

    @Override
    public ByteBuddy byteBuddy(ClassFileVersion classFileVersion) {
        return typeStrategyEntryPoint
            .byteBuddy(classFileVersion)
            .with(ClassWriterStrategy.Default.CONSTANT_POOL_DISCARDING)
            .ignore(none()); // Traverse through all (include synthetic) methods of type
    }

    @Override
    public DynamicType.Builder<?> transform(TypeDescription typeDescription,
                                            ByteBuddy byteBuddy,
                                            ClassFileLocator classFileLocator,
                                            MethodNameTransformer methodNameTransformer) {
        return typeStrategyEntryPoint
            .transform(typeDescription, byteBuddy, classFileLocator, methodNameTransformer);
    }
}

Upvotes: 1

Views: 299

Answers (2)

Sergey Polyakov
Sergey Polyakov

Reputation: 41

Eventually I've invented a workaround that allows to handle the synthetic bridge methods and at the same time still to use ElementMatcher-s to select methods to remove... As mentioned @Rafael Winterhalter (author) above in its comment: Byte-Buddy lib at its current (v1.10.22 at the moment) version does not handle bridge methods by using its existing MemberRemoval class. So just extend it to remove/strip methods in the following manner:

package com.pany.of.yours.byte.buddy;
    
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.asm.MemberRemoval;
import net.bytebuddy.build.Plugin;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.ClassWriterStrategy;
import net.bytebuddy.dynamic.scaffold.MethodGraph;
import net.bytebuddy.dynamic.scaffold.inline.MethodNameTransformer;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.pool.TypePool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static net.bytebuddy.matcher.ElementMatchers.is;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
import static net.bytebuddy.matcher.ElementMatchers.isBridge;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.none;
import static net.bytebuddy.matcher.ElementMatchers.returns;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments;

...

public class MethodsRemover implements Plugin {
    private static final Logger log = LoggerFactory.getLogger(MethodsRemover.class);

    private static final Object[][] STRIP_METHODS = {
        {SomeClass.class, "getProc", void.class} //,
        // other methods here
    };

    public MethodsRemover() {
    }

    @Override
    public boolean matches(TypeDescription typeDefinitions) {
        // return typeDefinitions.getName().equals("pkg.SomeClass");
        return typeDefinitions.isAssignableTo(SomeClassSuper.class)    }

    @Override
    public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder,
                                        TypeDescription typeDescription,
                                        ClassFileLocator classFileLocator) {
        try{
            log.info(" ByteBuddy processing type =========> {}", typeDescription);
            return builder.visit(new MemberRemovalEx().stripMethods(
                ElementMatchers.none()// <= or you can use ElementMatchers.any();
                .or(t -> {            // <= + .and(..) - as a start point instead.
                    log.debug("ByteBuddy processing      method --> {}", t);
                    return false;
                })
                .or(
                    isAnnotatedWith(Transient.class)
                    .and(t -> {
                        log.info(
                            " ByteBuddy strip transient method ++> {}",
                            t
                        );
                        return true;
                    })
                )
                .or(
                    target -> Arrays.stream(STRIP_METHODS).anyMatch(
                        m -> {
                            Class<?> methodReturnType = getMethodReturnType(m);
                            String methodName = getMethodName(m);
                            Class<?>[] methodParameters = getMethodParameters(m);
                            return
                                isPublic()
                                .and(returns(
                                    isVoid(methodReturnType)
                                        ? is(TypeDescription.VOID)
                                        : isSubTypeOf(methodReturnType)
                                ))
                                .and(named(methodName))
                                .and(isNoParams(m)
                                    ? takesNoArguments()
                                    : takesArguments(methodParameters)
                                )
                                .and(t -> {
                                    log.info(
                                        " ByteBuddy strip signature method ++> {}",
                                        t
                                    );
                                    return true;
                                }).matches(target)
                            ;
                        }
                    )
                )
            ));
        } catch (Exception e) {
            log.error("ByteBuddy error: ", e);
            throw e;
        }
    }

    ...

    public static class EntryPoint implements net.bytebuddy.build.EntryPoint {
        private net.bytebuddy.build.EntryPoint typeStrategyEntryPoint = Default.REDEFINE;

        public EntryPoint() {
        }

        public EntryPoint(net.bytebuddy.build.EntryPoint typeStrategyEntryPoint) {
            this.typeStrategyEntryPoint = typeStrategyEntryPoint;
        }

        @Override
        public ByteBuddy byteBuddy(ClassFileVersion classFileVersion) {
            return typeStrategyEntryPoint
                .byteBuddy(classFileVersion)
                .with(MethodGraph.Compiler.Default.forJVMHierarchy()) // Change hashCode/equals by including a return type
                .with(ClassWriterStrategy.Default.CONSTANT_POOL_DISCARDING) // Recreate constants pool
                .ignore(none()); // Traverse through all (include synthetic) methods of type
        }

        @Override
        public DynamicType.Builder<?> transform(TypeDescription typeDescription,
                                                ByteBuddy byteBuddy,
                                                ClassFileLocator classFileLocator,
                                                MethodNameTransformer methodNameTransformer) {
            return typeStrategyEntryPoint
                .transform(typeDescription, byteBuddy, classFileLocator, methodNameTransformer);
        }
    }

    private class MemberRemovalEx extends MemberRemoval {
        private final Junction<FieldDescription.InDefinedShape> fieldMatcher;
        private final Junction<MethodDescription> methodMatcher;

        public MemberRemovalEx() {
            this(ElementMatchers.none(), ElementMatchers.none());
        }

        public MemberRemovalEx(Junction<FieldDescription.InDefinedShape> fieldMatcher,
                               Junction<MethodDescription> methodMatcher) {
            super(fieldMatcher, methodMatcher);
            this.fieldMatcher = fieldMatcher;
            this.methodMatcher = methodMatcher;
        }

        @Override
        public MemberRemoval stripInvokables(ElementMatcher<? super MethodDescription> matcher) {
            return new MemberRemovalEx(this.fieldMatcher, this.methodMatcher.or(matcher));
        }

        @Override
        public ClassVisitor wrap(TypeDescription instrumentedType,
                                 ClassVisitor classVisitor,
                                 Implementation.Context implementationContext,
                                 TypePool typePool,
                                 FieldList<FieldDescription.InDefinedShape> fields,
                                 MethodList<?> methods,
                                 int writerFlags,
                                 int readerFlags) {
            MethodList<MethodDescription.InDefinedShape> typeBridgeMethods =
                instrumentedType.getDeclaredMethods().filter(isBridge());
            int bridgeMethodCount = typeBridgeMethods.size();
            if (bridgeMethodCount > 0) {
                List<MethodDescription> methodsPlusBridges = new ArrayList<>(
                    methods.size() + bridgeMethodCount
                );
                methodsPlusBridges.addAll(typeBridgeMethods);
                methodsPlusBridges.addAll(methods);
                methods = new MethodList.Explicit<>(methodsPlusBridges);
            }
            return super.wrap(
                instrumentedType,
                classVisitor,
                implementationContext,
                typePool,
                fields,
                methods,
                writerFlags,
                readerFlags
            );
        }
    }
}

And also here is the used byte-buddy Maven plugin configuration:

<build>
    <plugins>
        <plugin>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-maven-plugin</artifactId>
            <version>${byte-buddy-maven-plugin.version}</version>
            <executions>
                <execution>
                    <id>byte.buddy.strip.methods</id>
                    <phase>process-classes</phase>
                    <goals>
                        <goal>transform</goal>
                    </goals>
                    <configuration>
                        <transformations>
                            <transformation>
                                <!-- Next plugin transformer removes @Transient annotated and some predefined methods from entities -->
                                <plugin>com.pany.of.yours.byte.buddy.MethodsRemover</plugin>
                                <!-- Optionally, specify groupId, artifactId, version of the class -->
                            </transformation>
                        </transformations>
                        <!-- Optionally, add 'initialization' block with EntryPoint class -->
                        <initialization>
                            <entryPoint>
                                com.pany.of.yours.byte.buddy.MethodsRemover$EntryPoint
                            </entryPoint>
                        </initialization>
                    </configuration>
                </execution>
            </executions>
            <dependencies>
                <dependency>
                    <groupId>some.your.aux.dependency.group</groupId>
                    <artifactId>dependency-artifact</artifactId>
                    <version>${project.version}</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

Upvotes: 0

Holger
Holger

Reputation: 298499

In an attempt to reproduce your issue, I used the following program using ASM (the library which is also used by Byte-Buddy):

ClassWriter cw = new ClassWriter(0);
cw.visit(52, ACC_ABSTRACT, "Invalid", null, "java/lang/Object", null);
MethodVisitor mv = cw.visitMethod(
    ACC_ABSTRACT|ACC_PUBLIC, "test", "()Lnon/existent/Class;", null, null);
mv.visitEnd();
cw.visitEnd();
byte[] invalidclassBytes = cw.toByteArray();

cw = new ClassWriter(new ClassReader(invalidclassBytes), 0);
cw.visit(52, ACC_ABSTRACT|ACC_INTERFACE, "Test", null, "java/lang/Object", null);
mv = cw.visitMethod(ACC_STATIC|ACC_PUBLIC, "test", "()V", null, null);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello from generated class");
mv.visitMethodInsn(INVOKEVIRTUAL,
    "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
cw.visitEnd();
byte[] classBytes = cw.toByteArray();

MethodHandles.lookup().defineClass(classBytes);
Class.forName("Test").getDeclaredMethod("test").invoke(null);

System.out.println();

Path p = Files.write(Files.createTempFile("Class", "Test.class"), classBytes);
ToolProvider.findFirst("javap")
    .ifPresent(javap -> javap.run(System.out, System.err, "-c", "-v", p.toString()));
Files.delete(p);

try {
    Class<?> cl = MethodHandles.lookup().defineClass(invalidclassBytes);
    System.out.println("defined " + cl);
    cl.getMethods();
}
catch(Error e) {
    System.out.println("got expected error " + e);
}

It first generates bytecode for a class named Invalid containing a method with a return type non.existent.Class. It then generates a class Test using a ClassReader reading the bytecode of first as input to the ClassWriter, which will copy the entire constant pool, including the references to non-existing classes.

This second class, Test, is turned into a runtime class and its test method invoked. Further, the bytecode is dumped to a temporary file and javap run over it, to show the constant pool. Only after these steps, an attempt to create a runtime class for Invalid is made, to provoke an error.

On my machine, it prints:

Hello from generated class

Classfile /C:/Users/███████████/AppData/Local/Temp/Class10921011438737096460Test.class
  Last modified 29.03.2021; size 312 bytes
  SHA-256 checksum 63df4401143b4fb57b4815fc193f3e47fdd4c301cd76fa7f945edb415e14330a
interface Test
  minor version: 0
  major version: 52
  flags: (0x0600) ACC_INTERFACE, ACC_ABSTRACT
  this_class: #8                          // Test
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 1, attributes: 0
Constant pool:
   #1 = Utf8               Invalid
   #2 = Class              #1             // Invalid
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               test
   #6 = Utf8               ()Lnon/existent/Class;
   #7 = Utf8               Test
   #8 = Class              #7             // Test
   #9 = Utf8               ()V
  #10 = Utf8               java/lang/System
  #11 = Class              #10            // java/lang/System
  #12 = Utf8               out
  #13 = Utf8               Ljava/io/PrintStream;
  #14 = NameAndType        #12:#13        // out:Ljava/io/PrintStream;
  #15 = Fieldref           #11.#14        // java/lang/System.out:Ljava/io/PrintStream;
  #16 = Utf8               Hello from generated class
  #17 = String             #16            // Hello from generated class
  #18 = Utf8               java/io/PrintStream
  #19 = Class              #18            // java/io/PrintStream
  #20 = Utf8               println
  #21 = Utf8               (Ljava/lang/String;)V
  #22 = NameAndType        #20:#21        // println:(Ljava/lang/String;)V
  #23 = Methodref          #19.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #24 = Utf8               Code
{
  public static void test();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=0
         0: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #17                 // String Hello from generated class
         5: invokevirtual #23                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}
defined class Invalid
got expected error java.lang.NoClassDefFoundError: non/existent/Class

It shows that the signature of the first class’ method ()Lnon/existent/Class; is present in the second class file but since there is no method definition pointing to it, it’s just an unused UTF-8 type entry without any hint about containing type references, so it can’t cause any harm.

But it’s even shown that with the widespread Hotspot JVM, having a real class entry pointing to the yet-not-defined class Invalid doesn’t prevent us from loading and using the class Test.

Even more interestingly, the attempt to define a runtime class for Invalid succeeded too, as the message “defined class Invalid” has been printed. It required an actual operation stumbling over the absent non/existent/Class, like cl.getMethods() to provoke an error.


I did another step and fed the generated bytecode to CFR on www.javadecompilers.com. It produced

/*
 * Decompiled with CFR 0.150.
 */
interface Test {
    public static void test() {
        System.out.println("Hello from generated class");
    }
}

showing that those dangling entries of the constant pool did not led to the generation of import statements.


It all indicates that your assumption that there is no active use of the class SomeClass in your transformed class is wrong. There must be an active use of the class that causes the exception and the generation of the import statement.

It’s also worth noting that in the other direction, compiling source code containing import statements of otherwise unused classes, no reference to those classes will appear in the class file.


The information given in this comment is crucial:

I've forgot to specify that SomeClassClient has a super class and also some interface in its hierarchy which(interface) defines this TProc getProc() method with generic return type which in turn extends AbstractSomeClass and is passed as SomeClass to super class definition.

javap displays:

  • before instrumentation: SomeClass getProc()
  • after instrumentation: AbstractSomeClass getProc()
    Where as CFR disassembler shows only import statement.

I added formatting to the comment text

What you have here, is a bridge method. Since the original class implemented the method with a more specific return type, the compiler added a synthetic method overriding the AbstractSomeClass getProc() method and delegating to the SomeClass getProc().

You removed the SomeClass getProc() but not the bridge method. The bridge method is the code that still has references to the SomeClass. The decompiler produced the import statement as it encountered the reference to SomeClass when processing the bridge method but did not generate source code for the bridge method as for normal code that would be unnecessary as generating source code for the actual target method is sufficient to reproduce the bridge method.

To eliminate the SomeClass reference completely, you must remove both methods from the bytecode. For ordinary Java code, you can simply relax the return type checking, as the Java language doesn’t allow to define multiple methods with the same name and parameter types. So when the template’s return type is a reference type, you may simply match any reference return type, to match any overriding method and all of its bridge methods. You could add a check for the bridge method flag when the return type is a super type of the template’s return type, but, as said, for ordinary Java code, this is not necessary.

Upvotes: 6

Related Questions