javabytecodejava-bytecode-asmbyte-buddybcel

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

Solution

  • 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>