教你怎么實現(xiàn)java語言的在線編譯
一、前言
- 使用過leetcode或者類似在線編譯網站功能的人,或許會比較感興趣,關于在線編譯的實現(xiàn)原理,由于我比較頭鐵,所以一沖動之下畢業(yè)設計的項目選擇制作一個類似于在線編譯的一個網站。
- 在決定做這個之前,大概對這方面的東西一竅不通,網上的資料很多也是比較千篇一律,給我這種萌新帶來的難度不是一點半點,當然,最終收獲還是挺大的,所以想寫一點東西,作為梳理,也給以后想學的人做一個參考作用(其實在寫的過程中還是踩了一些坑的)。
- 最終,其實成果挺水的,做出來的成品,就只是實現(xiàn)了一個簡陋的Java語言的在線編譯功能,這里也想吐槽一下,其實leetcode,支持那么多語言的在線編譯真的挺厲害的。
二、前期準備
首先在運行java程序之前,肯定要想辦法把.java
的文件使用編譯器,編譯成.class
的字節(jié)碼文件。
運氣好的是,強大的Java已經具備類似的API,就是JavaCompiler
類,下面做一點簡單介紹:
JavaCompiler是java語言自帶的一個接口,大概是一個對Java編譯器的一個抽象,通過ToolProvider 類的靜態(tài)方法獲取其實現(xiàn)對象:
public interface JavaCompiler extends Tool, OptionChecker
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
稍微看一下源碼
private static final String defaultJavaCompilerName = "com.sun.tools.javac.api.JavacTool"; private static synchronized ToolProvider instance() { if (instance == null) instance = new ToolProvider(); return instance; } /** * Gets the Java™ programming language compiler provided * with this platform. * @return the compiler provided with this platform or * {@code null} if no compiler is provided */ public static JavaCompiler getSystemJavaCompiler() { return instance().getSystemTool(JavaCompiler.class, defaultJavaCompilerName); }
可以知道,返回的是一個JavacTool對象,是一個接口實現(xiàn)類
public final class JavacTool implements JavaCompiler {
這個類實現(xiàn)了run
方法
public interface Tool { int run(InputStream in, OutputStream out, OutputStream err, String... arguments); }
各個參數(shù)的意思分別是
in
java編譯器提供信息
out
用于獲取輸出信息
err
用于獲取錯誤信息
arguments
編譯的文件(路徑)
前面三個參數(shù)如果,為null
則會用默認標準輸入輸出代替。網上到處都搜的到不做累述。
三、JavaCompiler V1.0
于是就有了第一種在線編譯運行的實現(xiàn)思路,使用文件IO來動態(tài)生成.java
格式的文件與路徑,然后寫入代碼內容。
最初我便是打算姑且使用這種方式,由于數(shù)據(jù)封裝對象UserDto與Question都具有一個唯一的Id屬性,因此 xx.userId.questionId
似乎挺適合用來做生成文件的類路徑的,類名就可以統(tǒng)一學習leetcode使用Solution ,于是一番努力后寫出了我的Compilerv1.0
然而這種方式就給人感覺很low,“java動態(tài)編譯”聽起來還挺不錯的,結果一細看,就這?
而且,這樣的實現(xiàn),每次前端給一個請求過來都要進行文件讀寫操作,如果之前沒有建立好相應路徑與文件,還得重新新建,于是,當用戶和題目多起來了以后那將是一個龐大的文件數(shù)量(最大值:用戶數(shù)X題目數(shù)X2),甚至并發(fā)量稍微有一點還不知道會出現(xiàn)什么問題。
四、JavaCompiler V2.0
在線編譯最理想的情況是:
前端表單傳給你需要編譯的java文件字符串內容,然后將數(shù)據(jù)直接交給自定義編譯器,編譯器經過編譯后返回Class對象,然后你再進行相應操作。
為了實現(xiàn)這個功能,除了JavaCompiler
還需要去了解如下對象:
JavaFileObject
(大概就是java文件的抽象)JavaFileManager
(大概就是Java文件管理操作的封裝)
相關內容是從上面博客鏈接學會的,我自己再做了些改動:
1.需要自定義一個JavaFileObject
重寫一些方法
2.需要自定義一個JavaFileManager
重寫一些方法
大致原理就是,由于Java封裝的特性,只要類的行為正確,可以關心類的內部細節(jié),所以,獲取.java
文件內容,最初是從文件中獲取,如果我們重寫相應方法,意味著我們可以將要編譯的String內容,直接返回給相應處理程序,只要調用相應方法,返回的內容正確,其實并不用關心,數(shù)據(jù)到底是從哪來的。
下面是我的
五、JavaFileObject實現(xiàn)
public class JavaFileObjectBean extends SimpleJavaFileObject { /** * Construct a SimpleJavaFileObject of the given kind and with the * given URI. * * @param uri the URI for this file object * @param kind the kind of this file object */ private String javaCode; private ByteArrayOutputStream outputStream; public JavaFileObjectBean(String className, String javaCode) { super(URI.create("string:///"+className.replace(".","/")+Kind.SOURCE.extension), Kind.SOURCE); // System.out.println("string:///" + className.replace(".", "/") + Kind.SOURCE.extension); this.javaCode=javaCode; //this.outputStream=new ByteArrayOutputStream(); } protected JavaFileObjectBean(String className, Kind kind) { super(URI.create("string:///"+className.replace(".","/")+kind.extension), kind); // System.out.println("!!"); this.outputStream=new ByteArrayOutputStream(); } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return this.javaCode; } @Override public OutputStream openOutputStream() throws IOException { return this.outputStream; } public byte[] getBytes(){ return this.outputStream.toByteArray(); } }
繼承自SimpleJavaFileObject
public class SimpleJavaFileObject implements JavaFileObject
新加了幾個屬性
private String javaCode; private ByteArrayOutputStream outputStream;
- javaCode:用來保存需要編譯的Java文件內容
- outputStream:用來保存編譯后,Class對象的二進制流
重寫了兩個構造器方法:
public JavaFileObjectBean(String className, String javaCode) { super(URI.create("string:///"+className.replace(".","/")+Kind.SOURCE.extension), Kind.SOURCE); // System.out.println("string:///" + className.replace(".", "/") + Kind.SOURCE.extension); this.javaCode=javaCode; //this.outputStream=new ByteArrayOutputStream(); } protected JavaFileObjectBean(String className, Kind kind) { super(URI.create("string:///"+className.replace(".","/")+kind.extension), kind); // System.out.println("!!"); this.outputStream=new ByteArrayOutputStream(); }
第一個構造方法:用于自己創(chuàng)建對象時使用,調用父類構造方法的同時初始化屬性:javaCode
完成初始化以后,相關對象會調用重寫的方法
@Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return this.javaCode; }
然后進行編譯。
第二個構造方法是給相關API調用,然后會調用重寫的方法
@Override public OutputStream openOutputStream() throws IOException { return this.outputStream; }
將編譯結果寫入提供的IO流
重寫好的JavaFileObject類配合自定義JavaFileManager使用
public class JavaFileManagerBean extends ForwardingJavaFileManager { private JavaFileObjectBean javaFileObjectBean; /** * Creates a new instance of ForwardingJavaFileManager. * * @param fileManager delegate to this file manager */ protected JavaFileManagerBean(JavaFileManager fileManager) { super(fileManager); //this.javaFileObjectBean= new JavaFileObjectBean(); } @Override public ClassLoader getClassLoader(Location location) { return new SecureClassLoader(){ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = javaFileObjectBean.getBytes(); return super.defineClass(name,bytes,0,bytes.length); } }; } @Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { this.javaFileObjectBean = new JavaFileObjectBean(className,kind); return this.javaFileObjectBean; } }
相關API會調用
public JavaFileObject getJavaFileForOutput
此時,內置IO流屬性會被初始化,然后寫入編譯的Class對象的二進制流信息,最后自定義一下類加載器的findClass
方法,利用loadClass
方法獲取編譯后得到的結果
cls=manager.getClassLoader(null).loadClass(className);
由于沒有實際文件,最后會由下面的代碼,尋找到需要加載到的類信息就會調用之前的重寫的findClass方法得到Class 對象(defineClass方法用來將二進制流信息還原為Class對象)
if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); }
暫時就介紹這么多,剩下的內容以后有時間再整理,如果又沒說清楚的地方歡迎指正。
到此這篇關于教你怎么實現(xiàn)java語言的在線編譯的文章就介紹到這了,更多相關實現(xiàn)java語言的在線編譯內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- 詳解Java的編譯執(zhí)行與解釋執(zhí)行
- java實現(xiàn)動態(tài)編譯并動態(tài)加載
- 改善Java代碼之慎用java動態(tài)編譯
- java利用JEXL實現(xiàn)動態(tài)表達式編譯
- java編譯命令基礎知識點
- java編譯后的文件出現(xiàn)xx$1.class的原因及解決方式
- java編譯器和JVM的區(qū)別
- 利用 Docker 構建簡單的 java 開發(fā)編譯環(huán)境的方法詳解
- 手動編譯并運行Java項目實現(xiàn)過程解析
- IDEA不編譯除了.java之外的文件的解決辦法(推薦)
- 2020年支持java8的Java反編譯工具匯總(推薦)
- IDEA 中使用 ECJ 編譯出現(xiàn) java.lang.IllegalArgumentException的錯誤問題
相關文章
詳解如何在低版本的Spring中快速實現(xiàn)類似自動配置的功能
這篇文章主要介紹了詳解如何在低版本的Spring中快速實現(xiàn)類似自動配置的功能,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-05-05Spring Security 密碼驗證動態(tài)加鹽的驗證處理方法
小編最近在改造項目,需要將gateway整合security在一起進行認證和鑒權,今天小編給大家分享Spring Security 密碼驗證動態(tài)加鹽的驗證處理方法,感興趣的朋友一起看看吧2021-06-06