如何删除/缩小“导入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 代码,这不是必需的。