写一个在线 Java 脚本执行器
在生产环境中,有时候我们想要快速执行一段代码,但是又不得不经历上线的痛苦(分情况哈,有时候这种痛苦是必须的)或者在某些场景中,不能重启避免破坏现场,那么有个在线脚本执行器就最好不过了。于是在工作之余,便写了这么一个 jrc 小工具 (当然市场上可能会有更好的选择,比如阿里巴巴的arthas,大家如果有更习惯的工具,也可以不参考我这个哈)
这个工具主要就是利用了java自带的javac包里的相关api实现的。先放一段效果图
写一个在线Java脚本执行器
编译代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public JrcResult compile(String javaCode) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
JrcJavaFileManager fileManager = JavaFileManagerFactory.getJavaFileManager(compiler.getStandardFileManager(diagnostics, null, null));
ClassInfo classInfo = getClassFileFromJavaSource(javaCode); List<JavaFileObject> javaFileObjects = new ArrayList<>(); javaFileObjects.add(new StringJavaFileObject(classInfo.className, javaCode));
List<String> options = new ArrayList<>(); options.add("-encoding"); options.add("UTF-8"); options.add("-classpath"); options.add(classpath);
StringWriter outWriter = new StringWriter(); JavaCompiler.CompilationTask task = compiler.getTask(outWriter, fileManager, diagnostics, options, null, javaFileObjects); boolean success = task.call(); }
|
整段代码还是比较简单的
- 获取系统Java编译器
- 获取源码的类信息,比如名称,方法等等
- 将源码存储进
StringJavaFileObject
- 设置cp等进行编译
大体的流程就是这几步就完成了。
整个过程中的难点是如果我们的工程是基于springboot的话,那么需要遍历springboot里面的文件夹和文件,针对springboot的处理可以参考 SpringBoot Loader 浅析
下面主要是说一下 对JavaFileManager的处理。在SpringBootLauncher里只是实现了对springboot fat jar的处理,但是具体和JavaCompiler 的融合还是在 SpringBootJavaFileManager 这个里处理的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| public class SpringBootJavaFileManager extends JrcJavaFileManager {
private static final Logger logger = LoggerFactory.getLogger(SpringBootJavaFileManager.class);
SpringBootLauncher springBootLauncher;
public SpringBootJavaFileManager(StandardJavaFileManager standardManager) { super(standardManager);
try { springBootLauncher = new SpringBootLauncher(); springBootLauncher.launch(); } catch (Exception e) { logger.error("", e); } }
@Override public ClassLoader getClassLoader(Location location) { ClassLoader cl = Thread.currentThread().getContextClassLoader();
ClassLoaderUtil.setClassLoader(new JrcLaunchedURLClassLoader(cl)); return cl; }
@Override public Iterable<JavaFileObject> list(Location location, String packageName, Set set, boolean recurse) throws IOException {
String packagePath = packageName.replaceAll("\\.", "/"); List<SpringBootArchiveEntry> entries = springBootLauncher.getEntries(packagePath);
List<JavaFileObject> list = entries.stream().map(it -> new JarJavaFileObject(it, JavaFileObject.Kind.CLASS)).collect(Collectors.toList());
Iterable<JavaFileObject> superList = super.list(location, packageName, set, recurse); if (superList == null) { return list; }
for (JavaFileObject o : superList) { list.add(o); }
return list; }
@Override public String inferBinaryName(Location location, JavaFileObject file) { if (file instanceof JarJavaFileObject) { return file.getName(); } else { return super.inferBinaryName(location, file); } }
}
|
- 在构造
SpringBootJavaFileManager
实例的时候,开启SpringBoot fat jar的扫描。
- 重写
getClassLoader()
获取springboot loader里的 LaunchedURLClassLoader
,并将它设置成全局的classloader,主要是后面在执行方法时使用该classloader加载类
- 重写
list()
方法,利用SpringBootLauncher
找到springboot fat jar里面的文件和文件夹
- 重写
inferBinaryName()
方法,这是因为在list()方法中返回的是自定义的JarJavaFileObject
,而super.inferBinaryName()
里有个校验,file 必须是 BaseFileObject
,因此这里有个判断,如果是JarJavaFileObject类型
,直接获取名字返回
还有一点是对于classloader的处理,因为在执行方法的时候需要将编译的class字节码加载进jvm里,所以自定义了一个classloader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class JrcLaunchedURLClassLoader implements JrcClassLoader {
private static final Logger logger = LoggerFactory.getLogger(JrcLaunchedURLClassLoader.class);
private ClassLoader launchedURLClassLoader;
public JrcLaunchedURLClassLoader(ClassLoader launchedURLClassLoader) { this.launchedURLClassLoader = launchedURLClassLoader; }
public Class defineClass(String name, byte[] b) { try { return launchedURLClassLoader.loadClass(name); } catch (ClassNotFoundException e) { } try { Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{String.class, byte[].class, int.class, int.class}); boolean isAccessible = defineClassMethod.isAccessible(); if (!isAccessible) { defineClassMethod.setAccessible(true); }
Object result = defineClassMethod.invoke(launchedURLClassLoader, name, b, 0, b.length); defineClassMethod.setAccessible(isAccessible);
return (Class) result; } catch (NoSuchMethodException e) { logger.error("defineClass name:{}", name, e); return null; } catch (IllegalAccessException e) { logger.error("defineClass name:{}", name, e); return null; } catch (InvocationTargetException e) { logger.error("defineClass name:{}", name, e); return null; } } }
|
大体的思路就是这样,具体的细节可以参考 jrc
PS: 当然大家也可以选择不上传java代码,直接将本地编译好的class字节码上传就可以了,这里也就是给大家提供一个思路。
另外更加产品化的东西可以考虑接入maven api实现依赖包的搜索下载,目前只能提供手动jar包上传方式。 ——-》 这个已经实现了