Groovy特性

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

Groovy代码能够与Java代码很好地结合,也能用于扩展现有代码。相对于Java,它在编写代码的灵活性上有非常明显的提升。Groovy是动态编译语言,广泛用作脚本语言和快速原型语言,主要优势之一就是它的生产力。Groovy代码通常要比Java代码更容易编写,而且编写起来也更快,这使得它有足够的资格成为开发工作包中的一个附件。

Groovy与Java集成的方式

单纯实现Groovy脚本执行很简单,一般有三种方式:GroovyClassLoader、GroovyShell、GroovyScirptEngine。

GroovyClassLoader

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

GroovyClassLoader loader = new GroovyClassLoader();

Class groovyClass = loader.parseClass(new File(groovyFileName));

GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();

groovyObject.invokeMethod("run", "helloworld");

GroovyShell

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

// 脚本写在文件中,先解析成Script对象,再运行

GroovyShell shell = new GroovyShell();

Script groovyScript = shell.parse(new File(groovyFileName));

Object[] args = {};

groovyScript.invokeMethod("run", args);

// 直接运行脚本表达式

Binding bind = new Binding();

bind.setVariable("name", "zhangsan");

bind.setVariable("age", "25");

GroovyShell shell = new GroovyShell(bind);

Object obj = shell.evaluate("str = name + age; println str; return str");

System.out.println(obj);

// 脚本语句直接用字符串写完,先解析成Script对象,再运行

String scriptText = "println 'Script!'; return '222'";

Script script = shell.parse(scriptText);

Object res = script.run();

System.out.println(res);

GroovyScriptEngine

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

FunArgGroove.groovy文件

package com.alipay.cci

String printArg(String name){

System.out.println("参数:"+name);

return "返回结果:"+name;

}

//执行方法

printArg(arg);

GroovyScriptEngineApp.java文件

public class GroovyScriptEngineApp {

public static void main(String[] args) {

try {

// GroovyScriptEngine的根路径,如果参数是字符串数组,说明有多个根路径

GroovyScriptEngine engine = new GroovyScriptEngine("src/test/java/com/alipay/cci/");

Binding binding = new Binding();

// arg 和 参数同名

binding.setVariable("arg", "测试参数");

Object result = engine.run("FunArgGroove.groovy", binding);

System.out.println(result);

} catch (IOException e) {

e.printStackTrace();

} catch (ResourceException e) {

e.printStackTrace();

} catch (ScriptException e) {

e.printStackTrace();

}

}

}

现实中使用最多的集成方式是GroovyShell,原因在于使用脚本的场景更多的是想依赖其灵活动态的特性,不像Java逻辑一变就需要重新发布。而本身脚本的逻辑不会特别复杂,更多的是对传入的参数进行简单的计算看是否符合期望。

Groovy脚本与class文件的对应关系

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

Groovy脚本中没有任何类定义

直接说结论,如果Groovy脚本里只有执行代码,没有定义任何类(Class),则编译器会生成一个名Script的子类,类名是Script+数字(例如Script1、Script2、Script3),脚本代码会被包含在一个名为run的方法中,同时还会生成一个main方法,作为整个脚本的入口。

GroovyShell方式运行脚本示例。

@Test

public void testGroovyShellParse() throws Exception {

GroovyShell groovyShell = new GroovyShell();

// 要执行的脚本内容,是一行一行的代码,在shell.evaluate(script)的时候,就是逐行执行。

// 如果最后有return语句,就可以接收返回结果。

String scriptText = "println 'Script!'; return '222'";

// 下面这行parse是关键,根据字符串生成Script对象

// 会先将scriptText解析成Class,再反射生成其实例

// Class的名称是Script1\Script2这种形式

Script script = groovyShell.parse(scriptText);

Object res = script.run();

System.out.println(res);

}

以上代码中groovyShell.parse是关键,根据字符串生成Script对象,利用该对象执行脚本中的可执行代码。groovyShell.parse会调用到的关键方法如下。

public class GroovyShell extends GroovyObjectSupport {

//...省略

private GroovyClassLoader loader;

private int counter;

//...省略

// 根据字符串scriptText生成Script对象实例

public Script parse(String scriptText) throws CompilationFailedException {

return this.parse(scriptText, this.generateScriptName());

}

// 这里为脚本生成名称,Script1.groovy、Script2.groovy等

protected synchronized String generateScriptName() {

return "Script" + ++this.counter + ".groovy";

}

// 根据字符串scriptText解析出Script对象

public Script parse(final String scriptText, final String fileName) throws CompilationFailedException {

GroovyCodeSource gcs = (GroovyCodeSource)AccessController.doPrivileged(new PrivilegedAction() {

public GroovyCodeSource run() {

return new GroovyCodeSource(scriptText, fileName, "/groovy/shell");

}

});

return this.parse(gcs);

}

public Script parse(GroovyCodeSource codeSource) throws CompilationFailedException {

// 先解析成Class对象,再调用Class的newInstance()方法反射生成一个Script对象

return InvokerHelper.createScript(this.parseClass(codeSource), this.context);

}

// 利用GroovyClassLoader生成一个脚本对应的Class对象

private Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException {

//

return this.loader.parseClass(codeSource, false);

}

//...省略

}

一路Debug追踪到GroovyClassLoader的parseClass方法中去,查看根据脚本字符串scriptText生成的Class对象,如下图所示,可以发现生成的Class对象类名是Script1,GroovyShell类的counter变量会一直自增,后续生成的类名就会是Script2、Script3等。

Groovy脚本文件中没有任何类定义

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

例如以下FunArgGroove.groovy文件,文件中没有定义类,只有函数定义与执行代码。

package com.alipay.cci

String printArg(String name){

System.out.println("参数:"+name);

return "返回结果:"+name;

}

//执行方法

printArg(arg);

以下通过GroovyShell类解析FunArgGroove.groovy文件,并执行文件中的执行代码。

@Test

public void testGroovyShellParseFile() throws Exception {

// 脚本写在文件中,先解析成Class对象,再Script对象,再运行

GroovyShell groovyShell = new GroovyShell();

// 下面这行parse是关键,根据文件生成Script对象

Script groovyScript = groovyShell.parse(new File("src/test/java/com/alipay/cci/FunArgGroove.groovy"));

Object[] args = {"ZhangSan"};

Object result = groovyScript.invokeMethod("printArg", args);

System.out.println(result);

}

以上代码中groovyShell.parse是关键,根据文件生成Script对象,利用该对象可执行脚本文件中的可执行代码。groovyShell.parse会调用到的关键方法如下。

public class GroovyShell extends GroovyObjectSupport {

//...省略

private GroovyClassLoader loader;

//...省略

// 根据传入的文件生成Script实例

public Script parse(File file) throws CompilationFailedException, IOException {

return this.parse(new GroovyCodeSource(file, this.config.getSourceEncoding()));

}

// 先根据传入的文件生成Class对象实例,然后再利用class的newInstance()方法反射生成Script类型实例

public Script parse(GroovyCodeSource codeSource) throws CompilationFailedException {

return InvokerHelper.createScript(this.parseClass(codeSource), this.context);

}

// 根据传入的文件生成Class对象实例

private Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException {

return this.loader.parseClass(codeSource, false);

}

}

一路Debug追踪到GroovyClassLoader的parseClass方法中去,查看根据脚本文件生成的Class对象,如下图所示,可以发现生成的Class对象全路径类名和脚本文件的全路径名一样。

进入IDEA的target目录,查看FunArgGroove.groovy文件编译后的FunArgGroove.class文件详情如下,编译器生成了一个类名与脚本文件的文件名相同的类,这个类是Script的子类,脚本文件中定义的printArg函数在类中也被重新定义,同时还生成了一个main方法,作为整个脚本的入口。

Groovy脚本文件中定义了一个与文件名同名的类

直接说结论,如果Groovy脚本文件里仅含有一个类,而这个类的名字又和脚本文件的名字一致,这种情况下就和写了Java类是一样的,编译器会生成与所定义的类一致的class文件。

Groovy脚本文件中定义了多个类

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

运行时动态执行Groovy脚本常见的坑

使用GroovyClassLoader的parseClass方法导致元数据区OOM的问题

如果应用中内嵌Groovy引擎,会动态执行传入的表达式并返回执行结果,而Groovy每执行一次脚本,都会生成一个脚本对应的Class对象,并new一个InnerLoader(继承了GroovyClassLoader)去加载,而InnerLoader和脚本对象都无法在GC的时候被回收,运行一段时间后将metaspace占满,触发Full GC。

@Test

public void testGroovyClassLoader() throws Exception {

// 脚本表达式

String scriptText = "def sum(int a, int b) {println a + b; return a + b;}";

GroovyClassLoader groovyLoader = new GroovyClassLoader();

// 将传入的scriptText字符串解析成Class实例

Class