JAVA中动态编译JAVA代码 计算机 内容发表于2017-12-08

公司项目有个小需求,需要在java代码中读取mysql或者其他渠道来的java代码来执行一段业务逻辑,也就是动态编译然后执行java代码。
常见的这种需求有’热部署’。

在业务系统中动态编译执行java代码是很危险的操作,搞不好容易把自己搭进去。

为了让代码不从java文件中加载,直接从各种渠道得到字符代码,从字符中加载,需要自己继承 SimpleJavaFileObject 类来实现。

public class GenericJavaFileObject extends SimpleJavaFileObject {

    final private String content;

    public GenericJavaFileObject(String className, String content) throws Exception {
        super(URI.create("string:///" + className.replace('.', File.separatorChar)
                + JavaFileObject.Kind.SOURCE.extension), JavaFileObject.Kind.SOURCE);
        this.content = content;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
        return content;
    }

}

动态编译和运行的时候如果要使用不属于 java.lang.* 的包里的对象,需要在classpath中说明jar所在的目录。
在这里我随便拿了个外部的 Page 对象扔进去测试。

类编译加载运行的代码如下:
page 为我假设传入到java脚本内部的变量,动态编译 scriptContent 里的java代码。

public void runScript(Page page, String scriptContent) {
    URLClassLoader classLoader = null;
    try {

        if (StringUtils.isBlank(scriptContent)) {
            LOG.warn("scriptContent is blank.");
            return;
        }

        // 1.获得JavaCompiler,final static

        // 2.创建一个DiagnosticCollector对象,用于获取编译输出信息
        DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>();

        // 3.获得JavaFileManager对象,用于管理需要编译的文件
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnosticCollector, null, null);

        // 4.生成一个JavaFileObject对象,表示需要编译的源文件
        GenericJavaFileObject fileObject = new GenericJavaFileObject("ScriptClass", scriptContent);

        // 5.组成一个JavaFileObject的遍历器
        Iterable<GenericSpiderJavaFileObject> fileObjects = Collections.singletonList(fileObject);

        // 6.编译后文件输出地址
        String compileToPath = "E:\\test\\temp";
        //传入对象的包所在的位置
        String path = Page.class.getProtectionDomain().getCodeSource().getLocation().getFile();
        Iterable<String> options = Arrays.asList("-d", compileToPath, "-classpath", path);

        // 7.获得编译任务
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticCollector, options, null, fileObjects);

        // 8.编译
        Boolean result = task.call();

        if (result) {
            // 编译成功
            LOG.info("compile success");
        } else {
            // 编译失败,打印错误信息
            StringBuilder errorInfo = new StringBuilder();
            for (Diagnostic<?> diagnostic : diagnosticCollector.getDiagnostics()) {
                errorInfo.append("编译错误。 Code:").append(diagnostic.getCode())
                        .append("\r\n").append("Kind:").append(diagnostic.getKind())
                        .append("\r\n").append("StartPosition:").append(diagnostic.getStartPosition())
                        .append("\r\n").append("EndPosition:").append(diagnostic.getEndPosition())
                        .append("\r\n").append("Position:").append(diagnostic.getPosition())
                        .append("\r\n").append("Source:").append(diagnostic.getSource())
                        .append("\r\n").append("Message:").append(diagnostic.getMessage(null))
                        .append("\r\n").append("ColumnNumber:").append(diagnostic.getColumnNumber())
                        .append("\r\n").append("LineNumber:").append(diagnostic.getLineNumber());
            }
            LOG.error("diagnostic:" + errorInfo.toString());
            throw new RuntimeException("compile error");
        }

        // 9.关闭JavaFileManager
        fileManager.close();

        // 运行

        // 10.创建ClassLoader,并设置目录为编译时的输出目录

        File file1 = new File(compileToPath);
        File file2 = new File(path);
        URL[] urls = {file1.toURI().toURL(),file2.toURI().toURL()};

        classLoader = new URLClassLoader(urls, this.getClass().getClassLoader());

        // 11.构建类的Class对象
        Class<?> scriptClass = classLoader.loadClass("net.yuxianghe.generic.script.ScriptClass");

        // 12.获得类的方法
        Method scriptClassMethod = scriptClass.getDeclaredMethod("run", Page.class);

        // 13.创建一个类的实例
        Object object = scriptClass.newInstance();

        // 14.运行函数
        scriptClassMethod.invoke(object, page);

        classLoader.close();
    } catch (Exception e) {
        LOG.error("JavaExecuteServiceImpl.runScript Exception:", e);
        throw new RuntimeException("JavaExecuteServiceImpl.runScript Exception");
    } finally {
        if (classLoader != null) {
            try {
                classLoader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

上面的方法执行后会在 compileToPath 定义的目录生成编译后的class文件,所以在运行时,URLClassLoader 在进行类加载的时候,你需要告诉它字节码文件的位置和依赖的jar包的位置。

测试例子:

@Test
public void testJavaExecuteServiceImpl() {
    String javaCode = "package net.yuxianghe.generic.script; \n" +
            "import us.codecraft.webmagic.Page; \n" +
            "public class ScriptClass {\n" +
            "    public void run(Page page) {\n" +
            "        page.setRawText(\"12312312\");\n" +
            "        System.out.println(\"ScriptClass.run success\");\n" +
            "    }\n" +
            "}\n";
    Page page = new Page();
    //不要在意这个接口,重点在执行的这个 `runScript` 方法
    System.out.println("page.getRawText: "+page.getRawText());
    ExecuteService executeService = new JavaExecuteServiceImpl();
    executeService.runScript(page, javaCode);
    System.out.println("page.getRawText: "+page.getRawText());
}

输出结果:

12312312
ScriptClass.run success

看了看java的类加载机制,也看到很多网上的文章。
各种操作,各种转载,各种报错,还不如自己动手试一试来的快。