Java中运行Groovy的三种方式以及对比。

近期在做设备标签,业务需要可以根据设备属性灵活配置标签。
Java中运行Groovy,有三种比较常用的类支持:GroovyShell,GroovyClassLoader以及Java-Script引擎(JSR-223).

    1) GroovyShell: 通常用来运行"script片段"或者一些零散的表达式(Expression)(每次会产生实例,大量执行会有oom问题)

    2) GroovyClassLoader: 如果脚本是一个完整的文件,特别是有API类型的时候,比如有类似于JAVA的接口,面向对象设计时,通常使用GroovyClassLoader.(适用于复杂场景使用)

    3) ScriptEngine: JSR-223应该是推荐的一种使用策略.规范化,而且简便.(自动缓存实例,性能有优势)

    官方参考文档:http://docs.groovy-lang.org/latest/html/documentation/guide-integrating.html

一.GroovyShell代码样例

    1) 简单的表达式执行,方法调用

Java代码 

 收藏代码

  1. /** 
  2.  * 简答脚本执行 
  3.  * @throws Exception 
  4.  */  
  5. public static void evalScriptText() throws Exception{  
  6.     //groovy.lang.Binding  
  7.     Binding binding = new Binding();  
  8.     GroovyShell shell = new GroovyShell(binding);  
  9.       
  10.     binding.setVariable("name", "zhangsan");  
  11.     shell.evaluate("println 'Hello World! I am ' + name;");  
  12.     //在script中,声明变量,不能使用def,否则scrope不一致.  
  13.     shell.evaluate("date = new Date();");  
  14.     Date date = (Date)binding.getVariable("date");  
  15.     System.out.println("Date:" + date.getTime());  
  16.     //以返回值的方式,获取script内部变量值,或者执行结果  
  17.     //一个shell实例中,所有变量值,将会在此"session"中传递下去."date"可以在此后的script中获取  
  18.     Long time = (Long)shell.evaluate("def time = date.getTime(); return time;");  
  19.     System.out.println("Time:" + time);  
  20.     binding.setVariable("list", new String[]{"A","B","C"});  
  21.     //invoke method  
  22.     String joinString = (String)shell.evaluate("def call(){return list.join(' - ')};call();");  
  23.     System.out.println("Array join:" + joinString);  
  24.     shell = null;  
  25.     binding = null;  
  26. }  

    GroovyShell是一种性能较低的方式,因为每次都需要创建shell和script,这也意味着每次都需要对expression进行“编译”(JAVA Class)。

    2)  伪main方法执行.

Java代码 

 收藏代码

  1. /** 
  2.  * 当groovy脚本,为完整类结构时,可以通过执行main方法并传递参数的方式,启动脚本. 
  3.  */  
  4. public static void evalScriptAsMainMethod(){  
  5.     String[] args = new String[]{"Zhangsan","10"};//main(String[] args)  
  6.     Binding binding = new Binding(args);  
  7.     GroovyShell shell = new GroovyShell(binding);  
  8.     shell.evaluate("static void main(String[] args){ if(args.length != 2) return;println('Hello,I am ' + args[0] + ',age ' + args[1])}");  
  9.     shell = null;  
  10.     binding = null;  
  11. }  

    3)  通过Shell运行具有类结构的Groovy脚本

Java代码 

 收藏代码

  1. /** 
  2.  * 运行完整脚本 
  3.  * @throws Exception 
  4.  */  
  5. public static void evalScriptTextFull() throws Exception{  
  6.     StringBuffer buffer = new StringBuffer();  
  7.     //define API  
  8.     buffer.append("class User{")  
  9.             .append("String name;Integer age;")  
  10.             //.append("User(String name,Integer age){this.name = name;this.age = age};")  
  11.             .append("String sayHello(){return 'Hello,I am ' + name + ',age ' + age;}}\n");  
  12.     //Usage  
  13.     buffer.append("def user = new User(name:'zhangsan',age:1);")  
  14.             .append("user.sayHello();");  
  15.     //groovy.lang.Binding  
  16.     Binding binding = new Binding();  
  17.     GroovyShell shell = new GroovyShell(binding);  
  18.     String message = (String)shell.evaluate(buffer.toString());  
  19.     System.out.println(message);  
  20.     //重写main方法,默认执行  
  21.     String mainMethod = "static void main(String[] args){def user = new User(name:'lisi',age:12);print(user.sayHello());}";  
  22.     shell.evaluate(mainMethod);  
  23.     shell = null;  
  24. }  

    4)  方法执行和分部调用

Java代码 

 收藏代码

  1. /** 
  2.  * 以面向"过程"的方式运行脚本 
  3.  * @throws Exception 
  4.  */  
  5. public static void evalScript() throws Exception{  
  6.     Binding binding = new Binding();  
  7.     GroovyShell shell = new GroovyShell(binding);  
  8.     //直接方法调用  
  9.     //shell.parse(new File(//))  
  10.     Script script = shell.parse("def join(String[] list) {return list.join('--');}");  
  11.     String joinString = (String)script.invokeMethod("join", new String[]{"A1","B2","C3"});  
  12.     System.out.println(joinString);  
  13.     脚本可以为任何格式,可以为main方法,也可以为普通方法  
  14.     //1) def call(){...};call();  
  15.     //2) call(){...};  
  16.     script = shell.parse("static void main(String[] args){i = i * 2;}");  
  17.     script.setProperty("i", new Integer(10));  
  18.     script.run();//运行,  
  19.     System.out.println(script.getProperty("i"));  
  20.     //the same as  
  21.     System.out.println(script.getBinding().getVariable("i"));  
  22.     script = null;  
  23.     shell = null;  
  24. }  

二. GroovyClassLoader代码示例

    1) 解析groovy文件

Java代码 

 收藏代码

  1. /** 
  2.  * from source file of *.groovy 
  3.  */  
  4. public static void parse() throws Exception{  
  5.     GroovyClassLoader classLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader());  
  6.     File sourceFile = new File("D:\\TestGroovy.groovy");//文本内容的源代码  
  7.     Class testGroovyClass = classLoader.parseClass(new GroovyCodeSource(sourceFile));  
  8.     GroovyObject instance = (GroovyObject)testGroovyClass.newInstance();//proxy  
  9.     Long time = (Long)instance.invokeMethod("getTime", new Date());  
  10.     System.out.println(time);  
  11.     Date date = (Date)instance.invokeMethod("getDate", time);  
  12.     System.out.println(date.getTime());  
  13.     //here  
  14.     instance = null;  
  15.     testGroovyClass = null;  
  16. }  

    2) 如何加载已经编译的groovy文件(.class)

Java代码 

 收藏代码

  1. public static void load() throws Exception {  
  2.     GroovyClassLoader classLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader());  
  3.     BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\TestGroovy.class"));  
  4.     ByteArrayOutputStream bos = new ByteArrayOutputStream();  
  5.     for(;;){  
  6.         int i = bis.read();  
  7.         if( i == -1){  
  8.             break;  
  9.         }  
  10.         bos.write(i);  
  11.     }  
  12.     Class testGroovyClass = classLoader.defineClass(null, bos.toByteArray());  
  13.     //instance of proxy-class  
  14.     //if interface API is in the classpath,you can do such as:  
  15.     //MyObject instance = (MyObject)testGroovyClass.newInstance()  
  16.     GroovyObject instance = (GroovyObject)testGroovyClass.newInstance();  
  17.     Long time = (Long)instance.invokeMethod("getTime", new Date());  
  18.     System.out.println(time);  
  19.     Date date = (Date)instance.invokeMethod("getDate", time);  
  20.     System.out.println(date.getTime());  
  21.       
  22.     //here  
  23. bis.close();  
  24.     bos.close();  
  25.     instance = null;  
  26.     testGroovyClass = null;  
  27. }  

三. ScriptEngine

    1) pom.xml依赖

Xml代码 

 收藏代码

  1. <dependency>  
  2.     <groupId>org.codehaus.groovy</groupId>  
  3.     <artifactId>groovy</artifactId>  
  4.     <version>2.1.6</version>  
  5. </dependency>  
  6. <dependency>  
  7.     <groupId>org.codehaus.groovy</groupId>  
  8.     <artifactId>groovy-jsr223</artifactId>  
  9.     <version>2.1.6</version>  
  10. </dependency>  

    2) 代码样例

Java代码 

 收藏代码

  1. public static void evalScript() throws Exception{  
  2.     ScriptEngineManager factory = new ScriptEngineManager();  
  3.     //每次生成一个engine实例  
  4.     ScriptEngine engine = factory.getEngineByName("groovy");  
  5.     System.out.println(engine.toString());  
  6.     assert engine != null;  
  7.     //javax.script.Bindings  
  8.     Bindings binding = engine.createBindings();  
  9.     binding.put("date", new Date());  
  10.     //如果script文本来自文件,请首先获取文件内容  
  11.     engine.eval("def getTime(){return date.getTime();}",binding);  
  12.     engine.eval("def sayHello(name,age){return 'Hello,I am ' + name + ',age' + age;}");  
  13.     Long time = (Long)((Invocable)engine).invokeFunction("getTime", null);  
  14.     System.out.println(time);  
  15.     String message = (String)((Invocable)engine).invokeFunction("sayHello", "zhangsan",new Integer(12));  
  16.     System.out.println(message);  
  17. }  

    需要提醒的是,在groovy中,${expression} 将会被认为一个变量,如果需要输出"$"符号,需要转义为"\$".   

    这是一种性能较高的方式,engine我们可以声明为全局实例,是线程安全的。每次调用时只需要创建新的Binndings即可,此外如果脚本已经编译过(首次执行之后)其Class将会被缓存,则此后不需要再次编译。

 

Groovy代码文件与class文件的对应关系
而作为基于JVM的语言,Groovy可以非常容易的和Java进行互操作,但也需要编译成class文件后才能运行,所以了解Groovy代码文件和class文件的对应关系,有助于更好地理解Groovy的运行方式和结构。

对于没有任何类定义
如果Groovy脚本文件里只有执行代码,没有定义任何类(class),则编译器会生成一个Script的子类,类名和脚本文件的文件名一样,而脚本的代码会被包含在一个名为run的方法中,同时还会生成一个main方法,作为整个脚本的入口。

对于仅有一个类
如果Groovy脚本文件里仅含有一个类,而这个类的名字又和脚本文件的名字一致,这种情况下就和Java是一样的,即生成与所定义的类一致的class文件。

对于多个类
如果Groovy脚本文件含有多个类,groovy编译器会很乐意地为每个类生成一个对应的class文件。如果想直接执行这个脚本,则脚本里的第一个类必须有一个static的main方法。

groovy与java集成中经常出现的问题
使用GroovyShell的parse方法导致perm区爆满的问题
如果应用中内嵌Groovy引擎,会动态执行传入的表达式并返回执行结果,而Groovy每执行一次脚本,都会生成一个脚本对应的class对象,并new一个InnerLoader去加载这个对象,而InnerLoader和脚本对象都无法在gc的时候被回收运行一段时间后将perm占满,一直触发fullgc。

为什么Groovy每执行一次脚本,都会生成一个脚本对应的class对象?
一个ClassLoader对于同一个名字的类只能加载一次,都由GroovyClassLoader加载,那么当一个脚本里定义了C这个类之后,另外一个脚本再定义一个C类的话,GroovyClassLoader就无法加载了。为什么这里会每次执行都会加载?

这是因为对于同一个groovy脚本,groovy执行引擎都会不同的命名,且命名与时间戳有关系。当传入text时,class对象的命名规则为:"script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy"。这就导致就算groovy脚本未发生任何变化,每次执行parse方法都会新生成一个脚本对应的class对象,且由GroovyClassLoader进行加载,不断增大perm区。

为什么InnerLoader加载的对应无法通过gc清理掉?
大家都知道,JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载:1. 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;2. 加载该类的ClassLoader已经被GC;3. 该类的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。

在GroovyClassLoader代码中有一个class对象的缓存,进一步跟下去,发现每次编译脚本时都会在Map中缓存这个对象,即:setClassCacheEntry(clazz)。每次groovy编译脚本后,都会缓存该脚本的Class对象,下次编译该脚本时,会优先从缓存中读取,这样节省掉编译的时间。这个缓存的Map由GroovyClassLoader持有,key是脚本的类名,这就导致每个脚本对应的class对象都存在引用,无法被gc清理掉。

如何解决?
请参考:Groovy引发的PermGen区爆满问题定位与解决。

如需更深入的理解GroovyClassLoader体系,请参考下面这篇文章Groovy深入探索——Groovy的ClassLoader体系

使用GroovyClassLoader加载机制导致频繁gc问题
通常使用如下代码在Java 中执行 Groovy 脚本:

GroovyClassLoader groovyLoader = new GroovyClassLoader();
Class<Script> groovyClass = (Class<Script>) groovyLoader.parseClass(groovyScript);
Script groovyScript = groovyClass.newInstance();
每次执行groovyLoader.parseClass(groovyScript),Groovy 为了保证每次执行的都是新的脚本内容,会每次生成一个新名字的Class文件,这个点已经在前文中说明过。当对同一段脚本每次都执行这个方法时,会导致的现象就是装载的Class会越来越多,从而导致PermGen被用满。
同时这里也存在性能瓶颈问题,如果去分析这段代码会发现90%的耗时占用在Class

为了避免这一问题通常做法是缓存Script对象,从而避免以上2个问题。在这过程中通常又会引入新的问题:

高并发情况下,binding对象混乱导致计算出错
在高并发的情况下,在执行赋值binding对象后,真正执行run操作时,拿到的binding对象可能是其它线程赋值的对象,所以出现数据计算混乱的情况

长时间运行仍然出现oom,无法解决Class
这点在上文中已经提到,由于groovyClassLoader会缓存每次编译groovy脚本的Class对象,下次编译该脚本时,会优先从缓存中读取,这样节省掉编译的时间。导致被加载的Class对象因为存在引用而无法被卸载,虽然通过缓存避免了短时间内大量生成新的class对象,但如果长时间运营仍然会存在问题。

比较好的做法是:

每个 script 都 new 一个 GroovyClassLoader 来装载;
对于 parseClass 后生成的 Class 对象进行cache,key 为 groovyScript 脚本的md5值。
CodeCache用满,导致JIT禁用问题
对于大量使用Groovy的应用,尤其是 Groovy 脚本还会经常更新的应用,由于这些Groovy脚本在执行了很多次后都会被JVM编译为 native 进行优化,会占据一些 CodeCache 空间,而如果这样的脚本很多的话,可能会导致 CodeCache 被用满,而 CodeCache 一旦被用满,JVM 的 Compiler 就会被禁用,那性能下降的就不是一点点了
 

 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值