java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > java编译源文件

java代码实现编译源文件

作者:倚栏听风雨

这篇文章主要为大家详细介绍了Java通过 JavaCompiler 实现编译源文件的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下

原理

从Java 6开始,引入了Java代码重写过的编译器接口,使得我们可以在运行时编译Java源代码,然后再通过类加载器将编译好的类加载进JVM,这种在运行时编译代码的操作就叫做动态编译。

主要类库

JavaCompiler -表示java编译器, run方法执行编译操作. 还有一种编译方式是先生成编译任务(CompilationTask), 让后调用CompilationTask的call方法执行编译任务

JavaFileObject -表示一个java源文件对象

JavaFileManager - Java源文件管理类, 管理一系列JavaFileObject

Diagnostic -表示一个诊断信息

DiagnosticListener -诊断信息监听器, 编译过程触发. 生成编译task(JavaCompiler#getTask())或获取FileManager(JavaCompiler#getStandardFileManager())时需要传递DiagnosticListener以便收集诊断信息

流程图

源码文件 -> 字节码文件

    public static void fromJavaFile() {
        // 获取Javac编译器对象
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        // 获取文件管理器:负责管理类文件的输入输出
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        // 获取要被编译的Java源文件
        File file = new File("/Users//github/perfect/perfect-javassist/src/main/java/com/jc/javassist/compiler/TestHello.java");
        // 通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个JavaFileObject
        Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);
        // 生成编译任务
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
        // 执行编译任务
        task.call();
    }

我们这里准备了TestHello.java

public class TestHello {
    public static void main(String[] args) {
        System.out.println("this is a test");
    }
}

我们试着手动加载该class文件,使用类加载器的defineClass方法,可以直接加载字节码文件。

public static Class<?> loadClassFromDisk(String path) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
   // defineClass 为 ClassLoader 类的一个方法,用于加载类
   // 但是这个方法是 protected 的,所以需要通过反射的方式获取这个方法的权限
   Class<ClassLoader> classLoaderClass = ClassLoader.class;
    Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
    defineClass.setAccessible(true);
   // 读取文件系统的 file 为 byte 数组
   File file = new File(path);
   byte[] bytes = new byte[(int) file.length()];
   try (FileInputStream fileInputStream = new FileInputStream(file)) {
        fileInputStream.read(bytes);
    } catch (IOException e) {
        e.printStackTrace();
    }
   // 执行 defineClass 方法 返回 Class 对象
   return (Class<?>) defineClass.invoke(Thread.currentThread().getContextClassLoader(), bytes, 0, bytes.length);
}

由于TestHello中的方法为静态方法,使用class反射机制执行方法

// 执行编译任务
Boolean call = task.call();
if (call) {
    Class<?> o = loadClassFromDisk("/Users/oneyoung/oneyoung/project/my/code/src/main/java/top/oneyoung/dynamic/TestHello.class");
    Method main = o.getMethod("main", String[].class);
    main.invoke(null, new Object[]{new String[]{}});
}

执行结果

| this is a test

源码字符串 -> 字节码文件

在流程图中,getTask().call()会通过调用作为参数传入的JavaFileObject对象的getCharContent()方法获得字符串序列,即源码的读取是通过 JavaFileObject的 getCharContent()方法,那我们只需要重写getCharContent()方法,即可将我们的字符串源码装进JavaFileObject了。构造SourceJavaFileObject实现定制的JavaFileObject对象,用于存储字符串源码

public class SourceJavaFileObject extends SimpleJavaFileObject {
   /**
     * The source code of this "file".
     */
   private final String code;
    SourceJavaFileObject(String name, String code) {
       super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
       this.code = code;
    }
   @Override
   public CharSequence getCharContent(boolean ignoreEncodingErrors) {
       return code;
    }
}

则创建JavaFileObject对象时,变为了:

// 通过源代码字符串获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个JavaFileObject
SourceJavaFileObject javaFileObject = new SourceJavaFileObject("TestHello", "public class TestHello { public static void main(String[] args) { System.out.println("Hello World"); } }");
// 生成编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Collections.singleton(javaFileObject));

执行后,同样编译出了class文件,不过由于没有指定编译的class输出路径,他会默认放在源文件的根目录下

源码字符串 -> 字节码数组

如果我们进行动态编译时,想要直接输入源码字符串并且输出的是字节码数组,而不是输出字节码文件,又该如何实现?实际上,这是从内存中得到源码,再输出到内存的方式。

在getTask().call()源代码执行流程图中,我们可以发现JavaFileObject 的 openOutputStream()方法控制了编译后字节码的输出行为,编译完成后会调用openOutputStream获取输出流,并写数据(字节码)。所以我们需要重写JavaFileObject 的 openOutputStream()方法。

同时在执行流程图中,我们还发现用于输出的JavaFileObject 对象是JavaFileManager的getJavaFileForOutput()方法提供的,所以为了让编译器编译完成后,将编译得到的字节码输出到我们自己构造的JavaFileObject 对象,我们还需要自定义JavaFileManager。

这里我使用类委托的方式,把大部分功能委托给了传入的StandardJavaFileManager,主要是重写了getJavaFileForOutput,使输出编译完成的字节码文件为字节数组。

然后增加了方法getBytesByClassName获取编译完成的字节码字节数组

public class ByteArrayJavaFileManager implements JavaFileManager {
   private static final Logger LOG = LoggerFactory.getLogger(ByteArrayJavaFileManager.class);
   private final StandardJavaFileManager fileManager;
   /**
     * synchronizing due to ConcurrentModificationException
     */
   private final Map<String, ByteArrayOutputStream> buffers = Collections.synchronizedMap(new LinkedHashMap<>());
   public ByteArrayJavaFileManager(StandardJavaFileManager fileManager) {
       this.fileManager = fileManager;
    }
   @Override
   public ClassLoader getClassLoader(Location location) {
       return fileManager.getClassLoader(location);
    }
   @Override
   public synchronized Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse) throws IOException {
       return fileManager.list(location, packageName, kinds, recurse);
    }
   @Override
   public String inferBinaryName(Location location, JavaFileObject file) {
       return fileManager.inferBinaryName(location, file);
    }
   @Override
   public boolean isSameFile(FileObject a, FileObject b) {
       return fileManager.isSameFile(a, b);
    }
   @Override
   public synchronized boolean handleOption(String current, Iterator<String> remaining) {
       return fileManager.handleOption(current, remaining);
    }
   @Override
   public boolean hasLocation(Location location) {
       return fileManager.hasLocation(location);
    }
   @Override
   public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException {
       if (location == StandardLocation.CLASS_OUTPUT) {
           boolean success;
           final byte[] bytes;
           synchronized (buffers) {
                success = buffers.containsKey(className) && kind == Kind.CLASS;
                bytes = buffers.get(className).toByteArray();
            }
           if (success) {
               return new SimpleJavaFileObject(URI.create(className), kind) {
                   @Override
                   public InputStream openInputStream() {
                       return new ByteArrayInputStream(bytes);
                    }
                };
            }
        }
       return fileManager.getJavaFileForInput(location, className, kind);
    }
   @Override
   public JavaFileObject getJavaFileForOutput(Location location, final String className, Kind kind, FileObject sibling) {
       return new SimpleJavaFileObject(URI.create(className), kind) {
           @Override
           public OutputStream openOutputStream() {
               // 字节输出流用与FileManager输出编译完成的字节码文件为字节数组
               ByteArrayOutputStream bos = new ByteArrayOutputStream();
               // 将每个需要加载的类的输出流进行缓存
               buffers.putIfAbsent(className, bos);
               return bos;
            }
        };
    }
   @Override
   public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
       return fileManager.getFileForInput(location, packageName, relativeName);
    }
   @Override
   public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException {
       return fileManager.getFileForOutput(location, packageName, relativeName, sibling);
    }
   @Override
   public void flush() {
       // Do nothing
   }
   @Override
   public void close() throws IOException {
       fileManager.close();
    }
   @Override
   public int isSupportedOption(String option) {
       return fileManager.isSupportedOption(option);
    }
   public void clearBuffers() {
       buffers.clear();
    }
   public Map<String, byte[]> getAllBuffers() {
        Map<String, byte[]> ret = new LinkedHashMap<>(buffers.size() * 2);
        Map<String, ByteArrayOutputStream> compiledClasses = new LinkedHashMap<>(ret.size());
       synchronized (buffers) {
            compiledClasses.putAll(buffers);
        }
        compiledClasses.forEach((k, v) -> ret.put(k, v.toByteArray()));
       return ret;
    }
   public byte[] getBytesByClassName(String className) {
       return buffers.get(className).toByteArray();
    }
}

然后我们修改下之前的执行流程

public static void fromJavaSourceToByteArray1() throws Exception {
   // 获取Javac编译器对象
   JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
   // 获取文件管理器:负责管理类文件的输入输出
   StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
   // 创建自定义的FileManager
   ByteArrayJavaFileManager byteArrayJavaFileManager = new ByteArrayJavaFileManager(fileManager);
   // 通过源代码字符串获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个JavaFileObject
   JavaFileObject javaFileObject = new SourceJavaFileObject("TestHello", "public class TestHello { public static void say(String args) { System.out.println(args); } }");
    JavaCompiler.CompilationTask task = compiler.getTask(null, byteArrayJavaFileManager, null, null, null, Collections.singletonList(javaFileObject));
   // 执行编译任务
   Boolean call = task.call();
   if (Boolean.TRUE.equals(call)) {
       byte[] testHellos = byteArrayJavaFileManager.getBytesByClassName("TestHello");
        Class<ClassLoader> classLoaderClass = ClassLoader.class;
        Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        Object invoke = defineClass.invoke(TestHello.class.getClassLoader(), testHellos, 0, testHellos.length);
        Class clazz = (Class) invoke;
        clazz.getMethod("say", String.class).invoke(null, "你好");
    }
}

以上就是java代码实现编译源文件的详细内容,更多关于java编译源文件的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文