📜  Java 创建javap工具(1)

📅  最后修改于: 2023-12-03 14:42:17.851000             🧑  作者: Mango

Java创建javap工具

简介

javap是Java开发中一个非常重要的工具,它可以将class文件中的字节码反汇编成易于理解的文本形式,方便程序员进行代码阅读和调试。本文介绍如何利用Java自己创建一个类似的javap工具。

实现
代码结构
public class Javap {
    public static void main(String[] args) {
        // 解析命令行参数
        ...
        // 获取class文件字节码
        ...
        // 反汇编并输出
        ...
    }
}
解析命令行参数

使用args4j库来解析命令行参数,它可以让我们非常方便地定义和解析命令行参数。下面是一个例子:

public class Javap {
    @Option(name = "-v", usage = "Verbose mode")
    private boolean verbose;

    @Argument(index = 0, required = true, usage = "Class name")
    private String className;

    public static void main(String[] args) {
        Javap javap = new Javap();
        CmdLineParser parser = new CmdLineParser(javap);
        try {
            parser.parseArgument(args);
        } catch (CmdLineException e) {
            System.err.println(e.getMessage());
            parser.printUsage(System.err);
            System.exit(1);
        }
        // 保存命令行参数
        javap.saveCommandLineArguments(args);
        // ...
    }
}
获取class文件字节码

使用ClassLoader来加载class文件,并利用java.lang.instrument.ClassDefinition类将class文件的字节码转换成字节数组。下面是一个例子:

public class Javap {
    private byte[] getClassBytes(String className) throws IOException {
        Class<?> clazz = Class.forName(className);
        URL classUrl = clazz.getResource('/' + className.replace('.', '/') + ".class");
        URLConnection connection = classUrl.openConnection();
        try (InputStream input = connection.getInputStream()) {
            return IOUtils.toByteArray(input);
        }
    }

    public static void main(String[] args) {
        // ...
        byte[] classBytes = javap.getClassBytes(javap.className);
        // ...
    }
}
反汇编并输出

使用Java提供的javax.tools.ToolProvider类加载javax.tools.JavaCompiler,并使用javax.tools.JavaFileManager来提供反汇编服务。下面是一个例子:

public class Javap {
    private void execute() throws Exception {
        // ...
        // 获取Java编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 获取Java文件管理器,用于提供反汇编服务
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        // 将class文件字节码转换成JavaFileObject对象
        JavaFileObject fileObject = new SimpleJavaFileObject(URI.create(className.replaceAll("\\.", "/") + Kind.CLASS.extension), Kind.CLASS) {
            @Override
            public InputStream openInputStream() throws IOException {
                return new ByteArrayInputStream(classBytes);
            }
        };
        // 获取反汇编器
        ForwardingJavaFileManager<JavaFileManager> manager = new ForwardingJavaFileManager<>(fileManager) {
            @Override
            public ClassLoader getClassLoader(Location location) {
                return new ClassLoader() {
                    @Override
                    public Class<?> loadClass(String name) throws ClassNotFoundException {
                        // 反汇编后Java代码
                        String javaCode = getJavaCode(name);
                        if (javaCode != null) {
                            // 输出Java代码
                            System.out.println(javaCode);
                        }
                        return super.loadClass(name);
                    }
                };
            }
        };
        // 编译Java字符串(只有一个空类),触发反汇编
        compiler.getTask(null, manager, null, null, null, Collections.singletonList(new JavaStringObject(className)), Collections.singletonList(fileObject)).call();
        // ...
    }
}
完整代码

下面是完整的代码:

import com.sun.codemodel.internal.JExpr;
import com.sun.codemodel.internal.JExpression;
import com.sun.codemodel.internal.JFormatter;
import com.sun.codemodel.internal.JInvocation;
import com.sun.codemodel.internal.JOp;
import com.sun.codemodel.internal.JType;
import com.sun.codemodel.internal.JVar;
import com.sun.tools.javac.file.BaseFileObject;
import com.sun.tools.javac.util.ListBuffer;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;

import javax.lang.model.SourceVersion;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

public class Javap {
    @Option(name = "-v", usage = "Verbose mode")
    private boolean verbose;

    @Argument(index = 0, required = true, usage = "Class name")
    private String className;

    private List<String> classpath = new ArrayList<>();
    private List<String> commandLineArguments = new ArrayList<>();
    private byte[] classBytes;

    public static void main(String[] args) {
        Javap javap = new Javap();
        CmdLineParser parser = new CmdLineParser(javap);
        try {
            parser.parseArgument(args);
        } catch (CmdLineException e) {
            System.err.println(e.getMessage());
            parser.printUsage(System.err);
            System.exit(1);
        }
        // 保存命令行参数
        javap.saveCommandLineArguments(args);
        try {
            // 获取class文件字节码
            javap.classBytes = javap.getClassBytes(javap.className);
            // 反汇编并输出Java代码
            javap.execute();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void saveCommandLineArguments(String[] args) {
        Collections.addAll(commandLineArguments, args);
    }

    private void execute() throws Exception {
        // 输出完整命令行参数
        if (verbose) {
            System.out.println(String.join(" ", commandLineArguments));
        }
        // 获取Java编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 获取Java文件管理器,用于提供反汇编服务
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        // 将class文件字节码转换成JavaFileObject对象
        JavaFileObject fileObject = new SimpleJavaFileObject(URI.create(className.replaceAll("\\.", "/") + Kind.CLASS.extension), Kind.CLASS) {
            @Override
            public InputStream openInputStream() throws IOException {
                return new ByteArrayInputStream(classBytes);
            }
        };
        // 获取反汇编器
        ForwardingJavaFileManager<JavaFileManager> manager = new ForwardingJavaFileManager<>(fileManager) {
            @Override
            public ClassLoader getClassLoader(Location location) {
                return new ClassLoader() {
                    @Override
                    public Class<?> loadClass(String name) throws ClassNotFoundException {
                        // 反汇编后Java代码
                        String javaCode = getJavaCode(name);
                        if (javaCode != null) {
                            // 输出Java代码
                            System.out.println(javaCode);
                        }
                        return super.loadClass(name);
                    }
                };
            }
        };
        // 编译Java字符串(只有一个空类),触发反汇编
        compiler.getTask(null, manager, null, null, null, Collections.singletonList(new JavaStringObject(className)), Collections.singletonList(fileObject)).call();
    }

    private byte[] getClassBytes(String className) throws IOException {
        Class<?> clazz = Class.forName(className);
        URL classUrl = clazz.getResource('/' + className.replace('.', '/') + ".class");
        URLConnection connection = classUrl.openConnection();
        try (InputStream input = connection.getInputStream()) {
            return IOUtils.toByteArray(input);
        }
    }

    private String getJavaCode(String name) throws ClassNotFoundException {
        // 只反汇编指定类
        if (!name.equals(className)) {
            return null;
        }
        // 获取反汇编后Java代码的输出流
        StringWriter out = new StringWriter();
        PrintWriter writer = new PrintWriter(out);
        // 设置反汇编选项
        List<String> options = new ArrayList<>();
        options.add("-p");
        // 创建反汇编器
        com.sun.tools.javap.Main.Result result = com.sun.tools.javap.Main.run(options.toArray(new String[0]), new String[]{className}, writer, writer);
        writer.flush();
        // 输出反汇编成功
        if (resultOK(result, options)) {
            return formatJavaCode(out.toString());
        } else {
            return null;
        }
    }

    private boolean resultOK(com.sun.tools.javap.Main.Result result, List<String> options) {
        if (result != com.sun.tools.javap.Main.Result.OK) {
            if (verbose) {
                System.err.printf("Error executing javap with options %s: %s%n", options, result);
            }
            return false;
        } else {
            return true;
        }
    }

    private String formatJavaCode(String javaCode) {
        StringBuilder sb = new StringBuilder();
        sb.append("```java\n");
        sb.append(javaCode);
        sb.append("```");
        return sb.toString();
    }

    private static class JavaStringObject extends BaseFileObject {
        private final String name;

        protected JavaStringObject(String className) {
            super(URI.create(className.replaceAll("\\.", "/") + ".java"), Kind.SOURCE);
            this.name = StringUtils.substringAfterLast(className, ".");
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            return "public class " + name + " {}";
        }

        @Override
        public InputStream openInputStream() {
            return new ByteArrayInputStream(new byte[0]);
        }

        @Override
        public OutputStream openOutputStream() {
            return new OutputStream() {
                @Override
                public void write(int b) throws IOException {
                    // ignore
                }
            };
        }

        @Override
        public String getName() {
            return name;
        }
    }

    private static class CmdLineParser extends PosixParser {
        private final Object bean;

        public CmdLineParser(Object bean) {
            this.bean = bean;
        }

        public void parseArgument(String[] args) throws CmdLineException {
            CommandLine cmd = this.parse(buildOptions(), args, false);
            for (Field field : this.bean.getClass().getDeclaredFields()) {
                Option option = field.getAnnotation(Option.class);
                Argument argument = field.getAnnotation(Argument.class);
                if (option != null) {
                    String name = field.getName();
                    name = Character.toUpperCase(name.charAt(0)) + name.substring(1);
                    if (!StringUtils.isEmpty(option.name())) {
                        name = option.name();
                    }
                    Object value;
                    Class<?> type = field.getType();
                    if (type.equals(boolean.class)) {
                        value = cmd.hasOption(name);
                    } else {
                        value = cmd.getOptionValue(name);
                    }
                    try {
                        field.setAccessible(true);
                        field.set(this.bean, value);
                    } catch (IllegalAccessException e) {
                        throw new CmdLineException(e);
                    }
                } else if (argument != null) {
                    String value = cmd.getArgs()[argument.index()];
                    try {
                        field.setAccessible(true);
                        field.set(this.bean, value);
                    } catch (IllegalAccessException e) {
                        throw new CmdLineException(e);
                    }
                } else {
                    // ignore
                }
            }
        }

        public void printUsage(PrintWriter writer) {
            HelpFormatter formatter = new HelpFormatter();
            formatter.printUsage(writer, 80, "javap", buildOptions());
        }

        private Options buildOptions() {
            Options options = new Options();
            for (Field field : this.bean.getClass().getDeclaredFields()) {
                Option option = field.getAnnotation(Option.class);
                if (option != null) {
                    String name = field.getName();
                    name = Character.toUpperCase(name.charAt(0)) + name.substring(1);
                    if (!StringUtils.isEmpty(option.name())) {
                        name = option.name();
                    }
                    String argName = option.usage().split(" ")[0];
                    options.addOption(name, !field.getType().equals(boolean.class), option.usage(), argName);
                } else {
                    Argument argument = field.getAnnotation(Argument.class);
                    if (argument != null) {
                        options.addOption(null, true, argument.usage(), argument.metaVar());
                    } else {
                        // ignore
                    }
                }
            }
            return options;
        }
    }
}
测试

编译Javap.java,并在命令行中运行:

java Javap com.example.TestClass

将会输出反汇编后的Java代码。

结语

javap是Java开发中一个非常实用的工具,通过Java创建自己的javap工具,不仅可以对字节码进行反汇编,而且还可以灵活地处理反汇编后的Java代码,为代码阅读和调试提供了便利。