前言
前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 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()方法即可获取目标类编译后的字节码对应的字节数组(二进制内容)
- 实现 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()); } }
- 封装了上面的 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); } }
- 使用 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); } }
我们在启动应用时,更换了代码文件内存,然后直接反射调用对象的方法。执行结果:
可以看到,新的代码已经生效!!!