Java中调用Groovy脚本

Groovy是构建在JVM上的一个轻量级动态语言,其是Java实现的,与Java语法类是,能很好的与Java代码结合,及扩展现有代码。

Java在语音动态性方面只能通过反射,且参数传递格式很严格不是很灵活,而Groovy是构建在JVM上的一个轻量级动态语言,其是Java实现的,与Java语法类是,能很好的与Java代码结合,及动态扩展现有代码。

Java中可以通过GroovyScriptEngineGroovyClassLoaderGroovyShellScriptEngineManager等方式调用Groovy,以及在实际项目中的运用。Maven依赖:

1
2
3
4
5
6
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy.version}</version>
<type>pom</type>
</dependency>

GroovyScriptEngine

从指定的位置(文件系统,URL,数据库等)加载Groovy脚本,并且随着脚本变化而重新加载。在相互关联的多个脚本情况下使用GroovyScriptEngine更好些。

1
2
3
4
5
6
7
GroovyScriptEngine engine = new GroovyScriptEngine("src/test/resources/groovy/");

Map<String, Object> param = new HashMap<>();
param.put("id", "KKKKKKKKKKKKKK");
param.put("aa", 45);
Binding binding = new Binding(param);
Object result = engine.run("Mixed.groovy", binding);

GroovyClassLoader

GroovyClassLoader是一个定制的类装载器,负责解释加载Java类中用到的Groovy类。

1
2
3
4
5
6
7
8
9
GroovyClassLoader loader = new GroovyClassLoader();
Class aClass = loader.parseClass(new File("src/test/resources/groovy/Mixed.groovy"));
try {
GroovyObject instance = (GroovyObject) aClass.newInstance();
Object result = instance.invokeMethod("Mixed", new Object[]{"KKKKKKKKKKKKKK", 45});
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}

GroovyShell

GroovyShell允许在Java类甚至Groovy类中求任意Groovy表达式的值。可用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。多用于推求独立的脚本或表达式。

即使使用GroovyShell也有多种实现方式,使用invokeMethod方法调用:

1
2
3
4
5
6
7
8
GroovyShell loader = new GroovyShell();
Script script = loader.parse(new File("src/test/resources/groovy/Mixed.groovy"));
try {
Object result = script.invokeMethod("Mixed", new Object[]{"KKKKKKKKKKKKKK", 45});
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}

通过GroovyShellevaluate方式直接调用脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
Map<String, Object> param = new HashMap<>();
param.put("id", "KKKKKKKKKKKKKK");
param.put("aa", 45);
param.put("bb", 55L);
param.put("cc", 9.9999);
Binding binding = new Binding(param);
GroovyShell loader = new GroovyShell(binding);
try {
Object result = loader.evaluate("return id + (aa + bb + cc)");
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}

通过InvokerHelper类来调用:

1
2
3
4
5
6
7
8
9
10
11
12
Map<String, Object> param = new HashMap<>();
param.put("id", "KKKKKKKKKKKKKK");
param.put("aa", 45);
param.put("bb", 55L);
param.put("cc", 9.9999);
Binding binding = new Binding(param);

GroovyShell shell = new GroovyShell();
Script script = shell.parse(new File("src/test/resources/groovy/Mixed.groovy"));

Object result = InvokerHelper.createScript(script.getClass(), binding).run();
System.out.println(result);

还可以通过GroovyShell来直接parse脚本内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map<String, Object> param = new HashMap<>();
param.put("id", "KKKKKKKKKKKKKK");
param.put("aa", 45);
param.put("bb", 55L);
param.put("cc", 9.9999);
Binding binding = new Binding(param);

GroovyShell shell = new GroovyShell();
Script script = shell.parse("def Mixed(String id, int aa, Long bb, double cc) {\n" +
" return id + (aa + bb + cc)\n" +
"}\n" +
"Mixed(id, aa, bb, cc)");

Object result = InvokerHelper.createScript(script.getClass(), binding).run();
System.out.println(result);

ScriptEngineManager

1
2
3
4
5
6
7
8
9
10
11
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");

Bindings binding = engine.createBindings();
binding.put("id", "KKKKKKKKKKKKKK");
binding.put("aa", 45);
binding.put("bb", 55L);
binding.put("cc", 9.9999);

Object result = engine.eval("return id + (aa + bb + cc)", binding);
System.out.println(result);

集成常见问题

使用GroovyShell的parse方法导致perm区爆满的问题

若应用中内嵌Groovy引擎,会动态执行传入的表达式并返回执行结果,而Groovy每执行一次脚本,都会生成一个脚本对应的class对象,并new一个InnerLoader去加载这个对象,而InnerLoader和脚本对象都无法在gc的时候被回收运行一段时间后将perm占满,一直触发fullgc

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

JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载:

  • 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;

  • 加载该类的ClassLoader已经被GC

  • 该类的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。

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