程式碼線上編譯器(上)- 編輯及編譯

weixin_34232744發表於2018-10-30

此文已由作者姚太行授權網易雲社群釋出。

歡迎訪問網易雲社群,瞭解更多網易技術產品運營經驗。

線上編譯器

程式碼線上編譯器,即線上程式碼編寫執行工具,提供給使用者線上程式碼編輯、程式碼提示、程式碼診斷、編譯、執行等一系列從程式碼編寫到啟動執行過程中必要的功能服務,以達到IDE的核心功能,應用範圍較廣,從使用場景下大致分為兩類:

一般場景
  • 功能基礎:僅基於開發語言的語法特點及常用原生庫。

  • 內容描述:此應用場景下,對一些涉及IO,諸如讀寫、外部請求等極端操作型別支援程度較高,程式碼執行環境通常使用沙箱,以滿足安全性需要。

  • 應用範疇:主要的應用業務範疇有線上程式碼輔助編輯工具(Tool等)、線上考試平臺(牛客網等)、演算法競賽刷題平臺(leetcode等)。

特殊場景
  • 功能基礎:基於平臺提供的大量工具API,僅結合必要的常用原生庫。

  • 內容描述:此應用場景下,使用者編寫的程式碼涉及的內容被限制在平臺規定的有界範圍內,程式碼風格、格式、結構也需按照平臺規範進行展開,編譯器除在基本語法檢測的基礎上也會對程式碼內涉及內容、方法做細緻檢測,對一些涉及IO、讀寫、網路請求等敏感操作會進行嚴格限制。由於需要使用平臺本身提供的API,故簡單的沙箱已經無法滿足需要,需要針對不同的業務特點進行特殊的程式碼執行環境安全保障。

  • 應用範疇:應用方面,根據平臺工具API提供的出發點不同,業務範疇會被限制在平臺涉及的範圍內。在量化範疇內,多數量化平臺會提供Python、Java的策略程式碼線上編譯功能,並提供相關API以供使用者完成量化策略開發的需要。

由於一般場景比較常見,開發及搭建的相關成熟樣例也較多,本文在此不過多進行討論。對於特殊場景,本文將結合在網易貴金屬量化平臺Java線上編譯器的相關案例,對於線上編譯部分的實現思路進行詳細闡述。

案例說明

網易貴金屬量化平臺,核心是利用線上編譯器相關原理,(目前)提供了針對貴金屬交易的相關量化策略開發功能。後文每一個部分將以此平臺為案例,結合理論總結進行案例闡述。為方便之後的闡述,現對系統基本情況作出簡單說明:

  • 業務核心說明:使用者可結合自身市場投資經驗,形成策略,以回測或實盤方式,使用歷史行情或實時行情以策略內容進行在歷某個階段或實時地模擬交易操作,輸出策略交易盈虧,以達到驗證策略、優化策略、積攢投資經驗的目的。

  • 策略:策略即“決定何種條件下觸發交易”的一段邏輯,條件判定依據除時間及商品行情外,還可能包含機器學習結果、訓練模型結果以及經濟學指標等。表現在量化平臺上是一段Java(或其他語言)程式碼,程式碼通過呼叫平臺提供的介面進行邏輯判斷以及交易操作。

  • 策略輸出:策略輸出的直接結果就是交易訊號本身及交易記錄,統計出某段時間該策略總盈虧、最大回撤、夏普率等常用盈虧評價統計指標。

過程上體現為:

  • 使用者編寫策略

  • 平臺模擬交易

  • 交易結果統計

使用者編寫策略 模擬交易並統計結果

線上編輯及編譯

一個完整的線上編譯流程,是從使用者編寫的程式碼開始的(當然程式碼來源不僅僅侷限於此),程式碼從構建(編寫或組裝)到編譯直至執行,最終輸出結果或造成預期影響。流程包括

  • 程式碼構建

  • 語法檢測

  • 程式碼診斷

  • 程式碼編譯

  • 程式碼執行

  • 內容反饋

程式碼構建

程式碼構建,涉及到語言型別、程式碼結構以及最終的程式碼生成方式。

語言型別

線上編譯器平臺構建前,需明確平臺支援的語言型別。語言型別會影響到的方面:

  • 編譯方式:可歸納至以下三種型別:

    • 解釋型:解釋型語言編寫的程式,由其對應的解釋程式執行的,不會直接涉及到編譯過程,如JavaScript等。此類語言在搭建時一般可以動態的進行執行,而無需後臺程式進行繁瑣的編譯過程。在平臺架構設計時,可結合實際需要將相關程式碼的處理過程直接放置於平臺上層(如瀏覽器本身),直接反饋結果,而無需將請求處理過程放置在底層,反而會把邏輯搞複雜。

    • 編譯型:編譯型語言通常功能較為強大且相對底層,需要先將程式碼編譯為目標程式機器碼檔案,如C、C++等,目標程式檔案可脫離程式碼在計算機上多次執行。此類語言的使用者程式碼,需將使用者最終提交的程式碼交由伺服器等具體計算機進行處理後,再進行程式執行進而反饋程式執行結果。

    • 混合型:混合型語言與編譯型語言不同點在於,編譯過程不生成機器碼而生成位元組碼檔案,如Java、Python等,位元組碼檔案同樣可被載入至特殊的執行環境中多次執行但卻無法被計算器直接識別。此類語言的使用者程式碼,同樣需要交由伺服器等計算機進行處理,但執行時必須交由能夠提供特殊執行環境的計算機來執行。

  • 程式碼風格:程式碼風格,主要是需要確定程式碼是否對格式有特殊的要求,從而對提示過程作出優化,且會對之後的程式碼檢測過程提供便利。例如Python會對縮排有強依賴,那麼在程式碼提示和使用者使用方面需要進行特殊的服務優化。

  • 程式碼提示:程式碼提示必須在語言型別確認後才可確定,一般的基於瀏覽器的前端線上編輯框架,對某些語言的原生API會有現成的提示,除這一部分外,如果需要提示給使用者平臺自身開發的一些額外的API,則需要對這部分額外的內容整理為程式碼提示要求的格式,進行補充與匯入。

程式碼結構

一般場景下,對使用者程式碼的結構一般沒有特殊需求,即與一般的IDE功能相同。 但是在特殊場景下,由於程式碼編寫的目的相對明確,程式碼中包含的內容也是有預期的,所以在使用者程式碼編寫前,就可以通過固定程式碼結構的方式來限制使用者程式碼的編寫內容及構成,在之後的程式碼檢測階段,也可以根據此固定格式來進行初步的程式碼合理性檢測。

以Java為例,固定結構的內容包括:

  • 禁止指定package結構

  • 禁止類import匯入

  • 必須繼承的父類

  • 必須實現的介面

  • 類唯一性

  • 必須包含的方法

  • 程式碼固定位置的提示性用註釋

生成方式

程式碼生成方式上,根據平臺對使用者程式碼編寫過程中的不同支援方式,在互動層面,使用者生成自己程式碼的路徑會有所不同,但最終結果均以生成合理程式碼為目標。

量化平臺範疇中,使用者程式碼用於實現對既往資料計算學習從而在未來做出決策的策略,以目前市場上一些特徵較為突出的量化平臺為例,生成方式可包括:

  • 原始程式碼編輯方式

(樣例圖片來源:網易貴金屬量化平臺)此種方式下,即便藉助程式碼提示和相關注釋說明,使用者在程式碼構建過程中也會較為困難,但對於成熟程式設計師而言,反而自由度會相對較高。

  • 元件化組建方式

(樣例圖片來源:BigQuant)此種方式中,對可預估的程式碼內容進行元件化,使用者選用其需要的元件,由平臺根據元件選擇情況負責拼接,大大降低了程式碼編寫的門檻,對於特殊行業需求但非計算機技術掌握者非常友好,且程式碼的合理性得到了極大的保證。

  • 視覺化元件組建方式

(樣例圖片來源:BigQuant)此種方式,是元件化的更高層面的包裝,程式碼編寫的門檻再一次被降低,且在表述程式碼邏輯過程中有奇效。

其實在程式碼生成方式上,結合不同的需求和業務領域的具體需要,還存在很多種不同的友好的生成方式。就上述三種方式而已,明顯可以看出後兩種方式在程式碼生成上更為友好和可用,但在自由度上可能有所降低。

程式碼生成方式上,如果程式碼內容可預估、結構相對固定,在有條件的情況下,建議在提供除原始程式碼編輯方式的基礎上,提供其他以組建為主體思路的程式碼生成方式。組建的程式碼生成方式,不但能提升使用者體驗,大幅度降低使用者使用門檻,還能夠有效降低使用者程式碼出現語法錯誤及邏輯不合理的可能性。

案例說明

結合這一部分關於程式碼構建的總結,案例中對應的相關部分內容如下:

  • 語言型別:Java8

  • 程式碼結構:使用者策略程式碼在編輯時,平臺會預先提供模板,並提供相關所有API的程式碼提示。模板內包含了必須實現的介面以及必須包含的方法,並在固定流程結構過程中標記了提示用註釋。在編寫過程中,對使用者編寫並不受限,程式碼檢測過程目前不在編輯過程中進行。

  • 生成方式:目前提供原始程式碼編輯方式,未來計劃朝著元件化方向進行發展。

程式碼檢測

使用者程式碼檢測,是指在使用者程式碼在診斷及執行前對其內容及語法,針對語法合法性以及構建階段預想的程式碼結構進行檢測,以甄別使用者程式碼是否合理。如果在平臺設計時,使用者程式碼只是最終執行程式碼的一部分,公共部分由系統拼裝,也可在這一檢測過程中完成拼裝過程。

程式碼拼裝

程式碼拼裝,即對使用者編輯部分補充其餘的公共部分,這樣做既可以減少使用者需要編輯的程式碼量,又能在一定程度上限定使用者程式碼中出現一些意料之外的內容。

以Java為例,拼裝內容可包括:

  • package路徑:可限制最終生成的類的路徑

  • import類匯入:可限制使用者能夠使用的類範圍

  • 註解:可對使用者程式碼以類或方法為粒度追加其他行為

  • 通用方法:追加在執行時必須呼叫的通用方法(一般會置於抽象類中)

將拼裝內容以預想方式與使用者編輯部分合併為一個完整的可被檢測的程式碼檔案。

語法檢測

簡單的語法檢測可以直接通過識別檔案進行,或直接嘗試利用診斷過程獲知檔案是否語法合理,再複雜的就要結合編譯原理中的語法分析器構建抽象語法樹來進行詳細解析。

結構檢測

對照程式碼構建階段的程式碼結構相關內容,檢測內容包括:

  • 檔案路徑是否合理(包路徑)

  • 類名合法性

  • 類是否存在必要繼承及實現

  • 是否包含必要引數

  • 是否包含必要方法

  • 是否符合其他必要固定結構

經歷上述過程後,基本可以得到獲知一份使用者程式碼是否有被編譯診斷的必要性。

案例說明

結合這一部分關於程式碼檢測的總結,案例中對應的相關部分如下:

  • 程式碼拼裝:網易貴金屬量化平臺中,使用者只需繼承介面後,實現主體的三個方法,且在模板中對這三個方法的流程已做了詳細說明,類之外的部分是無需使用者編寫的。程式碼拼裝內容包括:

    • 設定類程式碼檔案的package路徑至統一位置,並結合使用者資訊和時間戳進行生成子路徑,防止路徑下類重名

    • 所有java.lang之外的包,只匯入涉及到的部分,涵蓋計算、資料結構、時間處理等內容

    • 匯入所有平臺提供的API類

  • 語法檢測:直接使用編輯工具診斷過程進行,沒有在這一部分使用到抽象語法樹。

  • 結構檢測:量化平臺上使用者程式碼以類作為使用者程式碼編寫的主體,而不包含其他內容,結構檢測內容包括:

    • 使用者程式碼部分不得為空

    • 使用者不得自主匯入類

    • 必須繼承使用者策略模板介面

    • 必須策略模板的完整實現類

    • class關鍵字唯一,類唯一,且不得包含內部類

經過初步檢測後,如果程式碼檢測無誤,就可以到程式碼診斷和程式碼編譯,以進一步證明程式碼的可用性。

程式碼診斷

程式碼診斷(Diagnostic),此處的診斷,指在編譯過程中,對程式碼是否可執行作出檢查,並報告相關問題位置的過程。一般的IDE在Build過程中均會進行診斷,診斷過程會報告問題型別並指出問題所在行號,但並非所有診斷都會存在行號。診斷內容涵蓋:

  • 語法合法性:語句本身是否合法

  • 檔案結構合法性:檔案內容是否符合某語言的基本要求

  • 呼叫合法性:檔案內涉及到的其他類或方法是否存在

程式碼診斷後,也就標誌程式碼檔案可以在當前環境中執行,但此階段內一般不會檢查執行時錯誤。一般的程式碼診斷會伴隨程式碼編譯過程進行,在程式碼編譯過程中通過監聽編譯過程獲得診斷資訊,但非編譯型語言的程式碼診斷是獨立進行的。

案例說明將與程式碼編譯過程一同進行。

程式碼編譯

程式碼編譯,及利用程式碼檔案生成為更底層目標檔案的過程。在編譯型語言中,翻譯為機器碼等可被計算機直接執行的內容;在混合型語言中,則將程式碼檔案編譯為可被JVM等執行環境識別的內容。

由於使用者提供的僅為程式碼部分,結合實際環境,程式碼可能具備不可通用性,只在固定環境下才可通過編譯,由於使用者使用的不是IDE,關於程式碼的診斷、編譯、載入過程都需要由平臺本身提供,即需要平臺開發者利用語言特性及固有工具開發相關功能。

關於編譯原理及流程相關內容,這裡不再贅述。

案例說明

網易貴金屬量化平臺使用的語言環境為Java。針對Java的編譯過程,在原生包javax.tools中提供了將Java原始檔編譯為.class檔案過程中需要的關鍵類,相關內容如下:

javax.tools.JavaCompiler:
/**
 * Interface to invoke Java™ programming language compilers from
 * programs.
 *
 * <p>The compiler might generate diagnostics during compilation (for
 * example, error messages).  If a diagnostic listener is provided,
 * the diagnostics will be supplied to the listener.  If no listener
 * is provided, the diagnostics will be formatted in an unspecified
 * format and written to the default output, which is {@code
 * System.err} unless otherwise specified.  Even if a diagnostic
 * listener is supplied, some diagnostics might not fit in a {@code
 * Diagnostic} and will be written to the default output.
 *
 ...複製程式碼

Java編譯工具, 編譯過程中會丟擲相關的診斷資訊。使用run方法執行編譯操作,也可先生成編譯任務(CompilationTask),之後呼叫CompilationTask的call方法執行編譯任務。

javax.tools.JavaFileObject:
/**
 * File abstraction for tools operating on Java&trade; programming language
 * source and class files.
 *
 * <p>All methods in this interface might throw a SecurityException if
 * a security exception occurs.
 *
 * <p>Unless explicitly allowed, all methods in this interface might
 * throw a NullPointerException if given a {@code null} argument.複製程式碼

Java原始檔物件,負責原始檔物件載入至記憶體。

javax.tools.JavaFileManage:
/**
 * File manager for tools operating on Java&trade; programming language
 * source and class files.  In this context, <em>file</em> means an
 * abstraction of regular files and other sources of data.
 ...複製程式碼

Java原始檔管理類, 用於管理一系列JavaFileObject。

javax.tools.Diagnostic:
/**
 * Interface for diagnostics from tools.  A diagnostic usually reports
 * a problem at a specific position in a source file.  However, not
 * all diagnostics are associated with a position or a file.
 ...複製程式碼

Java檔案診斷資訊。

javax.tools.DiagnosticListener:
/**
 * Interface for receiving diagnostics from tools.
 *
 * @param <S> the type of source objects used by diagnostics received
 * by this listener
 *複製程式碼

診斷資訊監聽器,編譯過程觸發。生成編譯任務(JavaCompiler.getTask())或獲取FileManager(JavaCompiler.getStandardFileManager())時需要傳遞DiagnosticListener以便收集診斷資訊。

在以上相關類的基礎上,呼叫方式如下:

public static void compile(File srcFile, String targetClassPath) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnosticListener = new DiagnosticCollector<>();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        Iterable it = fileManager.getJavaFileObjects(srcFile);
        createClassPathIfNotExists(targetClassPath);
        List<String> options = new ArrayList<>();
        options.add("-classpath");
        StringBuilder sb = new StringBuilder();
        URLClassLoader urlClassLoader = (URLClassLoader) Thread.currentThread().getContextClassLoader();        for (URL url : urlClassLoader.getURLs()) {
            sb.append(url.getFile().replace("%20", " ")).append(File.pathSeparator);
        }
        options.add(sb.toString());
        options.add("-d");
        options.add(targetClassPath);        try {
            JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticListener, options, null,
                    it);            boolean success = task.call();            if (!success) {
                StringBuilder errorMsg = new StringBuilder();                for (Diagnostic diagnostic : diagnosticListener.getDiagnostics()) {
                    errorMsg.append("line:").append(diagnostic.getLineNumber() - StrategyCodeConstant.DEFAULT_PRE_LINE)
                            .append(", ").append(diagnostic.getMessage(null)).append("\n");
                }                throw new CompileException(RetCode.COMPILE_ERROR, errorMsg.toString());
            }
        } catch (CompileException e) {            throw e;
        } catch (Exception e) {            throw new CompileException(RetCode.COMPILE_ERROR, e.getMessage(), e);
        }
    }複製程式碼

結合前面的方法說明,解釋方法內基本流程如下:

  1. 獲取系統編譯器

  2. 建立診斷監聽器

  3. 讀入Java原始檔

  4. 建立目標class檔案

  5. 設定類路徑等編譯引數

  6. 執行編譯任務

  7. 丟擲診斷資訊

經過上述流程後,如果監聽器未監聽到任何診斷,則最終生成的class檔案可直接被類載入器載入並執行。

在class檔案的留存方式上,可結合具體需要指定具體策略。如無需留存使用者程式碼,則可採用二進位制方式直接生成class檔案對應的記憶體,如果需要留存使用者程式碼,則看將編譯生成的class檔案以其他方式進行轉存。

程式碼執行

程式碼執行,即將編譯後的內容載入至指定環境執行,各語言根據自身特性均會提供相關流程,本身並無難度。此處的程式碼執行討論的內容,是如何將使用者程式碼與線上編譯器平臺本身執行環境相結合。

  • 一般場景下,使用者程式碼只依賴原生工具,自稱一體,如果語言存在類似JVM的執行環境,直接可以利用執行環境搭建簡易沙箱即可執行。

  • 特殊場景下,使用者程式碼除必要原生工具外,對平臺本身提供的API強依賴,由於API內容包羅永珍,可能涉及到外部訪問或公共伺服器記憶體使用,故單純的搭建沙箱,可能在一定程度上不能滿足需求。

既然單純的沙箱不能滿足需求,可能就面臨將使用者程式碼載入至平臺所在的執行環境中一同執行的情況。但在這一過程中,如何規範使用者程式碼的接入及呼叫動作就是重中之重,另外,如何在滿足使用者程式碼執行基本需求的基礎上又能維護平臺安全就是必須解決的問題(ps:安全問題會在另一篇文章中進行闡述)。

規範使用者程式碼的接入及呼叫動作,解決問題的入手點可以從以下幾個方面入手:

  • 明確使用者程式碼呼叫內容:使用者程式碼中究竟有何內容是必須使用平臺提供API的,是否可以窮舉所有行為。

  • 明確使用者程式碼結構:在明確行為的基礎上,使用者程式碼結構是否是可預知的,如果是可預知的,是否明確使用者程式碼存在對外互動介面。

  • 明確使用者程式碼呼叫方式:使用者程式碼只需被呼叫一次,還是需要呼叫若干次。

  • 明確使用者程式碼可能出現的問題:即便是程式碼診斷後,使用者程式碼還是可能出現執行時異常,對於這些可能出現的執行時異常,要有預估以及處理方案,是否跳過本次執行或者打斷執行過程。

結合以上思考點,需要確定的內容是:平臺應該如何去呼叫使用者程式碼,如何打通使用者程式碼到平臺的壁壘。

在較為合理的情況下,使用者程式碼經歷執行前的所有流程後,到這裡應該是可以預估形態的,使用者寫了什麼,會做什麼,怎麼用,平臺怎麼呼叫已經變得很明確了。

案例說明

網易貴金屬量化平臺,對使用者的策略程式碼,直接規定了模板,使用者編寫的Java類必須繼承策略模板結構並實現相關方法。

類策略模板介面內容:

/**
 * 策略類
 */public interface Strategy {    /**
     * 策略初始化的時候呼叫一次,用於選擇品種,設定手續費,金額,等等
     * 
     * @param context 上下文
     */
    void init(Context context);    /**
     * 策略的主要實現
     * 
     * @param context 上下文
     */
    void handle(Context context);    /**
     * 策略執行結束時呼叫一次
     * 
     * @param context 上下文
     */
    void onExit(Context context);

}複製程式碼

使用者的策略程式碼需要隨著時間推移,多次呼叫執行,進而模擬實際交易,此多次呼叫過程成為排程。具體的呼叫流程分為三個部分:

  1. 排程前:呼叫init方法,此方法內使用者需要初始化一些排程使用到的引數並給出初始值

  2. 排程中:按時間軸或行情訊息驅動方式,不斷執行handle方法內的策略主要實現內容,過程中會更新handle方法內涉及到的變數內容,使用者可以在這一過程中可隨意使用物件內變數用於變數的臨時儲存。排程內容包括行情查詢、模擬開倉平倉操作、數學計算等

  3. 排程後:執行onExit方法,此方法內使用者可以結合自身需要做策略排程完成時的處理動作,可以進行自定義的統計計算或輸出日誌等

量化平臺通過規定使用者程式碼結構的方式,進而規範了使用者程式碼的呼叫方式,使所有的使用者程式碼在呼叫過程中的行為保持統一。

內容反饋

內容反饋,使用者程式碼由產生到執行,需讓使用者感知到程式碼所產生的效果。在一般使用場景下,即簡單的線上編譯器,內容反饋表現在程式碼的編譯情況以及程式碼內輸出到控制檯的內容;但在特殊場景下,這兩部分的反饋內容對於使用者而言,是遠遠不夠的。

從反饋產生的時間上劃分,可分為以下三個階段:

  • 正式執行前,涵蓋內容包括:

    • 使用者程式碼語法檢測情況

    • 使用者程式碼編譯診斷情況

    • 使用者程式碼環境載入情況

  • 正式執行時,涵蓋內容包括:

    • 使用者程式碼執行中間值

    • 使用者程式碼執行時異常及錯誤

    • 使用者程式碼執行日誌

  • 執行結束後,涵蓋內容包括:

    • 使用者程式碼呼叫結束通知

    • 使用者程式碼方法返回值

    • 使用者程式碼生成的資料

    • 使用者資料計算、統計、圖形化處理結果等

對於以上內容,平臺可選擇性的向使用者進行反饋。反饋方式上,可根據平臺的具體表現形式進行選擇,也可分為同步和非同步兩部分進行分別通知,反饋通知形式包含:

  • 同步通知

    • 輸出控制檯

    • 訊息窗體等及時推送與反饋

    • 日誌訊息

  • 非同步通知

    • 執行日誌檔案

    • 執行情況報告檔案

    • 使用者原始資料

    • 使用者資料計算、統計、圖形化處理結果

案例說明

網易貴金屬量化平臺,對於內容反饋部分的實現,分為以下幾個部分:

  1. 程式碼檢測、診斷時,反饋檢測及診斷內容:

  1. 程式碼執行時,反饋系統及使用者自定義日誌

  1. 程式碼執行後,反饋策略日誌檔案、原始交易資訊、統計彙總

日誌檔案

原始交易資訊

統計彙總

反饋內容,應該結合需求使用者需求及使用反饋做出迭代調整,但內容只應限於使用者程式碼涉及部分,不應透露伺服器及平臺本身的執行狀態及重要引數資訊。

後文連結

本文結合網易貴金屬量化平臺實際運用場景,闡述了線上編譯器搭建思路,分析了各類可能的應用場景及思考要點,在這一過程中詳細介紹了編輯及編譯的過程。關於使用者程式碼的安全檢測與安全執行保障於後文闡述:

程式碼線上編譯器(下)- 使用者程式碼安全檢測

其他

Tool線上工具

推薦一個功能較為齊全的線上工具平臺:tool.lu/

Markdown圖片插入

由於Markdown不能直接插入圖片,圖片插入以連結方式進行,故需要用三方的圖床以儲存圖片並生成連結。推薦一個好用的圖床:微博圖床。可從chrome應用商店中下載外掛,登入微博後即可使用。可生成縮圖及原圖的HTTP、HTML、UBB、MarkDown連結。


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點選


相關文章:
【推薦】 構建SpringBoot基本框架(上篇)
【推薦】 SVN遷移到GIT


相關文章