Java中整合Groovy遇到的问题分析

一、背景

最近在做一个根据给定表达式动态解析得到结果的功能。
例如:给定表达式**“a>0”**,就可以根据给定参数a的值动态解析结果。
对比现在常见的开源规则表达式引擎Fel、Jeval、Jsel、Aviator、QLExpress、Groovy等之后,最终选定Groovy作为脚本引擎开发。

二、实现过程

(1)整合Groovy

基本上有三种途径:GroovyShell(以及Eval)、GroovyClassLoader和GroovyScriptEngine。
代码示例1:

GroovyClassLoader groovyLoader = new GroovyClassLoader();
Class<Script> groovyClass = (Class<Script>) groovyLoader.parseClass("a>0");
Script script = groovyClass.newInstance();
Binding bind = new Binding();
bind.setVariable("a", 1);
script.setBinding(bind);
script.run();

代码示例2:

Binding binging = new Binding();
bind.setVariable("a", 1);
GroovyShell shell = new GroovyShell(binging);
String express = "a>0" ;
Script script = shell.parse(express);
System.out.println(script.run());

代码示例3:

String express = "a>0" ;
GroovyShell shell = new GroovyShell();
Script script = shell.parse(express);
Binding binging = new Binding();
bind.setVariable("a", 1);
script.setBinding(binding);
System.out.println(script.run());

(2)发现问题

其实上面代码大致的思路都是: 为Groovy脚本代码包装生成class,然后产生该类实例对象,在具体执行其包装的逻辑代码。但是对于groovy脚本, 它默认会生成名字为script + System.currentTimeMillis() + Math.abs(text.hashCode())的class类, 也就是说传入脚本, 它都会生成一个新类, 就算同一段groovy脚本代码, 每调用一次, 都会生成一个新类
这就会存在一系列的问题:

问题1:耗时问题!!!
parse()方法是非常耗时的,如果每一次有表达式都需要解析将会非常的慢。根据自己测试的结果来看:shell.parse(express);耗时在200ms-300ms之间,但是script.run()耗时却几乎接近0ms。

问题2:Full GC问题!!!
当JVM中运行的Groovy脚本存在大量并发时,如果按照默认的策略,每次运行都会重新编译脚本,调用类加载器进行类加载。临时加载的类未能及时被释放,进而导致PermGen OutOfMemoryError;没那么严重的时候也会引发比较频繁的full GC从而影响稳定运行时的性能。

问题3:线程安全问题!!!
高并发情况下,执行赋值binding对象后,真正执行run操作时,拿到的Binding对象可能是其它线程赋值的对象,会出现执行脚本结果混乱的情况。

(3)问题解决

解决问题1:耗时问题
根据表达式expression执行完parse()后,使用Map缓存得到的Script对象。

解决问题2:Full GC问题
首先需要通过缓存来保证每个表达式只需要编译一次即可,
另外每次都通过 new GroovyClassLoader() 或者 new GroovyShell() 使用新的shell进行parse()

解决问题3:线程安全问题
(1)加锁
(2)每次生成新的Script对象

代码演示1:
这段代码主要是为了记录一下我的解决问题过程中的思路而已。
(1)同一个GroovyShell 进行编译;
(2)通过HashMap维护着对应关系的缓存,每次新的表达式解析的请求都会先去判断是否已经存在对应的Script对象。
(3)通过synchronized 实现线程同步

/**
 * @author Caocs
 * @date 2020/3/10
 */
public class RuleExecutor {
	private static final Object lock = new Object();
    private static final GroovyShell groovyShell = new GroovyShell();

    /**
     * 存放该规则组下的每个<表达式, Script>的对应映射关系
     */
    private Map<String, Script> expressionScriptCacheMap = new HashMap<>();
    
    /**
     * 只在第一次执行表达式expression时,实例化Script。
     * 然后多个线程同时操作scriptCache,需要保证线程安全。
     */
    private Script getScriptFromCache(String expression) {
        if (expressionScriptCacheMap.containsKey(expression)) {
            return expressionScriptCacheMap.get(expression);
        }
        synchronized (lock) {
            if (expressionScriptCacheMap.containsKey(expression)) {
                return expressionScriptCacheMap.get(expression);
            }
            // 同一个GroovyShell创建Script,容易发生full GC
            Script script = groovyShell.parse(expression);
            expressionScriptCacheMap.put(expression, script);
            return script;
        }
    }
    
	public Object ruleParse(String expression, Map<String, Object> paramMap) {
        Binding binding = new Binding(paramMap);
        Script script = getScriptFromCache(expression);
		// 通过加锁保证线程安全,但是当线程数过多时会变慢,线程阻塞
        synchronized (this){
            script.setBinding(binding);
            return script.run();
        }
    }
}

代码演示2:
(1)每一次都new GroovyClassLoader()进行编译;
我们不要采用一个全局的GroovyClassLoader,parseClass方法得到的Class

你可能感兴趣的:(Java中整合Groovy遇到的问题分析)