携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第26天,点击查看活动详情
前言
前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。
适用场景:代码逻辑需要经常变动的业务。
核心思想
- 页面改动 java 代码字符串
- java 代码字符串编译成 class
- 动态加载到 jvm
实现重点
JDK 提供了一个工具包 javax.tools 让使用者可以用简易的 API 进行编译。
这些工具包的使用步骤:
- 获取一个 javax.tools.JavaCompiler 实例。
- 基于 Java 文件对象初始化一个编译任务 CompilationTask 实例。
- 因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后可以通过自定义的类加载器加载对应的类实例
- 使用反射 API 进行实例化和后续的调用。
1. 代码编译
这一步需要将 java 文件编译成 class,其实平常的开发过程中,我们的代码编译都是由 IDEA、Maven 等工具完成。
内置的 SimpleJavaFileObject 是面向源码文件的,而我们的是源码字符串,所以需要实现 JavaFileObject 接口自定义一个 JavaFileObject。
public class CharSequenceJavaFileObject extends SimpleJavaFileObject {
public static final String CLASS_EXTENSION = ".class";
public static final String JAVA_EXTENSION = ".java";
private static URI fromClassName(String className) {
try {
return new URI(className);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(className, e);
}
}
private ByteArrayOutputStream byteCode;
private final CharSequence sourceCode;
public CharSequenceJavaFileObject(String className, CharSequence sourceCode) {
super(fromClassName(className + JAVA_EXTENSION), Kind.SOURCE);
this.sourceCode = sourceCode;
}
public CharSequenceJavaFileObject(String fullClassName, Kind kind) {
super(fromClassName(fullClassName), kind);
this.sourceCode = null;
}
public CharSequenceJavaFileObject(URI uri, Kind kind) {
super(uri, kind);
this.sourceCode = null;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return sourceCode;
}
@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(getByteCode());
}
// 注意这个方法是编译结果回调的OutputStream,回调成功后就能通过下面的getByteCode()方法获取目标类编译后的字节码字节数组
@Override
public OutputStream openOutputStream() {
return byteCode = new ByteArrayOutputStream();
}
public byte[] getByteCode() {
return byteCode.toByteArray();
}
}
如果编译成功之后,直接通过 CharSequenceJavaFileObject#getByteCode()方法即可获取目标类编译后的字节码对应的字节数组(二进制内容)
2. 实现 ClassLoader
因为JVM 里面的 Class 是基于 ClassLoader 隔离的,所以编译成功之后得通过自定义的类加载器加载对应的类实例,否则是加载不了的,因为同一个类只会加载一次。
主要关注 findClass 方法
public class JdkDynamicCompileClassLoader extends ClassLoader {
public static final String CLASS_EXTENSION = ".class";
private final static Map<String, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();
public JdkDynamicCompileClassLoader(ClassLoader parentClassLoader) {
super(parentClassLoader);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
JavaFileObject javaFileObject = javaFileObjectMap.get(name);
if (null != javaFileObject) {
CharSequenceJavaFileObject charSequenceJavaFileObject = (CharSequenceJavaFileObject) javaFileObject;
byte[] byteCode = charSequenceJavaFileObject.getByteCode();
return defineClass(name, byteCode, 0, byteCode.length);
}
return super.findClass(name);
}
@Override
public InputStream getResourceAsStream(String name) {
if (name.endsWith(CLASS_EXTENSION)) {
String qualifiedClassName = name.substring(0, name.length() - CLASS_EXTENSION.length()).replace('/', '.');
CharSequenceJavaFileObject javaFileObject = (CharSequenceJavaFileObject) javaFileObjectMap.get(qualifiedClassName);
if (null != javaFileObject && null != javaFileObject.getByteCode()) {
return new ByteArrayInputStream(javaFileObject.getByteCode());
}
}
return super.getResourceAsStream(name);
}
/**
* 暂时存放编译的源文件对象,key为全类名的别名(非URI模式),如club.throwable.compile.HelloService
*/
void addJavaFileObject(String qualifiedClassName, JavaFileObject javaFileObject) {
javaFileObjectMap.put(qualifiedClassName, javaFileObject);
}
Collection<JavaFileObject> listJavaFileObject() {
return Collections.unmodifiableCollection(javaFileObjectMap.values());
}
}
3. 封装了上面的 ClassLoader 和 JavaFileObject
public class JdkDynamicCompileJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
private final JdkDynamicCompileClassLoader classLoader;
private final Map<URI, JavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();
public JdkDynamicCompileJavaFileManager(JavaFileManager fileManager, JdkDynamicCompileClassLoader classLoader) {
super(fileManager);
this.classLoader = classLoader;
}
private static URI fromLocation(Location location, String packageName, String relativeName) {
try {
return new URI(location.getName() + '/' + packageName + '/' + relativeName);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
JavaFileObject javaFileObject = javaFileObjectMap.get(fromLocation(location, packageName, relativeName));
if (null != javaFileObject) {
return javaFileObject;
}
return super.getFileForInput(location, packageName, relativeName);
}
/**
* 这里是编译器返回的同(源)Java文件对象,替换为CharSequenceJavaFileObject实现
*/
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
JavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, kind);
classLoader.addJavaFileObject(className, javaFileObject);
return javaFileObject;
}
/**
* 这里覆盖原来的类加载器
*/
@Override
public ClassLoader getClassLoader(Location location) {
return classLoader;
}
@Override
public String inferBinaryName(Location location, JavaFileObject file) {
if (file instanceof CharSequenceJavaFileObject) {
return file.getName();
}
return super.inferBinaryName(location, file);
}
@Override
public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse) throws IOException {
Iterable<JavaFileObject> superResult = super.list(location, packageName, kinds, recurse);
List<JavaFileObject> result = new ArrayList<>();
// 这里要区分编译的Location以及编译的Kind
if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
// .class文件以及classPath下
for (JavaFileObject file : javaFileObjectMap.values()) {
if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
result.add(file);
}
}
// 这里需要额外添加类加载器加载的所有Java文件对象
result.addAll(classLoader.listJavaFileObject());
} else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
// .java文件以及编译路径下
for (JavaFileObject file : javaFileObjectMap.values()) {
if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
result.add(file);
}
}
}
for (JavaFileObject javaFileObject : superResult) {
result.add(javaFileObject);
}
return result;
}
/**
* 自定义方法,用于添加和缓存待编译的源文件对象
*/
public void addJavaFileObject(Location location, String packageName, String relativeName, JavaFileObject javaFileObject) {
javaFileObjectMap.put(fromLocation(location, packageName, relativeName), javaFileObject);
}
}
4. 使用 JavaCompiler 编译并反射生成实例对象
public final class JdkCompiler {
static DiagnosticCollector<JavaFileObject> DIAGNOSTIC_COLLECTOR = new DiagnosticCollector<>();
@SuppressWarnings("unchecked")
public static <T> T compile(String packageName,
String className,
String sourceCode) throws Exception {
// 获取系统编译器实例
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 设置编译参数
List<String> options = new ArrayList<>();
options.add("-source");
options.add("1.8");
options.add("-target");
options.add("1.8");
// 获取标准的Java文件管理器实例
StandardJavaFileManager manager = compiler.getStandardFileManager(DIAGNOSTIC_COLLECTOR, null, null);
// 初始化自定义类加载器
JdkDynamicCompileClassLoader classLoader = new JdkDynamicCompileClassLoader(Thread.currentThread().getContextClassLoader());
// 初始化自定义Java文件管理器实例
JdkDynamicCompileJavaFileManager fileManager = new JdkDynamicCompileJavaFileManager(manager, classLoader);
String qualifiedName = packageName + "." + className;
// 构建Java源文件实例
CharSequenceJavaFileObject javaFileObject = new CharSequenceJavaFileObject(className, sourceCode);
// 添加Java源文件实例到自定义Java文件管理器实例中
fileManager.addJavaFileObject(
StandardLocation.SOURCE_PATH,
packageName,
className + CharSequenceJavaFileObject.JAVA_EXTENSION,
javaFileObject
);
// 初始化一个编译任务实例
JavaCompiler.CompilationTask compilationTask = compiler.getTask(
null,
fileManager,
DIAGNOSTIC_COLLECTOR,
options,
null,
Collections.singletonList(javaFileObject)
);
Boolean result = compilationTask.call();
System.out.println(String.format("编译[%s]结果:%s", qualifiedName, result));
Class<?> klass = classLoader.loadClass(qualifiedName);
return (T) klass.getDeclaredConstructor().newInstance();
}
}
完成上面工具的搭建之后。我们可以接入数据库的操作了。数据库层面省略,只展示 service 层
service 层:
public class JavaService {
public Object saveAndGetObject(String packageName,String className,String javaContent) throws Exception {
Object object = JdkCompiler.compile(packageName, className, javaContent);
return object;
}
}
测试:
public class TestService {
public static void main(String[] args) throws Exception {
test();
}
static String content="package cn.mmc;\n" +
"\n" +
"public class SayHello {\n" +
" \n" +
" public void say(){\n" +
" System.out.println(\"11111111111\");\n" +
" }\n" +
"}";
static String content2="package cn.mmc;\n" +
"\n" +
"public class SayHello {\n" +
" \n" +
" public void say(){\n" +
" System.out.println(\"22222222222222\");\n" +
" }\n" +
"}";
public static void test() throws Exception {
JavaService javaService = new JavaService();
Object sayHello = javaService.saveAndGetObject("cn.mmc", "SayHello", content);
sayHello.getClass().getMethod("say").invoke(sayHello);
Object sayHello2 = javaService.saveAndGetObject("cn.mmc", "SayHello", content2);
sayHello2.getClass().getMethod("say").invoke(sayHello2);
}
}
我们在启动应用时,更换了代码文件内存,然后直接反射调用对象的方法。执行结果:
可以看到,新的代码已经生效!!!
注意,直接开放修改代码虽然方便,但是一定要做好安全防护