java代碼實現編譯源文件
原理
從Java 6開始,引入了Java代碼重寫過的編譯器接口,使得我們可以在運行時編譯Java源代碼,然后再通過類加載器將編譯好的類加載進JVM,這種在運行時編譯代碼的操作就叫做動態(tài)編譯。
主要類庫
JavaCompiler -表示java編譯器, run方法執(zhí)行編譯操作. 還有一種編譯方式是先生成編譯任務(CompilationTask), 讓后調用CompilationTask的call方法執(zhí)行編譯任務
JavaFileObject -表示一個java源文件對象
JavaFileManager - Java源文件管理類, 管理一系列JavaFileObject
Diagnostic -表示一個診斷信息
DiagnosticListener -診斷信息監(jiān)聽器, 編譯過程觸發(fā). 生成編譯task(JavaCompiler#getTask())或獲取FileManager(JavaCompiler#getStandardFileManager())時需要傳遞DiagnosticListener以便收集診斷信息
流程圖

源碼文件 -> 字節(jié)碼文件
public static void fromJavaFile() {
// 獲取Javac編譯器對象
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 獲取文件管理器:負責管理類文件的輸入輸出
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
// 獲取要被編譯的Java源文件
File file = new File("/Users//github/perfect/perfect-javassist/src/main/java/com/jc/javassist/compiler/TestHello.java");
// 通過源文件獲取到要編譯的Java類源碼迭代器,包括所有內部類,其中每個類都是一個JavaFileObject
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(file);
// 生成編譯任務
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
// 執(zhí)行編譯任務
task.call();
}
我們這里準備了TestHello.java
public class TestHello {
public static void main(String[] args) {
System.out.println("this is a test");
}
}
我們試著手動加載該class文件,使用類加載器的defineClass方法,可以直接加載字節(jié)碼文件。
public static Class<?> loadClassFromDisk(String path) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// defineClass 為 ClassLoader 類的一個方法,用于加載類
// 但是這個方法是 protected 的,所以需要通過反射的方式獲取這個方法的權限
Class<ClassLoader> classLoaderClass = ClassLoader.class;
Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
// 讀取文件系統的 file 為 byte 數組
File file = new File(path);
byte[] bytes = new byte[(int) file.length()];
try (FileInputStream fileInputStream = new FileInputStream(file)) {
fileInputStream.read(bytes);
} catch (IOException e) {
e.printStackTrace();
}
// 執(zhí)行 defineClass 方法 返回 Class 對象
return (Class<?>) defineClass.invoke(Thread.currentThread().getContextClassLoader(), bytes, 0, bytes.length);
}
由于TestHello中的方法為靜態(tài)方法,使用class反射機制執(zhí)行方法
// 執(zhí)行編譯任務
Boolean call = task.call();
if (call) {
Class<?> o = loadClassFromDisk("/Users/oneyoung/oneyoung/project/my/code/src/main/java/top/oneyoung/dynamic/TestHello.class");
Method main = o.getMethod("main", String[].class);
main.invoke(null, new Object[]{new String[]{}});
}
執(zhí)行結果
| this is a test
源碼字符串 -> 字節(jié)碼文件
在流程圖中,getTask().call()會通過調用作為參數傳入的JavaFileObject對象的getCharContent()方法獲得字符串序列,即源碼的讀取是通過 JavaFileObject的 getCharContent()方法,那我們只需要重寫getCharContent()方法,即可將我們的字符串源碼裝進JavaFileObject了。構造SourceJavaFileObject實現定制的JavaFileObject對象,用于存儲字符串源碼
public class SourceJavaFileObject extends SimpleJavaFileObject {
/**
* The source code of this "file".
*/
private final String code;
SourceJavaFileObject(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
則創(chuàng)建JavaFileObject對象時,變?yōu)榱耍?/p>
// 通過源代碼字符串獲取到要編譯的Java類源碼迭代器,包括所有內部類,其中每個類都是一個JavaFileObject
SourceJavaFileObject javaFileObject = new SourceJavaFileObject("TestHello", "public class TestHello { public static void main(String[] args) { System.out.println("Hello World"); } }");
// 生成編譯任務
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, Collections.singleton(javaFileObject));
執(zhí)行后,同樣編譯出了class文件,不過由于沒有指定編譯的class輸出路徑,他會默認放在源文件的根目錄下
源碼字符串 -> 字節(jié)碼數組
如果我們進行動態(tài)編譯時,想要直接輸入源碼字符串并且輸出的是字節(jié)碼數組,而不是輸出字節(jié)碼文件,又該如何實現?實際上,這是從內存中得到源碼,再輸出到內存的方式。
在getTask().call()源代碼執(zhí)行流程圖中,我們可以發(fā)現JavaFileObject 的 openOutputStream()方法控制了編譯后字節(jié)碼的輸出行為,編譯完成后會調用openOutputStream獲取輸出流,并寫數據(字節(jié)碼)。所以我們需要重寫JavaFileObject 的 openOutputStream()方法。
同時在執(zhí)行流程圖中,我們還發(fā)現用于輸出的JavaFileObject 對象是JavaFileManager的getJavaFileForOutput()方法提供的,所以為了讓編譯器編譯完成后,將編譯得到的字節(jié)碼輸出到我們自己構造的JavaFileObject 對象,我們還需要自定義JavaFileManager。
這里我使用類委托的方式,把大部分功能委托給了傳入的StandardJavaFileManager,主要是重寫了getJavaFileForOutput,使輸出編譯完成的字節(jié)碼文件為字節(jié)數組。
然后增加了方法getBytesByClassName獲取編譯完成的字節(jié)碼字節(jié)數組
public class ByteArrayJavaFileManager implements JavaFileManager {
private static final Logger LOG = LoggerFactory.getLogger(ByteArrayJavaFileManager.class);
private final StandardJavaFileManager fileManager;
/**
* synchronizing due to ConcurrentModificationException
*/
private final Map<String, ByteArrayOutputStream> buffers = Collections.synchronizedMap(new LinkedHashMap<>());
public ByteArrayJavaFileManager(StandardJavaFileManager fileManager) {
this.fileManager = fileManager;
}
@Override
public ClassLoader getClassLoader(Location location) {
return fileManager.getClassLoader(location);
}
@Override
public synchronized Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse) throws IOException {
return fileManager.list(location, packageName, kinds, recurse);
}
@Override
public String inferBinaryName(Location location, JavaFileObject file) {
return fileManager.inferBinaryName(location, file);
}
@Override
public boolean isSameFile(FileObject a, FileObject b) {
return fileManager.isSameFile(a, b);
}
@Override
public synchronized boolean handleOption(String current, Iterator<String> remaining) {
return fileManager.handleOption(current, remaining);
}
@Override
public boolean hasLocation(Location location) {
return fileManager.hasLocation(location);
}
@Override
public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException {
if (location == StandardLocation.CLASS_OUTPUT) {
boolean success;
final byte[] bytes;
synchronized (buffers) {
success = buffers.containsKey(className) && kind == Kind.CLASS;
bytes = buffers.get(className).toByteArray();
}
if (success) {
return new SimpleJavaFileObject(URI.create(className), kind) {
@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(bytes);
}
};
}
}
return fileManager.getJavaFileForInput(location, className, kind);
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, final String className, Kind kind, FileObject sibling) {
return new SimpleJavaFileObject(URI.create(className), kind) {
@Override
public OutputStream openOutputStream() {
// 字節(jié)輸出流用與FileManager輸出編譯完成的字節(jié)碼文件為字節(jié)數組
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 將每個需要加載的類的輸出流進行緩存
buffers.putIfAbsent(className, bos);
return bos;
}
};
}
@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
return fileManager.getFileForInput(location, packageName, relativeName);
}
@Override
public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) throws IOException {
return fileManager.getFileForOutput(location, packageName, relativeName, sibling);
}
@Override
public void flush() {
// Do nothing
}
@Override
public void close() throws IOException {
fileManager.close();
}
@Override
public int isSupportedOption(String option) {
return fileManager.isSupportedOption(option);
}
public void clearBuffers() {
buffers.clear();
}
public Map<String, byte[]> getAllBuffers() {
Map<String, byte[]> ret = new LinkedHashMap<>(buffers.size() * 2);
Map<String, ByteArrayOutputStream> compiledClasses = new LinkedHashMap<>(ret.size());
synchronized (buffers) {
compiledClasses.putAll(buffers);
}
compiledClasses.forEach((k, v) -> ret.put(k, v.toByteArray()));
return ret;
}
public byte[] getBytesByClassName(String className) {
return buffers.get(className).toByteArray();
}
}
然后我們修改下之前的執(zhí)行流程
public static void fromJavaSourceToByteArray1() throws Exception {
// 獲取Javac編譯器對象
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 獲取文件管理器:負責管理類文件的輸入輸出
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
// 創(chuàng)建自定義的FileManager
ByteArrayJavaFileManager byteArrayJavaFileManager = new ByteArrayJavaFileManager(fileManager);
// 通過源代碼字符串獲取到要編譯的Java類源碼迭代器,包括所有內部類,其中每個類都是一個JavaFileObject
JavaFileObject javaFileObject = new SourceJavaFileObject("TestHello", "public class TestHello { public static void say(String args) { System.out.println(args); } }");
JavaCompiler.CompilationTask task = compiler.getTask(null, byteArrayJavaFileManager, null, null, null, Collections.singletonList(javaFileObject));
// 執(zhí)行編譯任務
Boolean call = task.call();
if (Boolean.TRUE.equals(call)) {
byte[] testHellos = byteArrayJavaFileManager.getBytesByClassName("TestHello");
Class<ClassLoader> classLoaderClass = ClassLoader.class;
Method defineClass = classLoaderClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
defineClass.setAccessible(true);
Object invoke = defineClass.invoke(TestHello.class.getClassLoader(), testHellos, 0, testHellos.length);
Class clazz = (Class) invoke;
clazz.getMethod("say", String.class).invoke(null, "你好");
}
}
以上就是java代碼實現編譯源文件的詳細內容,更多關于java編譯源文件的資料請關注腳本之家其它相關文章!
相關文章
Springboot3+Redis實現消息隊列的多種方法小結
本文主要介紹了Springboot3+Redis實現消息隊列的多種方法小結,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2025-03-03
SpringCloud實現基于RabbitMQ消息隊列的詳細步驟
在Spring Cloud框架中,我們可以利用RabbitMQ實現強大而可靠的消息隊列系統,本篇將詳細介紹如何在Spring Cloud項目中集成RabbitMQ,并創(chuàng)建一個簡單的消息隊列,感興趣的朋友一起看看吧2024-03-03

