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

问题描述

我有以下课程:

    package some.clazz.client;

    import some.clazz.someClass;

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

我已经从 getProc() 类字节码中移除/缩小/删除了这个 SomeClassClient Java 方法 通过使用 new MemberRemoval().stripMethods(ElementMatcher); ByteBuddy 转换 在 net.bytebuddy:byte-buddy-maven-plugin Maven 插件中。 但是 import 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: {},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);
mv.visitEnd();
cw.visitEnd();
byte[] invalidclassBytes = cw.toByteArray();

cw = new ClassWriter(new ClassReader(invalidclassBytes),0);
cw.visit(52,ACC_ABSTRACT|ACC_INTERFACE,"Test",null);
mv = cw.visitMethod(ACC_STATIC|ACC_PUBLIC,"()V",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 读取 first 的字节码作为 ClassReader 的输入生成一个类 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 语句的源代码,否则不会在类文件中出现对这些类的引用。


this comment 中给出的信息至关重要:

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

javap 显示:

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

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

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

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

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

,

最终我发明了一种解决方法,允许处理合成桥接方法,同时仍然使用 ElementMatcher-s 来选择方法以删除... 正如上面@Rafael Winterhalter(作者)在其评论中提到的:当前(目前为 v1.10.22)版本的 Byte-Buddy lib 不使用其现有的 MemberRemoval 类处理桥接方法。因此,只需通过以下方式将其扩展为 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
            );
        }
    }
}

还有这里使用的字节伙伴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>