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.编写脚本执行阻塞方法

/**
 * 阻塞等待脚本执行结束,或者到达超时时间
 * 
 * 
 *     脚本执行超过等待时间,强制停止脚本线程
 * 
* * @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 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

你可能感兴趣的:(Java,groovy,脚本)