教你怎麼實現java語言的在線編譯

一、前言

  • 使用過leetcode或者類似在線編譯網站功能的人,或許會比較感興趣,關於在線編譯的實現原理,由於我比較頭鐵,所以一沖動之下畢業設計的項目選擇制作一個類似於在線編譯的一個網站。
  • 在決定做這個之前,大概對這方面的東西一竅不通,網上的資料很多也是比較千篇一律,給我這種萌新帶來的難度不是一點半點,當然,最終收獲還是挺大的,所以想寫一點東西,作為梳理,也給以後想學的人做一個參考作用(其實在寫的過程中還是踩瞭一些坑的)。
  • 最終,其實成果挺水的,做出來的成品,就隻是實現瞭一個簡陋的Java語言的在線編譯功能,這裡也想吐槽一下,其實leetcode,支持那麼多語言的在線編譯真的挺厲害的。

二、前期準備

首先在運行java程序之前,肯定要想辦法把.java的文件使用編譯器,編譯成.class的字節碼文件。

運氣好的是,強大的Java已經具備類似的API,就是JavaCompiler類,下面做一點簡單介紹:

JavaCompiler是java語言自帶的一個接口,大概是一個對Java編譯器的一個抽象,通過ToolProvider 類的靜態方法獲取其實現對象:

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對象,是一個接口實現類

public final class JavacTool implements JavaCompiler {

這個類實現瞭run方法

public interface Tool {
  int run(InputStream in, OutputStream out, OutputStream err, String... arguments);
}

各個參數的意思分別是

  • in

java編譯器提供信息

  • out

用於獲取輸出信息

  • err

用於獲取錯誤信息

  • arguments

編譯的文件(路徑)

前面三個參數如果,為null則會用默認標準輸入輸出代替。網上到處都搜的到不做累述。

三、JavaCompiler V1.0

於是就有瞭第一種在線編譯運行的實現思路,使用文件IO來動態生成.java格式的文件與路徑,然後寫入代碼內容。

最初我便是打算姑且使用這種方式,由於數據封裝對象UserDto與Question都具有一個唯一的Id屬性,因此 xx.userId.questionId似乎挺適合用來做生成文件的類路徑的,類名就可以統一學習leetcode使用Solution ,於是一番努力後寫出瞭我的Compilerv1.0

然而這種方式就給人感覺很low,“java動態編譯”聽起來還挺屌的,結果一細看,就這?

而且,這樣的實現,每次前端給一個請求過來都要進行文件讀寫操作,如果之前沒有建立好相應路徑與文件,還得重新新建,於是,當用戶和題目多起來瞭以後那將是一個龐大的文件數量(最大值:用戶數X題目數X2),甚至並發量稍微有一點還不知道會出現什麼問題。

四、JavaCompiler V2.0

在線編譯最理想的情況是

前端表單傳給你需要編譯的java文件字符串內容,然後將數據直接交給自定義編譯器,編譯器經過編譯後返回Class對象,然後你再進行相應操作。

為瞭實現這個功能,除瞭JavaCompiler還需要去瞭解如下對象:

  • JavaFileObject(大概就是java文件的抽象)
  • JavaFileManager(大概就是Java文件管理操作的封裝)

相關內容是從上面博客鏈接學會的,我自己再做瞭些改動:

1.需要自定義一個JavaFileObject重寫一些方法

2.需要自定義一個JavaFileManager重寫一些方法

大致原理就是,由於Java封裝的特性,隻要類的行為正確,可以關心類的內部細節,所以,獲取.java文件內容,最初是從文件中獲取,如果我們重寫相應方法,意味著我們可以將要編譯的String內容,直接返回給相應處理程序,隻要調用相應方法,返回的內容正確,其實並不用關心,數據到底是從哪來的。
下面是我的

五、JavaFileObject實現

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();
    }

第一個構造方法:用於自己創建對象時使用,調用父類構造方法的同時初始化屬性: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();
                }

暫時就介紹這麼多,剩下的內容以後有時間再整理,如果又沒說清楚的地方歡迎指正。

到此這篇關於教你怎麼實現java語言的在線編譯的文章就介紹到這瞭,更多相關實現java語言的在線編譯內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: