Search code examples
javabytecodejava-bytecode-asm

Getting updated byte array from ClassNode with Java ASM library


Using the Java ASM library you can parse and modify compiled Java classes. I'm trying to make a simple class change and store everything back into the Jar file. Example codes I find don't seem to correctly convert the ClassNode to a byte[]:

private static Map<String, byte[]> getClassBytes(final Map<String, ClassNode> classNodes)
{
    final HashMap<String, byte[]> classesMap = new HashMap<>();

    for (final ClassNode classNode : classNodes.values())
    {
        // TODO Get byte array from ClassNode
        final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        final String key = classNode.name;
        final byte[] byteArray = classWriter.toByteArray();
        classesMap.put(key, byteArray);
    }

    return classesMap;
}

Also, in the linked example above, what is MappedClass? I can't find its definition anywhere.

When running the code posted above, the byte[] produced is just a default one and not the actual byte code of the class. That makes sense, there is no call for adding/setting the correct class bytes. How can that be done?


Solution

  • You create a new ClassWriter object, passing in nothing relevant (a settings value, that's all). You then invoke toByteArray on it. Using 5 seconds to think it through, you should draw the conclusion that this cannot possibly work. The actual class content you'd want to write cannot possibly be known by the classWriter instance you just made - nothing in that code flow provides it to this ClassWriter instance.

    The linked code is of highly dubious quality. For example, it leaks the jar file as a resource (it isn't properly resource-guarded). It uses old API (File) with new concepts (.stream()), in fairly silly ways. It does the old 'catch an exception, run e.printStackTrace(), keep going' shtick, which is extremely bad code practice. Its presence in a bare-bones example is suspect; its presence in an example that is clearly intended to be ready to go is inexcusable). It involves outdated third party deps (IOUtils) that aren't actually needed, especially considering that this code wouldn't compile on anything less than java8, so clearly 'at least java 8' is fair game for this example.

    And then, finally, yes, the thing you zoomed in on: That ClassWriter code. Which is just broken.

    I think it's best to forget about this blog post. It's beyond saving.

    Okay, so how DO I do this?

    Well, you've started out by conflating two completely unrelated things 'write a jar file' and 'produce the byte content of a class file' are unrelated, so don't mix em up. You have 2 separate jobs, so write code that does job 1, and test that. Then write code that does job 2, and test that. Then write code that uses the code you wrote for job 1 and 2 and test that, and voila.

    How do I write into an existing jar?

    You don't. Not how it works. You make a new one. If you really need to, once you're done, you'd use Files.move to copy/move it 'over' the old one, thus effectively 'replacing it'.

    How do I make a new jar?

    new JarOutputStream for example. Check this SO answer for some insights on how.

    How do I feed the API of JarOutputStream given an ASM node?

    As for example this tutorial shows, ASM isn't so much an 'edit classes in place' kinda tool, it's an 'edit classes in transit' kinda tool. You make a reader which does nothing by itself, you then pile onto the reader various visitors that transform nodes as they are processed, you then hook this all up to a writer and hit the go button. Part of the point is that ASM may not even hold the entire class file in memory.

    After making the class writer, you're supposed to invoke all sorts of methods on the classwriter to actually 'write' the class file. You 'write' the basic stats such as the class's name and the like, then you 'write' fields, methods, initializers, and so on.

    If you want to write based on an existing class (i.e. transform something that already exists instead of making a class file from scratch), you'd make a reader, and transform as you go. That tutorial I linked shows how to do this.