Java脚本API运行脚本程序防止脚本死循环

前提概要

当我们使用java脚本API运行脚本的时候,在一些我们并不知道脚本的程序逻辑
并且无法修改脚本的特殊的场景下,如果脚本中存在死循环(endless loop)
或者高资源消耗的耗时循环语句,程序运行将会占用大量的系统资源,比如说CPU、
磁盘IO等。如果脚本程序是死循环并且程序同步地执行脚本的话,那么程序将会一直阻塞下去。

解决办法

由于在这些场景下,我们无法控制脚本的程序逻辑,无法改动脚本的代码,
所以有必要对脚本的执行进行控制。在这里我们可以通过异步调用的方式,
防止脚本执行阻塞对主程序带来的负面影响。并且通过添加超时机制,
对脚本执行超时的线程进行强制关闭,避免有死循环嫌疑的恶意脚本对系统资源的恶意消耗。

程序示例

1.编写脚本执行线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 脚本执行线程
*/
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.定义脚本执行超时异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 脚本执行超时异常
*/
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.编写脚本执行阻塞方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 阻塞等待脚本执行结束,或者到达超时时间
*
* <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.编写脚本执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 执行脚本中的方法
*
* @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