如何删除/缩小“导入some.clazz.SomeClass;”通过Java中的字节码操作库/框架来声明?

我有以下课程:

    package some.clazz.client;

    import some.clazz.SomeClass;

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

我已经通过在Maven 插件中使用ByteBuddy 转换getProc()SomeClassClient类字节码中删除/缩小/删除了这个Java 方法。但是语句仍然存在并由!new MemberRemoval().stripMethods(ElementMatcher);net.bytebuddy:byte-buddy-maven-pluginimport some.clazz.SomeClass;CFR Java Decompiler

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

如何从字节码中删除这个导入语句(我真的假设它位于常量池中)?因为我在尝试使用“SomeClassClient”类时仍然遇到 ClassNotFoundException。

我的课

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包含一个具有返回类型的方法non.existent.Class。然后它Test使用ClassReader读取 first 的字节码作为 的输入生成一个类ClassWriter,这将复制整个常量池,包括对不存在类的引用。

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

在我的机器上,它打印:

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

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

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

更有趣的是,定义运行时类的尝试也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语句的生成。


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

还值得注意的是,在另一个方向上,编译包含import其他未使用类的语句的源代码,类文件中将不会出现对这些类的引用。


此评论中提供的信息至关重要:

我忘了指定它SomeClassClient有一个超类,并且在它的层次结构中还有一些接口,它(接口)TProc getProc()用通用返回类型定义了这个方法,而通用返回类型又扩展AbstractSomeClass并作为SomeClass超类定义传递。

javap 显示:

  • 仪表前: SomeClass getProc()
  • 检测后:AbstractSomeClass getProc()
    CFR 反汇编程序仅显示导入语句。

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

你在这里拥有的是一种桥接方法。由于原始类使用更具体的返回类型实现了该方法,因此编译器添加了一个合成方法来覆盖该AbstractSomeClass getProc()方法并将其委托给SomeClass getProc().

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

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


以上是如何删除/缩小“导入some.clazz.SomeClass;”通过Java中的字节码操作库/框架来声明?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>