Browse Source

Merge pull request #1243 from lzpeng723/v5-dev

V5 dev
Golden Looly 5 years ago
parent
commit
f126c6ec34

+ 105 - 0
hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileManager.java

@@ -0,0 +1,105 @@
+package cn.hutool.core.compiler;
+
+import cn.hutool.core.io.IoUtil;
+
+import javax.tools.FileObject;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import java.io.InputStream;
+import java.security.SecureClassLoader;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Java 字节码文件对象
+ * 正常我们使用javac命令编译源码时会将class文件写入到磁盘中,但在运行时动态编译类不适合保存在磁盘中
+ * 我们采取此对象来管理运行时动态编译类生成的字节码
+ *
+ * @author lzpeng
+ * @see JavaSourceCompilerBak#compile()
+ * @see com.sun.tools.javac.api.ClientCodeWrapper.WrappedJavaFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject)
+ */
+final class JavaClassFileManager extends ForwardingJavaFileManager<JavaFileManager> {
+
+    /**
+     * 存储java字节码文件对象映射
+     */
+    private final Map<String, JavaFileObject> javaFileObjectMap = new HashMap<>();
+
+    /**
+     * 加载动态编译生成类的父类加载器
+     */
+    private final ClassLoader parent;
+
+    /**
+     * 构造
+     *
+     * @param parent      父类加载器
+     * @param fileManager 字节码文件管理器
+     * @see JavaSourceCompilerBak#compile()
+     */
+    protected JavaClassFileManager(final ClassLoader parent, final JavaFileManager fileManager) {
+        super(fileManager);
+        if (parent == null) {
+            this.parent = Thread.currentThread().getContextClassLoader();
+        } else {
+            this.parent = parent;
+        }
+    }
+
+    /**
+     * 获得动态编译生成的类的类加载器
+     *
+     * @param location 源码位置
+     * @return 动态编译生成的类的类加载器
+     * @see JavaSourceCompilerBak#compile()
+     */
+    @Override
+    public ClassLoader getClassLoader(final Location location) {
+        return new SecureClassLoader(parent) {
+
+            /**
+             * 查找类
+             * @param name 类名
+             * @return 类的class对象
+             * @throws ClassNotFoundException 未找到类异常
+             */
+            @Override
+            protected Class<?> findClass(final String name) throws ClassNotFoundException {
+                final JavaFileObject javaFileObject = javaFileObjectMap.get(name);
+                if (javaFileObject != null) {
+                    try {
+                        final InputStream inputStream = javaFileObject.openInputStream();
+                        final byte[] bytes = IoUtil.readBytes(inputStream);
+                        final Class<?> c = defineClass(name, bytes, 0, bytes.length);
+                        return c;
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+                throw new ClassNotFoundException(name);
+            }
+        };
+    }
+
+    /**
+     * 获得Java字节码文件对象
+     * 编译器编译源码时会将Java源码对象编译转为Java字节码对象
+     *
+     * @param location  源码位置
+     * @param className 类名
+     * @param kind      文件类型
+     * @param sibling   将Java源码对象
+     * @return Java字节码文件对象
+     * @see com.sun.tools.javac.api.lientCodeWrapper.WrappedJavaFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject)
+     */
+    @Override
+    public JavaFileObject getJavaFileForOutput(final Location location, final String className, final Kind kind, final FileObject sibling) {
+        final JavaFileObject javaFileObject = new JavaClassFileObject(className, kind);
+        javaFileObjectMap.put(className, javaFileObject);
+        return javaFileObject;
+    }
+
+}

+ 63 - 0
hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileObject.java

@@ -0,0 +1,63 @@
+package cn.hutool.core.compiler;
+
+
+import javax.tools.SimpleJavaFileObject;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+
+/**
+ * Java 字节码文件对象
+ *
+ * @author lzpeng
+ * @see JavaClassFileManager#getClassLoader(javax.tools.JavaFileManager.Location
+ * @see JavaClassFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject)
+ * @see com.sun.tools.javac.jvm.ClassWriter.ClassWriter#writeClass(com.sun.tools.javac.code.Symbol.ClassSymbol)
+ */
+final class JavaClassFileObject extends SimpleJavaFileObject {
+
+    /**
+     * 字节码输出流
+     */
+    private final ByteArrayOutputStream byteArrayOutputStream;
+
+    /**
+     * 构造
+     *
+     * @param className 需要编译的类名
+     * @param kind      需要编译的文件类型
+     * @see JavaClassFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject)
+     */
+    protected JavaClassFileObject(final String className, final Kind kind) {
+        super(URI.create("string:///" + className.replaceAll("\\.", "/") + kind.extension), kind);
+        this.byteArrayOutputStream = new ByteArrayOutputStream();
+    }
+
+    /**
+     * 获得字节码输入流
+     * 编译器编辑源码后,我们将通过此输出流获得编译后的字节码,以便运行时加载类
+     *
+     * @return 字节码输入流
+     * @see JavaClassFileManager#getClassLoader(javax.tools.JavaFileManager.Location)
+     */
+    @Override
+    public InputStream openInputStream() {
+        final byte[] bytes = byteArrayOutputStream.toByteArray();
+        return new ByteArrayInputStream(bytes);
+    }
+
+    /**
+     * 获得字节码输出流
+     * 编译器编辑源码时,会将编译结果输出到本输出流中
+     *
+     * @return 字节码输出流
+     * @see com.sun.tools.javac.jvm.ClassWriter.ClassWriter#writeClass(com.sun.tools.javac.code.Symbol.ClassSymbol)
+     */
+    @Override
+    public OutputStream openOutputStream() {
+        return this.byteArrayOutputStream;
+    }
+
+}

+ 292 - 0
hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java

@@ -0,0 +1,292 @@
+package cn.hutool.core.compiler;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.URLUtil;
+
+import javax.tools.*;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileObject.Kind;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Java 源码编译器
+ *
+ * @author lzpeng
+ */
+public final class JavaSourceCompiler {
+
+    /**
+     * java 编译器
+     */
+    private static final JavaCompiler JAVA_COMPILER = ToolProvider.getSystemJavaCompiler();
+
+    /**
+     * 待编译的文件 可以是 .java文件 压缩文件 文件夹 递归搜索文件夹内的zip包和jar包
+     */
+    private final List<File> sourceFileList = new ArrayList<>();
+
+    /**
+     * 编译时需要加入classpath中的文件 可以是 压缩文件 文件夹递归搜索文件夹内的zip包和jar包
+     */
+    private final List<File> libraryFileList = new ArrayList<>();
+
+    /**
+     * 源码映射 key: 类名 value: 类源码
+     */
+    private final Map<String, String> sourceCodeMap = new LinkedHashMap<>();
+
+    /**
+     * 编译类时使用的父类加载器
+     */
+    private final ClassLoader parentClassLoader;
+
+
+    /**
+     * 构造
+     *
+     * @param parent 父类加载器
+     */
+    private JavaSourceCompiler(ClassLoader parent) {
+        this.parentClassLoader = parent;
+    }
+
+
+    /**
+     * 创建Java源码编译器
+     *
+     * @param parent 父类加载器
+     * @return Java源码编译器
+     */
+    public static JavaSourceCompiler create(ClassLoader parent) {
+        return new JavaSourceCompiler(parent);
+    }
+
+
+    /**
+     * 向编译器中加入待编译的文件 支持 .java, 文件夹, 压缩文件 递归搜索文件夹内的压缩文件和jar包
+     *
+     * @param files 待编译的文件 支持 .java, 文件夹, 压缩文件 递归搜索文件夹内的压缩文件和jar包
+     * @return Java源码编译器
+     */
+    public JavaSourceCompiler addSource(final File... files) {
+        if (ArrayUtil.isNotEmpty(files)) {
+            this.sourceFileList.addAll(Arrays.asList(files));
+        }
+        return this;
+    }
+
+    /**
+     * 向编译器中加入待编译的源码Map
+     *
+     * @param sourceCodeMap 源码Map key: 类名 value 源码
+     * @return Java源码编译器
+     */
+    public JavaSourceCompiler addSource(final Map<String, String> sourceCodeMap) {
+        if (MapUtil.isNotEmpty(sourceCodeMap)) {
+            this.sourceCodeMap.putAll(sourceCodeMap);
+        }
+        return this;
+    }
+
+    /**
+     * 加入编译Java源码时所需要的jar包
+     *
+     * @param files 编译Java源码时所需要的jar包
+     * @return Java源码编译器
+     */
+    public JavaSourceCompiler addLibrary(final File... files) {
+        if (ArrayUtil.isNotEmpty(files)) {
+            this.libraryFileList.addAll(Arrays.asList(files));
+        }
+        return this;
+    }
+
+    /**
+     * 向编译器中加入待编译的源码Map
+     *
+     * @param className  类名
+     * @param sourceCode 源码
+     * @return Java文件编译器
+     */
+    public JavaSourceCompiler addSource(final String className, final String sourceCode) {
+        if (className != null && sourceCode != null) {
+            this.sourceCodeMap.put(className, sourceCode);
+        }
+        return this;
+    }
+
+    /**
+     * 编译所有文件并返回类加载器
+     *
+     * @return 类加载器
+     */
+    public ClassLoader compile() {
+        final ClassLoader parent;
+        if (this.parentClassLoader == null) {
+            parent = Thread.currentThread().getContextClassLoader();
+        } else {
+            parent = this.parentClassLoader;
+        }
+        // 获得classPath
+        final List<File> classPath = getClassPath();
+        final URL[] urLs = URLUtil.getURLs(classPath.toArray(new File[0]));
+        final URLClassLoader ucl = URLClassLoader.newInstance(urLs, parent);
+        if (sourceCodeMap.isEmpty() && sourceFileList.isEmpty()) {
+            // 没有需要编译的源码
+            return ucl;
+        }
+        // 没有需要编译的源码文件返回加载zip或jar包的类加载器
+        final Iterable<JavaFileObject> javaFileObjectList = getJavaFileObject();
+        // 创建编译器
+        final JavaFileManager standardJavaFileManager = JAVA_COMPILER.getStandardFileManager(null, null, null);
+        final JavaFileManager javaFileManager = new JavaClassFileManager(ucl, standardJavaFileManager);
+        final DiagnosticCollector<? super JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();
+        final List<String> options = new ArrayList<>();
+        if (!classPath.isEmpty()) {
+            final List<String> cp = classPath.stream().map(File::getAbsolutePath).collect(Collectors.toList());
+            options.add("-cp");
+            options.addAll(cp);
+        }
+        // 编译文件
+        final CompilationTask task = JAVA_COMPILER.getTask(null, javaFileManager, diagnosticCollector,
+                options, null, javaFileObjectList);
+        final Boolean result = task.call();
+        if (Boolean.TRUE.equals(result)) {
+            return javaFileManager.getClassLoader(StandardLocation.CLASS_OUTPUT);
+        } else {
+            // 编译失败,收集错误信息
+            final List<?> diagnostics = diagnosticCollector.getDiagnostics();
+            final String errorMsg = diagnostics.stream().map(String::valueOf)
+                    .collect(Collectors.joining(System.lineSeparator()));
+            // CompileException
+            throw new RuntimeException(errorMsg);
+        }
+    }
+
+    /**
+     * 获得编译源码时需要的classpath
+     *
+     * @return 编译源码时需要的classpath
+     */
+    private List<File> getClassPath() {
+        List<File> classPathFileList = new ArrayList<>();
+        for (File file : libraryFileList) {
+            List<File> jarOrZipFile = FileUtil.loopFiles(file, this::isJarOrZipFile);
+            classPathFileList.addAll(jarOrZipFile);
+            if (file.isDirectory()) {
+                classPathFileList.add(file);
+            }
+        }
+        return classPathFileList;
+    }
+
+    /**
+     * 获得待编译的Java文件对象
+     *
+     * @return 待编译的Java文件对象
+     */
+    private Iterable<JavaFileObject> getJavaFileObject() {
+        final Collection<JavaFileObject> collection = new ArrayList<>();
+        for (File file : sourceFileList) {
+            // .java 文件
+            final List<File> javaFileList = FileUtil.loopFiles(file, this::isJavaFile);
+            for (File javaFile : javaFileList) {
+                collection.add(getJavaFileObjectByJavaFile(javaFile));
+            }
+            // 压缩包
+            final List<File> jarOrZipFileList = FileUtil.loopFiles(file, this::isJarOrZipFile);
+            for (File jarOrZipFile : jarOrZipFileList) {
+                collection.addAll(getJavaFileObjectByZipOrJarFile(jarOrZipFile));
+            }
+        }
+        // 源码Map
+        collection.addAll(getJavaFileObjectByMap(this.sourceCodeMap));
+        return collection;
+    }
+
+    /**
+     * 通过源码Map获得Java文件对象
+     *
+     * @param sourceCodeMap 源码Map
+     * @return Java文件对象集合
+     */
+    private Collection<JavaFileObject> getJavaFileObjectByMap(final Map<String, String> sourceCodeMap) {
+        if (MapUtil.isNotEmpty(sourceCodeMap)) {
+            return sourceCodeMap.entrySet().stream()
+                    .map(entry -> new JavaSourceFileObject(entry.getKey(), entry.getValue(), Kind.SOURCE))
+                    .collect(Collectors.toList());
+        }
+        return Collections.emptySet();
+    }
+
+    /**
+     * 通过.java文件创建Java文件对象
+     *
+     * @param file .java文件
+     * @return Java文件对象
+     */
+    private JavaFileObject getJavaFileObjectByJavaFile(final File file) {
+        return new JavaSourceFileObject(file.toURI(), Kind.SOURCE);
+    }
+
+    /**
+     * 通过zip包或jar包创建Java文件对象
+     *
+     * @param file 压缩文件
+     * @return Java文件对象
+     */
+    private Collection<JavaFileObject> getJavaFileObjectByZipOrJarFile(final File file) {
+        final Collection<JavaFileObject> collection = new ArrayList<>();
+        try {
+            final ZipFile zipFile = new ZipFile(file);
+            final Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+                final ZipEntry zipEntry = entries.nextElement();
+                final String name = zipEntry.getName();
+                if (name.endsWith(".java")) {
+                    final InputStream inputStream = zipFile.getInputStream(zipEntry);
+                    final JavaSourceFileObject fileObject = new JavaSourceFileObject(name, inputStream, Kind.SOURCE);
+                    collection.add(fileObject);
+                }
+            }
+            return collection;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return Collections.emptyList();
+    }
+
+
+    /**
+     * 是否是jar 或 zip 文件
+     *
+     * @param file 文件
+     * @return 是否是jar 或 zip 文件
+     */
+    private boolean isJarOrZipFile(final File file) {
+        final String fileName = file.getName();
+        return fileName.endsWith(".jar") || fileName.endsWith(".zip");
+    }
+
+    /**
+     * 是否是.java文件
+     *
+     * @param file 文件
+     * @return 是否是.java文件
+     */
+    private boolean isJavaFile(final File file) {
+        final String fileName = file.getName();
+        return fileName.endsWith(".java");
+    }
+
+}

+ 98 - 0
hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java

@@ -0,0 +1,98 @@
+package cn.hutool.core.compiler;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.Charset;
+
+import javax.tools.SimpleJavaFileObject;
+
+import cn.hutool.core.io.IoUtil;
+
+/**
+ * Java 源码文件对象
+ *
+ * @author lzpeng
+ * @see JavaSourceCompilerBak#getJavaFileObjectByJavaFile(java.io.File)
+ * @see JavaSourceCompilerBak#getJavaFileObjectByZipOrJarFile(java.io.File)
+ * @see JavaSourceCompilerBak#getJavaFileObject(java.util.Map)
+ * @see com.sun.tools.javac.api.ClientCodeWrapper.WrappedFileObject#getCharContent(boolean)
+ */
+final class JavaSourceFileObject extends SimpleJavaFileObject {
+
+    /**
+     * 输入流
+     */
+    private InputStream inputStream;
+
+    /**
+     * 构造
+     *
+     * @param uri  需要编译的文件uri
+     * @param kind 需要编译的文件类型
+     * @see JavaSourceCompilerBak#getJavaFileObjectByJavaFile(java.io.File)
+     */
+    protected JavaSourceFileObject(URI uri, Kind kind) {
+        super(uri, kind);
+    }
+
+    /**
+     * 构造
+     *
+     * @param name        需要编译的文件名
+     * @param inputStream 输入流
+     * @param kind        需要编译的文件类型
+     * @see JavaSourceCompilerBak#getJavaFileObjectByZipOrJarFile(java.io.File)
+     */
+    protected JavaSourceFileObject(final String name, final InputStream inputStream, final Kind kind) {
+        super(URI.create("string:///" + name), kind);
+        this.inputStream = inputStream;
+    }
+
+    /**
+     * 构造
+     *
+     * @param className 需要编译的类名
+     * @param code      需要编译的类源码
+     * @param kind      需要编译的文件类型
+     * @see JavaSourceCompilerBak#getJavaFileObject(java.util.Map)
+     */
+    protected JavaSourceFileObject(final String className, final String code, final Kind kind) {
+        super(URI.create("string:///" + className.replaceAll("\\.", "/") + kind.extension), kind);
+        this.inputStream = new ByteArrayInputStream(code.getBytes());
+    }
+
+    /**
+     * 获得类源码的输入流
+     *
+     * @return 类源码的输入流
+     * @throws IOException IO 异常
+     */
+    @Override
+    public InputStream openInputStream() throws IOException {
+        if (inputStream == null) {
+            inputStream = toUri().toURL().openStream();
+        }
+        return new BufferedInputStream(inputStream);
+    }
+
+    /**
+     * 获得类源码
+     * 编译器编辑源码前,会通过此方法获取类的源码
+     *
+     * @param ignoreEncodingErrors 是否忽略编码错误
+     * @return 需要编译的类的源码
+     * @throws IOException IO异常
+     * @see com.sun.tools.javac.api.ClientCodeWrapper.WrappedFileObject#getCharContent(boolean)
+     */
+    @Override
+    public CharSequence getCharContent(final boolean ignoreEncodingErrors) throws IOException {
+        final InputStream in = openInputStream();
+        final String code = IoUtil.read(in, Charset.defaultCharset());
+        IoUtil.close(in);
+        return code;
+    }
+
+}

+ 6 - 0
hutool-core/src/main/java/cn/hutool/core/compiler/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 运行时编译java源码,动态从字符串或外部文件加载类
+ *
+ * @author : Lzpeng
+ */
+package cn.hutool.core.compiler;

+ 42 - 0
hutool-core/src/test/java/cn/hutool/core/compiler/JavaSourceCompilerTest.java

@@ -0,0 +1,42 @@
+package cn.hutool.core.compiler;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.ZipUtil;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.InputStream;
+
+/**
+ * Java源码编译器测试
+ *
+ * @author lzpeng
+ */
+public class JavaSourceCompilerTest {
+
+    /**
+     * 测试编译Java源码
+     */
+    @Test
+    public void testCompile() throws ClassNotFoundException {
+        final File libFile = ZipUtil.zip(FileUtil.file("lib.jar"),
+                new String[]{"a/A.class", "a/A$1.class", "a/A$InnerClass.class"},
+                new InputStream[]{
+                        FileUtil.getInputStream("test-compile/a/A.class"),
+                        FileUtil.getInputStream("test-compile/a/A$1.class"),
+                        FileUtil.getInputStream("test-compile/a/A$InnerClass.class")
+                });
+        final ClassLoader classLoader = JavaSourceCompiler.create(null)
+                .addSource(FileUtil.file("test-compile/b/B.java"))
+                .addSource("c.C", FileUtil.readUtf8String("test-compile/c/C.java"))
+                .addLibrary(libFile)
+                .compile();
+        final Class<?> clazz = classLoader.loadClass("c.C");
+        Object obj = ReflectUtil.newInstance(clazz);
+        Assert.assertTrue(String.valueOf(obj).startsWith("c.C@"));
+        FileUtil.del(libFile);
+    }
+
+}

BIN
hutool-core/src/test/resources/test-compile/a/A$1.class


BIN
hutool-core/src/test/resources/test-compile/a/A$InnerClass.class


BIN
hutool-core/src/test/resources/test-compile/a/A.class


+ 24 - 0
hutool-core/src/test/resources/test-compile/a/A.java

@@ -0,0 +1,24 @@
+package a;
+
+import cn.hutool.core.lang.ConsoleTable;
+import cn.hutool.core.lang.caller.CallerUtil;
+
+public class A {
+    private class InnerClass {
+    }
+
+    public A() {
+        new InnerClass() {{
+            int i = 0;
+            Class<?> caller = CallerUtil.getCaller(i);
+            final ConsoleTable t = new ConsoleTable();
+            t.addHeader("类名", "类加载器");
+            System.out.println("初始化 " + getClass() + " 的调用链为: ");
+            while (caller != null) {
+                t.addBody(caller.toString(), caller.getClassLoader().toString());
+                caller = CallerUtil.getCaller(++i);
+            }
+            t.print();
+        }};
+    }
+}

BIN
hutool-core/src/test/resources/test-compile/b/B.class


+ 8 - 0
hutool-core/src/test/resources/test-compile/b/B.java

@@ -0,0 +1,8 @@
+package b;
+import a.A;
+
+public class B {
+    public B() {
+        new A();
+    }
+}

BIN
hutool-core/src/test/resources/test-compile/c/C.class


+ 9 - 0
hutool-core/src/test/resources/test-compile/c/C.java

@@ -0,0 +1,9 @@
+package c;
+
+import b.B;
+
+public class C {
+    public C() {
+        new B();
+    }
+}