剛來公司就接了一個不發版直接改程式碼的需求
前言
前幾天突然接到一個技術需求,想要做一個功能。前端有一個表單,在頁面上可以直接寫 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);
}
}
我們在啟動應用時,更換了程式碼文件記憶體,然後直接反射調用對象的方法。執行結果:
可以看到,新的程式碼已經生效!!!