前几天突然接到一个技术需求,想要做一个功能。前端有一个表单,在页面上可以直接写 java 代码,写完后就能保存到数据库,并且这个代码实时生效。这岂非是不用发版就可以随时改代码了吗?而且有bug也不怕,随时改。

适用场景:代码逻辑需要经常变动的业务。

JDK 提供了一个工具包 javax.tools 让使用者可以用简易的 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);
- }
- }
- 复制代码
我们在启动应用时,更换了代码文件内存,然后直接反射调用对象的方法。执行结果:

可以看到,新的代码已经生效!!!
注意,直接开放修改代码虽然方便,但是一定要做好安全防护