为什么这段简单的代码会输出损坏的 class 文件?

Why is this simple bit of code outputting a corrupted class file?

ClassReader classReader = new ClassReader(new FileInputStream(new File("input.class")));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
Files.write(Paths.get("output.class"), classWriter.toByteArray());

反编译时output.class我得到

package corrupted_class_files;

input.class很好,我可以用ClassReader看说明书就好了,就是不能保存class

您的代码缺少将 class 特征从源实际复制到目标的步骤:

try(FileInputStream in = new FileInputStream(new File("input.class")) {
    ClassReader classReader = new ClassReader(in);
    ClassWriter classWriter = new ClassWriter(classReader, 0);
    classReader.accept(classWriter, 0);
    Files.write(Paths.get("output.class"), classWriter.toByteArray());
}

如果您的转换保留大部分原始 class 文件,则将 ClassReader 传递给 ClassWriter 的构造函数不会复制功能,而是启用优化。或者,正如 the documentation of ClassWriter(ClassReader classReader, int flags) 所说:

Constructs a new ClassWriter object and enables optimizations for "mostly add" bytecode transformations. These optimizations are the following:

  • The constant pool and bootstrap methods from the original class are copied as is in the new class, which saves time. New constant pool entries and new bootstrap methods will be added at the end if necessary, but unused constant pool entries or bootstrap methods won't be removed.
  • Methods that are not transformed are copied as is in the new class, directly from the original class bytecode (i.e. without emitting visit events for all the method instructions), which saves a lot of time. Untransformed methods are detected by the fact that the ClassReader receives MethodVisitor objects that come from a ClassWriter (and not from any other ClassVisitor instance).

因此,当您将 ClassWriter 直接链接到 accept 方法中的 ClassReader 时,所有方法访问者都将来自编写器,因此,它们都是直接复制的.

当您要显着更改 class 或构建新的 class 时,您可以改用构造函数 ClassWriter(int flags)

请注意 COMPUTE_FRAMES 已经暗示了 COMPUTE_MAXS。在上面的示例中,我没有指定任何一个,因为无论如何都会复制这些方法。当您真正要更改或添加代码并需要 COMPUTE_FRAMES 时,值得将 SKIP_FRAMES 指定为 reader,因为当它们从头开始重新计算时,解码原始帧没有意义无论如何。

所以典型的转换设置如下所示:

public class MyClassVisitor extends ClassVisitor {

    public MyClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions) {
        MethodVisitor visitor = super.visitMethod(
            access, name, desc, signature, exceptions);
        if(method matches criteria) {
            visitor = new MyMethodVisitorAdapter(visitor);
        }
        return visitor;
    }
}
try(FileInputStream in = new FileInputStream(new File("input.class"))) {
    ClassReader classReader = new ClassReader(in);
    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
    classReader.accept(new MyClassVisitor(classWriter), ClassReader.SKIP_FRAMES);
    Files.write(Paths.get("output.class"), classWriter.toByteArray());
}

当通过构造函数链接访问者时,您未覆盖的每个方法都将委托给链接的访问者,当最终目标是 ClassWriter 时复制原始构造,分别。 ClassWriter 提供的 MethodVisitor。如果该方法不满足你的转换条件,所以你return原来的MethodVisitor,上面描述的优化仍然适用。访问者方法遵循与 class 访问者相同的模式,覆盖您要拦截的那些方法。

顺便说一句,你应该避免将旧的 I/O 和 NIO 混用。您的代码的简化变体看起来像

ClassReader classReader = new ClassReader(Files.readAllBytes(Paths.get("input.class")));
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);
classReader.accept(new MyClassVisitor(classWriter), ClassReader.SKIP_FRAMES);
Files.write(Paths.get("output.class"), classWriter.toByteArray());

注意读写的对称性

不过,当您使用 getResource 等时,您可能被迫处理 InputStream。但是对于可以通过系统 class 加载程序访问的 classes,您也​​可以将 class 名称传递给 ClassReader(String) 构造函数。