背景
在很多场景下有需要执行异步任务,或者执行用户的自定义任务时,通常我们会使用Groovy脚本能力来完成任务。通过groovy动态脚本能力,在业务执行过程中动态执行不同业务线或者用户的脚本,来满足不同需求。
这样可以非常方便的进行业务拓展,但是也会带来一系列安全问题,
1 比如在脚本中调用了系统危险的方法,如System.exit 会导致整个服务停止
2 触发了死循环等场景,会导致任务卡死,使用多线程的话线程也很块就被占完。
3 使用Thread.sleep 将线程进行休眠
解决方案
关于以上三类问题,这里也进行了归纳总结,给出对应的方案
死循环执行
1 先定义一个死循环执行脚本,功能就是一直打印就可以了
private static String script = "import groovy.transform.TimedInterrupt\n" +
"\n" +
"import java.util.concurrent.TimeUnit\n" +
"\n" +
"class GroovyScriptTest {\n" +
" public String execute(String key) {\n" +
" while (true) {\n" +
" print(11);\n" +
" }\n" +
" return key + \":updated\";\n" +
" }\n" +
"}\n";
为了方便查看生成后的源码,这里将生成的目录设置为target目录下
public class GroovyClassLoaderTest2 {
private static String script = "import groovy.transform.TimedInterrupt\n" +
"\n" +
"import java.util.concurrent.TimeUnit\n" +
"\n" +
"class GroovyScriptTest {\n" +
"\n" +
" public String execute(String key) {\n" +
" while (true) {\n" +
" print(11);\n" +
" }\n" +
" return key + \":updated\";\n" +
" }\n" +
"}\n";
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
CompilerConfiguration config = new CompilerConfiguration();
config.setTargetDirectory(GroovyClassLoaderTest2.class.getClassLoader().getResource("./").getPath());
// 重置调用时间
GroovyClassLoader groovyClassLoader = new GroovyClassLoader(GroovyClassLoaderTest2.class.getClassLoader(), config);
Class aClass = groovyClassLoader.parseClass(script);
GroovyObject groovyObject = (GroovyObject) aClass.newInstance();
Object o = groovyObject.invokeMethod("execute", "key");
System.out.println("groovy执行结果:" + o);
}
}
可以看到 运行起来后控制台一直在输出1,在实际业务场景中,我们需要对这种行为进行管控,可以使用Groovy自带的注解
@TimedInterrupt(unit = TimeUnit.MILLISECONDS, value = 1000L)
这里可以对方法进行执行时间设置,可以指定执行时间单位和时间,比如这里设置执行为1000ms,到时间后任务将自动结束
这样就达到了我们需要的效果。
Thread.sleep问题
在脚本中使用了指定睡眠时间的场景,如适用Thread.sleep(100000)会严重的拖慢了整体执行效率,此时可以通过Groovy自带的机制
@groovy.transform.ThreadInterrupt
添加此注解后,我们可以主动设置线程为已中断的,如果使用线程池的话可以使用futuretask的cancel(true)方式超时中断线程
危险方法调用
这里有两种方式,一种是通过执行过程中的拦截器进行处理,可以查看GroovyInterceptor,还有就是在编译期间就识别出来危险方法,在前置阶段进行拦截(SecureASTCustomizer.ExpressionChecker),核心代码如下:
public static class NoSupportClassTest implements SecureASTCustomizer.ExpressionChecker {
@Override
public boolean isAuthorized(Expression expression) {
if (expression instanceof MethodCallExpression) {
MethodCallExpression mc = (MethodCallExpression) expression;
String className = mc.getReceiver().getText();
String method = mc.getMethodAsString();
System.out.println("=====>"+className + "." + method);
}
return true;
}
}
只要识别到需要拦截的方法,这里返回false就可以进行前置拦截。
原理分析
通过以上两种方式,可以对循环(for,while)和线程睡眠的方式进行拦截处理,到target目录下查看生成的class文件,在方法执行的时候会先判断线程是否已经被中断了,在每个循环执行的时候会判断下执行时间,这样组合起来就可以非常好的达到了我们需要的业务效果。
完整代码
public class GroovyClassLoaderTest3 {
private static String script = "import groovy.transform.TimedInterrupt\n" +
"\n" +
"import java.util.concurrent.TimeUnit\n" +
"\n" +
"class GroovyScriptTest {\n" +
"\n" +
" @TimedInterrupt(unit = TimeUnit.MILLISECONDS, value = 1000L)\n" +
" @groovy.transform.ThreadInterrupt\n" +
" public String execute(String key) {\n" +
" while (true) {\n" +
" print(11);\n" +
" }\n" +
" return key + \":updated\";\n" +
" }\n" +
"}\n";
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
CompilerConfiguration config = new CompilerConfiguration();
config.setTargetDirectory(GroovyClassLoaderTest3.class.getClassLoader().getResource("./").getPath());
SecureASTCustomizer secure = new SecureASTCustomizer();
secure.addExpressionCheckers(new NoSupportClassTest());
config.addCompilationCustomizers(secure);
// 重置调用时间
GroovyClassLoader groovyClassLoader = new GroovyClassLoader(GroovyClassLoaderTest3.class.getClassLoader(), config);
Class aClass = groovyClassLoader.parseClass(script);
GroovyObject groovyObject = (GroovyObject) aClass.newInstance();
Object o = groovyObject.invokeMethod("execute", "key");
System.out.println("groovy执行结果:" + o);
}
public static class NoSupportClassTest implements SecureASTCustomizer.ExpressionChecker {
@Override
public boolean isAuthorized(Expression expression) {
System.out.println(expression);
if (expression instanceof MethodCallExpression) {
MethodCallExpression mc = (MethodCallExpression) expression;
String className = mc.getReceiver().getText();
String method = mc.getMethodAsString();
System.out.println("=====>"+className + "." + method);
}
return true;
}
}
}