这里就不讲解啥是动态编译了,原理,区别的文章也有很多。
这里主要使用 线程池;重写 System.out的输出,并保存输出的结果。然后返回到客户端。
1.获取字符串类型的源码,转化成编译后的byte[]数组.
String source :客户端传过来的字符串类型的代码,DiagnosticCollector:编译结果收集器。
private static Pattern CLASS_PATTERN = Pattern.compile("class\\s+([$_a-zA-Z][$_a-zA-Z0-9]*)\\s*");
public static byte[] compile(String source, DiagnosticCollector compileCollector) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager javaFileManager =
new TmpJavaFileManager(compiler.getStandardFileManager(compileCollector, null, null));
// 从源码字符串中匹配类名
Matcher matcher = CLASS_PATTERN.matcher(source);
String className;
if (matcher.find()) {
className = matcher.group(1);
} else {
throw new IllegalArgumentException("No valid class");
}
// 把源码字符串构造成JavaFileObject,供编译使用
JavaFileObject sourceJavaFileObject = new TmpJavaFileObject(className, source);
Boolean result = compiler.getTask(null, javaFileManager, compileCollector,
null, null, Arrays.asList(sourceJavaFileObject)).call();
JavaFileObject bytesJavaFileObject = fileObjectMap.get(className);
if (result && bytesJavaFileObject != null) {
return ((TmpJavaFileObject) bytesJavaFileObject).getCompiledBytes();
}
return null;
}
上面 使用到 jdk自带的tools包下的工具:ToolProvider.getSystemJavaCompiler()
JavaFileManager 接口:java类文件管理器,管理JavaFileObject对象的工具, 需要自己 继承 ForwardingJavaFileManager
JavaFileObject 接口:把源码字符串构造成JavaFileObject. 需要自己 继承 SimpleJavaFileObject 重写 getCharContent
openOutputStream 方法。这样就可以获取 源码转换成字节码的 byte[] 数组。((TmpJavaFileObject) bytesJavaFileObject).getCompiledBytes();
2.在创建的线程池,执行 刚编译好的字节码。
private static final int N_THREAD = 5;
private static final ExecutorService pool = new ThreadPoolExecutor(N_THREAD, N_THREAD,
60L, TimeUnit.SECONDS, new ArrayBlockingQueue(N_THREAD));
//这样保证了 多客户端运行时互相不受影响。
// 运行字节码的main方法
Callable runTask = new Callable() {
@Override
public String call() throws Exception {
// classBytes 源码字节数组
return execute(classBytes);
}
};
Future res = pool.submit(runTask);
// 获取运行结果,处理非客户端代码错误
String runResult;
try {
runResult = res.get(15, TimeUnit.SECONDS);
} catch (InterruptedException e) {
runResult = "Program interrupted.";
} catch (ExecutionException e) {
runResult = e.getCause().getMessage();
} catch (TimeoutException e) {
runResult = "Time Limit Exceeded.";
}
// 重点在于 res.get() 获取的返回是哪的返回。这里就需要用到 System.out.println() 的方法来作为返回的结果。
3. execute(classBytes) 方法的实现。使用字节码修改器 替换掉 java/lang/System 的,自己继承 printStream类实现MyPrintSysteam。
类 MyPrintStream extends printStream{
private ThreadLocal out;
// 使用 ThreadLocal 保证每个线程的输出流互不影响。
private ThreadLocal trouble;
// 判断当前线程是否抛io错误。
}
类 MySystem
private final static printStream out = new MyPrintStream;
//调用 out.toString(); 就可以知道 自己System.out.println(123);输出的结果。
4. new 一个类加载器,把字节数组加载为Class对象。
Class clazz = classLoader.loadByte(bytes);
5.通过反射调用Class对象的main方法。
我们这里统一 已 main函数做入口。
try {
Method mainMethod = clazz.getMethod("main", new Class[] { String[].class });
mainMethod.invoke(null, new String[] { null });
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.getCause().printStackTrace(MySystem.err);
}
// 从MySystem中获取返回结果
String res = MySystem.getBufferString();
MySystem.closeBuffer();
引用外部的说法来说:
我们在使用动态编译时,需要注意以下几点:
(1)在框架中谨慎使用
比如要在Struts中使用动态编译,动态实现一个类,它若继承自ActionSupport就希望它成为一个Action。能做到,但是debug很困难;再比如在Spring中,写一个动态类,要让它动态注入到Spring容器中,这是需要花费老大功夫的。
(2)不要在要求高性能的项目使用
动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。不过,如果是在工具类项目中它则可以很好地发挥其优越性,比如在Eclipse工具中写一个插件,就可以很好地使用动态编译,不用重启即可实现运行、调试功能,非常方便。
(3)动态编译要考虑安全问题
如果你在Web界面上提供了一个功能,允许上传一个Java文件然后运行,那就等于说:“我的机器没有密码,大家都来看我的隐私吧”,这是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。
(4)记录动态编译过程
建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序。
个人觉得,线上项目还是尽量不要使用,可以在开发环境使用,就像jsp那样,即写即用。方便查询bug等。