如何在Java中通过字节码操作library/framework来remove/shrink'import some.clazz.SomeClass;'语句?

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

我有以下 class:

    package some.clazz.client;

    import some.clazz.SomeClass;

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

我 removed/shrunk/deleted 这个 getProc() Java 方法来自 SomeClassClient class 字节码 通过使用 new MemberRemoval().stripMethods(ElementMatcher); ByteBuddy 转换 在 net.bytebuddy:byte-buddy-maven-plugin Maven 插件中。 但是 import some.clazz.SomeClass; 语句仍然存在并显示为 CFR Java Decompiler!

SomeClassClient class 中没有对 SomeClass class 的任何其他引用。

如何从字节码中删除这个导入语句(实际上我假设它位于常量池中)? 因为我在尝试使用 'SomeClassClient' class.

时仍然遇到 ClassNotFoundException

我的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)
                            ;
                        }
                    )
                )
            ));
            ...
}

我添加了以下 EntryPoint 并在 bytebuddy 插件中配置它以供使用:

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

为了重现您的问题,我使用了以下使用 ASM 的程序(该库也被 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);
}

它首先为名为 Invalid 的 class 生成字节码,其中包含 return 类型 non.existent.Class 的方法。然后它使用 ClassReader 读取 first 的字节码作为 ClassWriter 的输入生成 class Test,这将复制整个常量池,包括对非现有 classes.

第二个 class、Test 变成 运行 时间 class 并调用其 test 方法。此外,字节码被转储到一个临时文件和 javap 运行 上面,以显示常量池。只有在这些步骤之后,才会尝试为 Invalid 创建 运行 时间 class,从而引发错误。

在我的机器上,它打印:

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

它表明第一个 class 方法 ()Lnon/existent/Class; 的签名存在于第二个 class 文件中,但是由于没有指向它的方法定义,它只是一个未使用的 UTF-8 类型条目,没有任何关于包含类型引用的提示,因此不会造成任何伤害。

但它甚至表明,使用广泛的 Hotspot JVM,有一个真正的 class 入口指向尚未定义的 class Invalid 并不会阻止我们加载并使用 class Test.

更有趣的是,尝试为 Invalid 定义 运行time class 的尝试也成功了,因为已打印消息“defined class Invalid”。它需要一个实际的操作绊倒 non/existent/Class,比如 cl.getMethods() 来引发错误。


我又做了一步,将生成的字节码提供给 www.javadecompilers.com 上的 CFR。它产生了

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

显示常量池的那些悬空条目并未导致生成 import 语句。


这一切都表明您假设在转换后的 class 中没有主动使用 class SomeClass 是错误的。必须主动使用导致异常和生成 import 语句的 class。

同样值得注意的是,在另一个方向上,编译源代码包含 import 其他未使用的 class 语句的源代码,不会在 class 中出现对那些 class 的引用=89=] 文件.


中给出的信息很关键:

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.

我在评论文本中添加了格式

你这里有一个 bridge method。由于最初的 class 使用更具体的 return 类型实现了该方法,因此编译器添加了一个合成方法来覆盖 AbstractSomeClass getProc() 方法并委托给 SomeClass getProc().

您删除了 SomeClass getProc() 但没有删除桥接方法。桥接方法是仍然引用 SomeClass 的代码。反编译器生成了 import 语句,因为它在处理桥接方法时遇到了对 SomeClass 的引用,但没有像正常代码那样生成桥接方法的源代码,因为生成源代码是不必要的实际目标方法足以重现桥接方法。

要完全消除 SomeClass 引用,您必须从字节码中删除这两个方法。对于普通的 Java 代码,你可以简单地放宽 return 类型检查,因为 Java 语言不允许定义多个具有相同名称和参数类型的方法。因此,当模板的 return 类型是引用类型时,您可以简单地匹配任何引用 return 类型,以匹配任何覆盖方法及其所有桥接方法。当 return 类型是模板的 return 类型的超类型时,您可以添加对桥接方法标志的检查,但是,如前所述,对于普通 Java 代码,这不是必需的.

最终我发明了一种解决方法,允许处理合成桥方法,同时仍然使用 ElementMatcher-s 到 select 方法来删​​除... 正如@Rafael Winterhalter(作者)在上面的评论中提到的:Byte-Buddy lib 在其当前版本(目前为 v1.10.22)中不使用其现有的 MemberRemoval class 来处理桥接方法。所以只需按以下方式将其扩展到 remove/strip 方法:

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

这里还有使用的byte-buddy Maven插件配置:

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