Java腳本API運行腳本程序防止腳本死循環

Java腳本API運行腳本程序防止死循環

前提概要

當我們使用java腳本API運行腳本的時候,在一些我們並不知道腳本的程序邏輯並且無法修改腳本的特殊的場景下,如果腳本中存在死循環(endless loop)或者高資源消耗的耗時循環語句,程序運行將會佔用大量的系統資源,比如說CPU、磁盤IO等。如果腳本程序是死循環並且程序同步地執行腳本的話,那麼程序將會一直阻塞下去。

解決辦法

由於在這些場景下,我們無法控制腳本的程序邏輯,無法改動腳本的代碼,所以有必要對腳本的執行進行控制。在這裏我們可以通過異步調用的方式,防止腳本執行阻塞對主程序帶來的負面影響。並且通過添加超時機制,對腳本執行超時的線程進行強制關閉,避免有死循環嫌疑的惡意腳本對系統資源的惡意消耗。

程序示例

1.編寫腳本執行線程

/**
 * 腳本執行線程
 */
private static abstract class ScriptThread extends Thread {
    private boolean done = false;

    boolean isDone() {
        return done;
    }

    @Override
    public void run() {
        execute();
        this.done = true;
    }

    public abstract void execute();
}

說明:

  • 線程中添加變量 done , 用來標誌腳本執行正常結束的情況。
  • run方法中調用execute()方法,execute執行完成將done標誌位置爲true。
  • 通過isDOne()方法判斷線程是否正常結束。
  • 創建ScriptThread對象需要實現execute()方法,方法內部添加執行腳本的邏輯代碼。

2.定義腳本執行超時異常類

/**
 * 腳本執行超時異常
 */
public static class ScriptTimeoutException extends Exception {
    private static final long serialVersionUID = 1L;
    private int timeout;

    public ScriptTimeoutException() {
        super("Script execute timeout.");
    }

    public ScriptTimeoutException(int timeout) {
        super("Script execute timeout.");
        this.timeout = timeout;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
}

說明:

  • 腳本執行超時後拋出ScriptTimeoutException對象,用來區分異常類型。

3.編寫腳本執行阻塞方法

/**
 * 阻塞等待腳本執行結束,或者到達超時時間
 * 
 * <pre>
 *     腳本執行超過等待時間,強制停止腳本線程
 * </pre>
 *
 * @param task 腳本執行線程
 * @return 1:腳本正常執行結束 2:腳本強制退出執行 0:其他
 */
@SuppressWarnings("deprecation")
private static int waitScriptRunning(ScriptThread task) {
    int result = 0;
    long start = System.currentTimeMillis();
    while (true) {
        if (task.isDone()) {//如果腳本執行已經結束
            result = 1;
            break;
        }
        long current = System.currentTimeMillis();
        if (current - start >= waitTime) {//超過腳本執行等待時間還未結束,取消執行,強制關閉線程
            if (!task.isDone()) {
                result = 2;
                task.stop();
            }
            break;
        }
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        }
    }
    return result;
}

說明:

  • 調用該方法將阻塞等待,直到線程執行完畢或者線程超時,如果超時,則強制關閉線程。(JDK Thread#stop()方法不推薦使用,在當前的特殊場景下,我們無法修改腳本邏輯,無法在腳本內部控制線程的中斷,因此需要使用stop方法對線程進行強制退出。)

4.編寫腳本執行方法

/**
 * 執行腳本中的方法
 *
 * @param scriptLang   腳本語言
 * @param script       需要執行的腳本文本
 * @param functionName 執行的方法名
 * @param args         執行腳本方法傳入的參數
 * @return 腳本返回值
 * @throws Exception exception
 */
public static Object invokeScriptFunction(String scriptLang, String script, String functionName, Object... args)
        throws Exception {
    final Map<String, Object> map = new HashMap<>();

    ScriptThread scriptThread = new ScriptThread() {
        @Override
        public void execute() {
            try {
                ScriptEngine engine = getEngine(scriptLang);
                if (engine == null)
                    throw new Exception(String.format("Script engine not get! No support for script [%s].", scriptLang));
                engine.eval(script);
                map.put("value", ((Invocable) engine).invokeFunction(functionName, args));
            } catch (Exception e) {
                map.put("exception", e);
            }
        }
    };
    scriptThread.start();

    int result = waitScriptRunning(scriptThread);
    if (result == 2) {
        throw new ScriptTimeoutException(waitTime);
    }

    Object o = map.get("exception");
    if (o != null) {
        throw (Exception) o;
    }
    return map.get("value");
}

說明:

  • 方法邏輯中首先新建腳本執行線程並提交線程的執行,接着調用等待方法阻塞等待腳本的執行,如果返回結果說明腳本超時,則拋出超時異常。
  • 腳本執行線程execute方法體中定義執行腳本的邏輯,將執行腳本正常情況下的返回值、異常情況下的異常對象儲存到map中。
  • 如果腳本執行正常結束沒有超時,則拿到map中的內容,若異常對象不爲空則拋出異常對象;若異常對象爲空則將腳本返回值返回。

附上Java腳本執行工具項目地址

https://github.com/johnsonmoon/ScriptExecuter

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章