在生产环境中,有时候我们想要快速执行一段代码,但是又不得不经历上线的痛苦(分情况哈,有时候这种痛苦是必须的)或者在某些场景中,不能重启避免破坏现场,那么有个在线脚本执行器就最好不过了。于是在工作之余,便写了这么一个 jrc 小工具
这个工具主要就是利用了java自带的javac包里的相关api实现的。先放一段效果图
编译代码
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));
//使用编译选项可以改变默认编译行为。编译选项是一个元素为String类型的Iterable集合
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 这个里处理的
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;
}
/**
* 将 JavaFileObject 转换成className
*
* @param location PLATFORM_CLASS_PATH
* @param file /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/lib/ct.sym(META-INF/sym/rt.jar/java/lang/Comparable.class)
* @return java.lang.Comparable
*/
@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
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包上传方式。 -------》 这个已经实现了