1. 背景

Apache的Groovy是Java平台上设计的面向对象编程语言。这门动态语言拥有类似Python、Ruby和Smalltalk中的一些特性,可以作为Java平台的脚本语言使用,Groovy代码动态地编译成运行于Java虚拟机(JVM)上的Java字节码,并与其他Java代码和库进行互操作。由于其运行在JVM上的特性,Groovy可以使用其他Java语言编写的库。Groovy的语法与Java非常相似,大多数Java代码也符合Groovy的语法规则,尽管可能语义不同。

------来自wikipedia

在网上看到看到一个很有意思的比喻,Groovy之于Java,就好比狂草之于楷书。写好了一样赏心悦目,但是正式场合(企业级开发)还是严肃一点的多。

但Groovy的灵活性、Java良好的兼容(JVM)、本身的语法对于Java工程师来说学习成本不高使得其成为了一项被广泛使用的脚本语言。

2. 实现

单纯实现Groovy脚本执行很简单,一般有三种方式,GroovyClassLoader,GroovyShell,GroovyScirptEngine。它们之间的区别在于:

GroovyClassLoader 会动态地加载一个脚本并执行它,可使用Binding对象输入参数。GroovyClassLoader是一个定制的类装载器,负责解释加载Java类中用到的Groovy类。

GroovyShell允许在Java类中求任意Groovy表达式的值。可使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果,GroovyShell还支持一些沙盒环境等特性,多用于推求对立的脚本或表达式。

GroovyScirptEngine作为一个引擎,功能更全面,它本身提供一些脚本的缓存等机制。,如果换成相互关联的多个脚本,使用GroovyScriptEngine会更好些。GroovyScriptEngine从您指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。同样,也允许传入参数值,并能返回脚本的值。

针对本次的使用场景,最终使用的是groovyshell,原因在于使用脚本的场景更多的是想依赖其灵活动态的特性,不想Java逻辑一变就需要重新发布。而本身脚本的逻辑不会特别复杂,更多的是对传入的参数进行简单的计算看是否符合期望。那么,不要对主流程甚至是JVM本身,应用本身造成影响是我们着重考虑的点。

2.1 最简单的实现

GroovyClassLoader groovyLoader = new GroovyClassLoader();
Class<Script> groovyClass groovyClass = (Class<Script>) groovyLoader.parseClass(scriptString);
Script groovyScript = groovyClass.newInstance();
Binding binding = new Binding();
Map variables = binding.getVariables();
variables.putAll(params);
groovyScript.setBinding(binding);
Object groovyResult = groovyScript.run();

可以看到跑起来很容易,但是这里面有很多坑,其它博客上或者是在我们技术团队都发生过真实的教训。

3. 优化

3.1 GroovyClassLoader

毕大师的文章也说到,我们不要采用一个全局的GroovyClassLoader,parseClass方法得到的Class<Script>对象会保存在PermGen,而Class对象被GC的条件之一是其ClassLoader先被GC,这就会导致PermGen的Class<Script>对象越来越多,最后被打满的情况。

所以这里很多文章都会推荐在需要的时候新new 一个GroovyClassLoader。对于GroovyClassLoader更详细的介绍可以阅读参考文档1。第二点是,完全相同的脚本多次执行,我们能否复用Class<Script>对象,这里推荐用LRU cache的比较多。对应到程序里,我们使用自定义配置的guava缓存,其中evict的设置(maxSize,超时的计算逻辑)需要使用者结合自己的使用场景来具体配置。

private Cache<String, Class<Script>> innerLruCache = CacheBuilder.newBuilder().maximumSize(1000) //最大容量.expireAfterAccess(6, TimeUnit.HOURS) //缓存过期时长.concurrencyLevel(Runtime.getRuntime().availableProcessors())// 设置并发级别为cpu核心数.build();String scriptKey = HashUtils.md5Hash(script.getBytes());
Class<Script> groovyClass = innerLruCache.getIfPresent(scriptKey);
if (groovyClass == null) {//缓存穿透,走2.1前3行的老逻辑//同时在每次更新缓存Class<Script>对象时候,采用了不同的groovyClassLoaderinnerLruCache.put(scriptKey, groovyClass);
}

3.2 自定义配置

在查看了很多资料后,发现如果替换GroovyClassLoader使用GroovyShell会获得更多安全性。而使用GroovyShell的时候本身对3.1GroovyClassLoader优化的思想不用变,因为GroovyShell的构造函数也会重新new 一个GroovyClassLoader

public GroovyShell(ClassLoader parent, Binding binding, final CompilerConfiguration config) {if (binding == null) {throw new IllegalArgumentException("Binding must not be null.");}if (config == null) {throw new IllegalArgumentException("Compiler configuration must not be null.");}final ClassLoader parentLoader = (parent!=null)?parent:GroovyShell.class.getClassLoader();this.loader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() {public GroovyClassLoader run() {return new GroovyClassLoader(parentLoader,config);}});this.context = binding;        this.config = config;
}

所以这个时候,2.1前三行的逻辑可以被替换为

//自定义配置
CompilerConfiguration config = new CompilerConfiguration();//添加线程中断拦截器,可拦截循环体(for,while)、方法和闭包的首指令
config.addCompilationCustomizers(new ASTTransformationCustomizer(ThreadInterrupt.class));//添加线程中断拦截器,可中断超时线程,当前定义超时时间为3s
Map<String, Object> timeoutArgs = ImmutableMap.of("value", 3);
config.addCompilationCustomizers(new ASTTransformationCustomizer(timeoutArgs, TimedInterrupt.class));//沙盒环境
config.addCompilationCustomizers(new SandboxTransformer());
GroovyShell sh = new GroovyShell(config);
new NoSystemExitSandbox().register();
new NoRunTimeSandbox().register();//确保在每次更新缓存Class<Script>对象时候,采用不同的groovyClassLoader
groovyScript = sh.parse(script);
groovyClass = (Class<Script>) groovyScript.getClass();

3.2.1 运行时元编程

可以看到采取自定义配置,我们能够通过Groovy运行时元编程来获得一些安全特性,eg:死循环的处理,中断超时等。虽然会损失一些性能,但是对于我的使用场景(安全>时效),是可以接受的。

关于运行时元编程:

它容许编译时生成代码。这种转换会影响程序的抽象语法树(AST,Abstract Syntax Tree),这也就是我们在 Groovy 中把它称为 AST 转换的原因。AST 转换能使我们实时了解编译过程,继而修改 AST,从而继续编译过程,生成常规的字节码。与运行时元编程相比,编译时元编程在类文件自身中(也就是说,在字节码内)就可以看到变化。这一点是非常重要的,比如说当你想让代码转换成为类抽象一部分时(实现接口,继承抽象类,等等),或者甚至当需要让类可从 Java (或其他的 JVM 语言)中调用时。

AST 转换可以为一个类添加一些方法。如果用运行时元编程来实现的话,新方法只能可见于 Groovy;而用编译时元编程来实现,新方法也可以在 Java 中显现出来。最后一点也同样重要,编译时元编程的性能要好过运行时元编程(因为不再需要初始化过程)。

------Groovy元编程(即参考文档2)

3.2.2 沙盒环境

引入沙盒环境实际上是在编译阶段对Groovy脚本做处理,特别是作为脚本执行其实我们只想让它使用合理的JVM资源,更不能执行像System.exit()这样的风险方法。

config.addCompilationCustomizers(new SandboxTransformer());就是允许Groovy代码进行沙盒转换的操作,沙盒具体执行哪些限制需要我们注册实现了GroovyInterceptor的类的实例。

举个例子,下面两个类分别禁止执行System.exit()方法以及Runtime这个类的所有方法。

class NoSystemExitSandbox extends GroovyInterceptor {@Overridepublic Object onStaticCall(GroovyInterceptor.Invoker invoker, Class receiver, String method, Object... args) throws Throwable {if (receiver == System.class && method == "exit") {throw new SecurityException("No call on System.exit() please");}return super.onStaticCall(invoker, receiver, method, args);}
}class NoRunTimeSandbox extends GroovyInterceptor {@Overridepublic Object onStaticCall(GroovyInterceptor.Invoker invoker, Class receiver, String method, Object... args) throws Throwable {if (receiver == Runtime.class) {throw new SecurityException("No call on RunTime please");}return super.onStaticCall(invoker, receiver, method, args);}
}

3.2.3 编译检查

这个稍微trick一点,实质上是使用GroovyShell的parse方法,如果编译报错会抛出MultipleCompilationErrorsException,异常信息还是很全的,甚至告诉你哪一行哪里出了什么问题,同时搭配ErrorCollector,能够获得一些额外的编译报错的信息。

try{groovyScript = sh.parse(scirptString);
}catch (MultipleCompilationErrorsException exception) {ErrorCollector errorCollector = exception.getErrorCollector();LOGGER.info("here are {} errors when compiling the script", errorCollector.getErrorCount());//do something else
}

3.3 脚本执行&结果获取

在2.1最简单的实现一节,按照那种写法,如果run()方法真的有什么问题,比如恶意的sleep,或者确实因为某些不可预测的原因(死循环已经可以)导致要执行很长时间,那主线程就卡顿在这里,这是不能接受的。所以我们要引入线程池来做这件事情。关于线程池这里不做过多解释,具体可以根据需要定制出更符合场景的线程池。

/*** fixed thread pool* 线程数为cpu核心数* 最多等待1000个任务,超过使用默认丢弃策略,抛出异常
*/
private ThreadPoolExecutor threadPool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),Runtime.getRuntime().availableProcessors(), 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
//对2.1第8行代码的改造
Future<Object> future = threadPool.submit((Callable<Object>) groovyScript::run);
try{Object groovyResult = future.get(3, TimeUnit.SECONDS);
}catch (TimeoutException exception) {future.cancel(true);LOGGER.info("try cancel future task, is cancelled:{}", future.isCancelled());//do something else
}

这里要特别解释一下前面使用V get(long timeout, TimeUnit unit)

throws InterruptedException, ExecutionException, TimeoutException;

是否和前面3.2.1运行时元编程的线程超时中断冲突。其实不会,比如再未引入运行时元编程时,我执行如下脚本

def eq (var1,var2){println('before sleep')sleep(4000)println('after sleep')return var1 == var2
};
eq(true,false); 

在3000ms过后,future.get()拿不到脚本执行的返回结果,抛出TimeoutException,但是主线程感知到了超时继续做别的处理,但是线程池中执行脚本的线程并没有真正被中断,主线程先打印出随后try cancel future task, is cancelled:{true},之后'after sleep'还是会被打印出来。所以实际上前面的运行时元编程线程超时中断是一个很好的补充机制。

关于future.cancel(true),查了一下这个方法是会调用interrupt()方法,但是只是设置了状态标志位,但并不是说线程就一定会被直接中断掉。

4.写在后面

接到这个需求的时候我是一脸懵的,因为groovy是一点都没接触过,最后用了4个人日做了这样一版东西,我深知不光要会用,能跑就行是最低要求而已,还要多了解一些原理性的东西。但是不免有一些纰漏和不足之处,欢迎大家多多指正。

文章的顺序基本上是我参考来自公司技术博客,各大技术博客,stackoverflow一点一点优化的过程,这个过程还是收获满满的,现在这段代码是跑在线上的,后面会继续观察它的表现,以及是否对JVM造成了影响。很多前辈都趟过雷,站在前人的经验教训上确实受益很多。

另外,为了避免冗长的代码影响大家思考,我将诸如异常处理等一些逻辑贴的比较少,但异常处理本身非常重要。

最后,希望能够对其他人起到一些帮助作用。

5. 参考文档

  • Groovy深入探索——Groovy的ClassLoader体系
  • Groovy元编程

groovy脚本执行与优化相关推荐

  1. 【Groovy】Groovy 脚本调用 ( 命令行执行 Groovy 脚本并传入参数 | 获取 Groovy 脚本执行参数 )

    文章目录 前言 一. 命令行执行 Groovy 脚本并传入参数 二.获取 Groovy 脚本执行参数 前言 在 Groovy 脚本 , Groovy 类 , Java 类中 , 可以调用 Groovy ...

  2. [SoapUI] 在SoapUI中通过Groovy脚本执行window命令杀掉进程

    //杀Excel进程 String line def p = "taskkill /F /IM EXCEL.exe".execute() def bri = new Buffere ...

  3. Groovy脚本极限优化

    前段时间开发的项目,项目需求要求支持业务人员频繁业务需求变更,业务要求每次策略变更第一时间线上生效.结合项目业务需要,我们选择进行业务领域抽象,把业务变更的需求提炼成为脚本操作,每次业务人员对业务的操 ...

  4. java调用Groovy脚本

    一.使用 用 Groovy 的 GroovyClassLoader ,它会动态地加载一个脚本并执行它.GroovyClassLoader是一个Groovy定制的类装载器,负责解析加载Java类中用到的 ...

  5. ElasticSearch Groovy脚本远程代码执行漏洞

    什么是ElasticSearch? 它是一种分布式的.实时性的.由JAVA开发的搜索和分析引擎. 2014年,曾经被曝出过一个远程代码执行漏洞(CVE-2014-3120),漏洞出现在脚本查询模块,由 ...

  6. 【Groovy】MOP 元对象协议与元编程 ( 使用 Groovy 元编程进行函数拦截 | 动态拦截函数 | 动态获取 MetaClass 中的方法 | evaluate 方法执行Groovy脚本 )

    文章目录 一.基础示例 二.根据字符串动态获取 MetaClass 中的方法 二.使用 evaluate 执行字符串形式的 Groovy 脚本 二.完整代码示例 一.基础示例 定义类 Student ...

  7. 【Groovy】Groovy 脚本调用 ( Groovy 类中调用 Groovy 脚本 | 创建 GroovyShell 对象并执行 Groovy 脚本 | 完整代码示例 )

    文章目录 一.Groovy 类中调用 Groovy 脚本 1.创建 GroovyShell 对象并执行 Groovy 脚本 2.代码示例 二.完整代码示例 1.调用者 Groovy 脚本的类 2.被调 ...

  8. 【Groovy】Groovy 脚本调用 ( Groovy 脚本中调用另外一个 Groovy 脚本 | 调用 evaluate 方法执行 Groovy 脚本 | 参数传递 )

    文章目录 一.Groovy 脚本中调用另外一个 Groovy 脚本 1.调用 evaluate 方法执行 Groovy 脚本 2.参数传递 二.完整代码示例 1.调用者 Groovy 脚本 2.被调用 ...

  9. shell的建立与执行实验报告_实验七 Shell脚本运行的优化

    实验七 Shell脚本运行的优化 一.添加窗口 在Shell脚本的运行过程中,也可以实现类似于Windows系统和Linux系统中的窗口效果,使脚本运行美观. 1.dialog软件的安装 - dial ...

最新文章

  1. R语言单因素重复测量方差分析(one-way repeated measures ANOVA)实战
  2. 一个用BitMap类完成的网页随机码图片生成类
  3. 卡尔曼滤波、粒子滤波【通俗解释】
  4. c语言写程序轮询是什么意思,单片机轮询按键程序
  5. 【script】数据处理的瑞士军刀 pandas
  6. 【转载】Mysql注入点在limit关键字后面的利用方法
  7. 高并发编程知识体系阅读总结
  8. SpringBoot-Freemarker与SpringBoot集成
  9. 可以修改Mac地址的工具WiFiSpoof for Mac
  10. opencv3.2.0实现视频抽帧,并保存成图片
  11. 微信小程序 java通过 rawData 和 session_key 生成 signature 签名
  12. msm8937 bootloader流程分析
  13. 实现ftpserver
  14. 基于Maven的Springboot+Mybatis+Druid+Swagger2+mybatis-generator框架环境搭建
  15. 将寄存器放入IOB的方法
  16. IEEE论文投稿模板及分类简介
  17. 函数的单调性和曲线的凹凸性
  18. 小米2miui适配android6,MIUI官方声明:小米2/2S确定升级MIUI6
  19. Visio镜像翻转图形
  20. 服务器和kad正在连接,P2P连不上kad网络怎么解决?P2P连不上kad网络的处理方法教程详解...

热门文章

  1. Altair Simdroid 流体分析模块介绍
  2. 金蝶云星空二维码整体解决方案 金蝶云星空条码管理系统 金蝶ERP移动解决方案 金蝶云星空条码扫描 金蝶云星空WMS仓库移动扫码 金蝶安卓PDA扫码方案 金蝶云星空出入库盘点出货条码扫码 提供源码
  3. 【C++】VSCode配置C++环境(详细教程)
  4. 小米android什么意思,小米互传的作用是什么?小米互传被称为安卓版Airdrop
  5. Kubernetes组件Ingress
  6. Dialog和PopupWindow的区别
  7. Pytorch cuda out of memory
  8. P5707 【深基2.例12】上学迟到
  9. python软件要钱吗-python 收费么
  10. ibm服务器面板报警指示灯含意