公司项目有个小需求,需要在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的类加载机制,也看到很多网上的文章。 各种操作,各种转载,各种报错,还不如自己动手试一试来的快。