获取到需要运行的代码的字节码后,我们接下来需要考虑的是如何通过我们得到的字节码将这个类的 main 方法运行起来,为了方便理解,我们将这个过程进行进一步拆分,分为以下 2 步:
- 类的加载:通过类加载器将字节码加载为 Class 对象;
- 类的运行:通过反射调用 Class 对象的 main 方法。
接下来,我们将对以上两个操作的具体实现细节进行进一步讲解。
首先,我们要注意的是,我们绝不可以通过系统可以提供给我们的应用程序类加载器来加载这个类的,因为这个类加载器是独一份的,如果通过这个类加载器加载了我们的字节码,当客户端对源码进行了修改,再次提交运行时,应用程序类加载器会认为这个类已经加载过了,不会再次加载它,这样除非重启服务器,否则我们永远都无法执行客户端提交来的新代码。
想要客户端提交来的代码可以不修改类名的随便修改,我们需要支持热加载。我们知道,两个类相等需要满足以下 3 个条件:
- 同一个 .class 文件;
- 被同一个虚拟机加载;
- 被同一个类加载器加载;
这 3 条中的前两条都不好破坏,我们只能对第三条加以破坏,即每次都新建一个类加载器加载客户端提交来的字节码。这需要我们实现一个新的类加载器: HotswapClassLoader
。
不过这里要注意,只有这个从客户端传来的类需要被多次加载,而这个类调用的其他类库方法之类的我们还是想要按照原有的双亲委派机制加载的,也就是说,只有我们自己调用 HotswapClassLoader 去加载类时,它直接把字节数组变成 Class 对象,当虚拟机调用它时,它还按照以前的规则使用 loadClass 方法加载类。
想要把存储字节码的自己数组装换成 Class 对象,我们需要通过 protected final Class<?> defineClass(String name, byte[] b, int off, int len)
来完成,所以我们只要新写一个 loadByte 方法把 defineClass 方法开放出来,我们自己要使用 HotswapClassLoader 加载类时就显式调用 loadByte 方法,虚拟机使用 HotswapClassLoader 时会去调用 loadClass 方法。
HotswapClassLoader 具体实现如下:
public class HotSwapClassLoader extends ClassLoader {
public HotSwapClassLoader() {
super(HotSwapClassLoader.class.getClassLoader());
}
public Class loadByte(byte[] classBytes) {
return defineClass(null, classBytes, 0, classBytes.length);
}
}
然后使用我们新写的类加载器,我们就可以通过以下两行代码无数次的加载客户端要运行的类了!
HotSwapClassLoader classLoader = new HotSwapClassLoader();
Class clazz = classLoader.loadByte(modifyBytes);
将类加载进虚拟机之后,我们就可以通过反射机制来运行该类的 main 方法了。
Method mainMethod = clazz.getMethod("main", new Class[] { String[].class });
mainMethod.invoke(null, new String[] { null });
我们并不知道客户端发来的程序的实际运行时间,出于安全的角度考虑,我们需要对其运行时间进行限制。
在 ExecuteStringSourceService 中,我们通过使用 Callable + Future 的方式来限制程序的执行时间,并且对运行过程中可能出现的错误进行 catch,返回给客户端。
ExecutorService pool = Executors.newSingleThreadExecutor();
Callable<String> runTask = new Callable<String>() {
@Override
public String call() throws Exception {
return JavaClassExecutor.execute(classBytes);
}
};
Future<String> res = pool.submit(runTask);
String runResult;
try {
runResult = res.get(RUN_TIME_LIMITED, TimeUnit.SECONDS);
} catch (InterruptedException e) {
runResult = "Program interrupted.";
} catch (ExecutionException e) {
runResult = e.getCause().getMessage();
} catch (TimeoutException e) {
runResult = "Time Limit Exceeded.";
} finally {
pool.shutdown();
}