WsztRush

解决运行Groovy脚本中的问题

在需要灵活配置时Groovy通常时一个不错的选择,但是问题比较多:

  1. 效率
  2. 垃圾回收
  3. 死循环
  4. 调用System.exit()等不安全的方法
  5. 拼命申请内存导致OOM:现在没有想到方案

第一、二中已经有比较成熟的方案,但是后面的两个问题就不那么好解决了。

死循环

执行Groovy脚本的时候我用的是下面的方法:

Class clazz = new GroovyClassLoader().parseClass("your code");
Method method = clazz.getMethod("xxxx");
method.invoke(null);

如果用户编写的脚本是(能用技术解决的东西最好不要用价值观来保证):

while(true){
	// do nothing.
}

机器的CPU马上会飚的很高,可能你会想到用Thread.stop()来终止这个罪恶的脚本,但是对于这个脚本:

while(true){
	try{/* your code */}
    catch(Exception e){ }
}

只能说根本停不下来。因为:

Thread.stop()仅仅是在线程的任意位置抛出ThreadDeath异常。

况且Thread.stop很早就不建议使用了,而是用Thread.interrupt(),但是简单来说中断仅仅是去通知一下目标线程,而不是真的去停掉它。

那么,现在的目标就是如何将中断检查的代码插入到用户编写的脚本中:

public static void checkInterrupted() {
    if (Thread.currentThread().isInterrupted()) {
        throw new RuntimeException("task is interrupted!");
    }
}

将输入的脚本作为字符串来处理,估计会累到吐血!接着自然想到先将源码结构化,那么自然想到在编译的过程中对语法树进行操作即可达到目的:

public static class SafeGroovyClassLoader extends GroovyClassLoader {
    protected CompilationUnit createCompilationUnit(CompilerConfiguration config, CodeSource source) {
    CompilationUnit compilationUnit = super.createCompilationUnit(config, source);
    compilationUnit.addPhaseOperation(new CompilationUnit.SourceUnitOperation() {
        public void call(SourceUnit source) throws CompilationFailedException {
            ModuleNode ast = source.getAST();
            // 自定义visitor来操作
        }
    }
}

遍历时我们需要处理的节点包括:

  1. FOR
  2. WHILE
  3. DOWHILE
  4. CATCH
  5. FINALLY

在这些块的开始位置插入checkInterrupted方法的调用:

private BlockStatement createCheckBlock(Statement statement) {
    if (statement instanceof BlockStatement) {
        BlockStatement blockStatement = (BlockStatement) statement;
        blockStatement.getStatements().add(0, checkInterruptedStatement);
        return blockStatement;
    } else if (statement instanceof EmptyStatement) {
        BlockStatement blockStatement = new BlockStatement();
        blockStatement.getStatements().add(checkInterruptedStatement);
        return blockStatement;
    } else {
        BlockStatement blockStatement = new BlockStatement();
        blockStatement.getStatements().add(checkInterruptedStatement);
        blockStatement.getStatements().add(statement);
        return blockStatement;
    }
}

public void visitCatchStatement(CatchStatement catchStatement) {
    catchStatement.getCode().visit(this);
    catchStatement.setCode(createCheckBlock(catchStatement.getCode()));
}

你想的话可以在checkInterrupted中加入循环次数统计。现在线程可以中断,貌似问题已经解决了,但是谁来中断他?有三种方案:

  1. 用线程池来运行脚本,在外面用Future.get来等待(超时中断)
  2. 创建一个线程来join当前线程(超时中断)
  3. 执行任务前将当前线程插入到map中,然后用一个定时任务来扫描并将超时的中断

第一、二种方案每个请求都会创建多余一个线程(不划算),那就来看看第三种方案的实现方式:

// 保存线程开始时间
Map<Thread, Long> threadCache = Maps.newHashMap();
// 执行前将线程插入
threadCache.put(Thread.currentThread(), System.currentTimeMillis());
// 遍历threadCache根据时间判断是否中断

这种方案实现起来要好好考虑一下同步的问题!

System.exit

在不做限制的情况下,执行上面方法系统直接就停止了,怎么办?办法很简单(当然不是靠价值观):建立黑名单,在遍历语法树时是否调用了黑名单中的方法。

总结

应用这么多,可能大家都需要用到一些脚本的功能,但是能也没有比较把这些都再各个系统里面去实现一把(没啥意义),如果有这么一个打通脚本与应用、数据的平台也不错!