动态执行Groovy脚本会遇到一个问题,那就是写Groovy脚本的人整活,在脚本里写了一个死循环。这时候整个系统就都GG了。
解决方案
在一个线程池里运行groovy脚本,如果线程运行超时就杀死这个线程
核心代码
private ExecutorService executor = Executors.newCachedThreadPool();
private int scriptTimeout = 1000;
try {
future = invoke(method, instance);
return future.get(scriptTimeout, TimeUnit.MILLISECONDS);
// 超时
} catch (TimeoutException e) {
killFutureThread(future);
getLogger().error(e.toString(), e);
return false;
// interrupt
} catch (InterruptedException e) {
future.cancel(true);
throw e;
// 执行异常
} catch (ExecutionException e) {
Throwable cause = t.getCause();
if(cause instanceof InvocationTargetException) {
// InvocationTargetException 的异常栈不一样
getLogger().error("执行错误", ((InvocationTargetException) cause).getTargetException());
}else{
getLogger().error("执行错误", StackTraceUtils.deepSanitize(cause));
}
} finally {
bindings.clear();
}
public FutureTask<Boolean> invoke(Method method, Script instance){
return (FutureTask<Boolean>) executor.submit(() -> {
return (boolean) method.invoke(instance);
});
}
// 单纯的future.cancel() 只有在线程阻塞时才会停掉线程,如果线程里是一个不会sleep的死循环,就无法成功停止。
// 这种情况下只能通过Thread.stop()来强行杀掉线程(这是一个因为比较危险所以被标记为废弃的函数)。
public void killFutureThread(FutureTask future){
try {
if (future.isDone()) {
return;
}
// 利用反射,强行取出正在运行该任务的线程
// 发现FutureTask有两种实现,目前开发使用的oracle java8和Linux上的openjdk8 / openjdk11,Thread都是直接作为私有属性runner
// 但是看到老的GNU的实现Thread属性是藏在FutureTask.Sync.runner属性,需要多反射一次Sync (http://fuseyism.com/classpath/doc/java/util/concurrent/FutureTask-source.html)
// 虽然只支持第一种在不通环境测试都通过了,还是做一下兼容
Thread execThread = null;
try{
Field runner = FutureTask.class.getDeclaredField("runner");
runner.setAccessible(true);
execThread = (Thread) runner.get(future);
}catch(NoSuchFieldException e) {
Field sync = FutureTask.class.getDeclaredField("sync");
sync.setAccessible(true);
Object obj = sync.get(future);
Field runner = obj.getClass().getDeclaredField("runner");
runner.setAccessible(true);
execThread = (Thread) runner.get(obj);
}
if (execThread == null) {
return;
}
Thread.State state = execThread.getState();
if (state == Thread.State.RUNNABLE) {
execThread.stop();// 非阻塞线程才STOP,例如死循环
}
// 其他情况cancel里会使用interrupt()方式,例如sleep、wait等等
future.cancel(true);
} catch (Exception e) {
getLogger().error("杀死超时的线程失败");
}
}