背景
在很多场景下有需要执行异步任务,或者执行用户的自定义任务时,通常我们会使用Groovy脚本能力来完成任务。通过groovy动态脚本能力,在业务执行过程中动态执行不同业务线或者用户的脚本,来满足不同需求。
这样可以非常方便的进行业务拓展,但是也会带来一系列安全问题,
1 比如在脚本中调用了系统危险的方法,如System.exit 会导致整个服务停止
2 触发了死循环等场景,会导致任务卡死,使用多线程的话线程也很块就被占完。
3 使用Thread.sleep 将线程进行休眠
解决方案
关于以上三类问题,这里也进行了归纳总结,给出对应的方案
死循环执行
1 先定义一个死循环执行脚本,功能就是一直打印就可以了
privatestaticString 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目录下
publicclassGroovyClassLoaderTest2{privatestaticString 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";publicstaticvoidmain(String[] args)throwsInstantiationException,IllegalAccessException{CompilerConfiguration config =newCompilerConfiguration();
config.setTargetDirectory(GroovyClassLoaderTest2.class.getClassLoader().getResource("./").getPath());// 重置调用时间GroovyClassLoader groovyClassLoader =newGroovyClassLoader(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),核心代码如下:
publicstaticclassNoSupportClassTestimplementsSecureASTCustomizer.ExpressionChecker{@OverridepublicbooleanisAuthorized(Expression expression){if(expression instanceofMethodCallExpression){MethodCallExpression mc =(MethodCallExpression) expression;String className = mc.getReceiver().getText();String method = mc.getMethodAsString();System.out.println("=====>"+className +"."+ method);}returntrue;}}
只要识别到需要拦截的方法,这里返回false就可以进行前置拦截。
原理分析
通过以上两种方式,可以对循环(for,while)和线程睡眠的方式进行拦截处理,到target目录下查看生成的class文件,在方法执行的时候会先判断线程是否已经被中断了,在每个循环执行的时候会判断下执行时间,这样组合起来就可以非常好的达到了我们需要的业务效果。
完整代码
publicclassGroovyClassLoaderTest3{privatestaticString 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";publicstaticvoidmain(String[] args)throwsInstantiationException,IllegalAccessException{CompilerConfiguration config =newCompilerConfiguration();
config.setTargetDirectory(GroovyClassLoaderTest3.class.getClassLoader().getResource("./").getPath());SecureASTCustomizer secure =newSecureASTCustomizer();
secure.addExpressionCheckers(newNoSupportClassTest());
config.addCompilationCustomizers(secure);// 重置调用时间GroovyClassLoader groovyClassLoader =newGroovyClassLoader(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);}publicstaticclassNoSupportClassTestimplementsSecureASTCustomizer.ExpressionChecker{@OverridepublicbooleanisAuthorized(Expression expression){System.out.println(expression);if(expression instanceofMethodCallExpression){MethodCallExpression mc =(MethodCallExpression) expression;String className = mc.getReceiver().getText();String method = mc.getMethodAsString();System.out.println("=====>"+className +"."+ method);}returntrue;}}}
版权归原作者 cmlbeliever 所有, 如有侵权,请联系我们删除。