动态执行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("杀死超时的线程失败");}}
