JAVA调用groovy脚本的方式

一、使用
用 Groovy 的 GroovyClassLoader ,它会动态地加载一个脚本并执行它。GroovyClassLoader是一个Groovy定制的类装载器,负责解析加载Java类中用到的Groovy类。
先创建一个groovy脚本,非常简单,定义一个用于计算的方法,groovy脚本如下:

def cal(int a, int b){
return a+b
}
在java用调用,通过GroovyClassLoader动态加载groovy脚本,然后执行计算:

GroovyClassLoader classLoader = new GroovyClassLoader();
Class groovyClass = classLoader.parseClass("def cal(int a, int b){\n" +
" return a+b\n" +
"}");
try {
Object[] param = { 8,7 };
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
int result = (int)groovyObject.invokeMethod("cal",param);
System.out.println(result);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
结果如下:

15
1
这是最简单的java调用groovy脚本的栗子。

二、实现原理
GroovyClassLoader是一个定制的类装载器,在代码执行时动态加载groovy脚本为java对象。大家都知道classloader的双亲委派,我们先来分析一下这个GroovyClassloader,看看它的祖先分别是啥:

def cl = this.class.classLoader
while (cl) {
println cl
cl = cl.parent
}

输出:

groovy.lang.GroovyClassLoader$InnerLoader@42607a4f
groovy.lang.GroovyClassLoader@42e99e4a
sun.misc.Launcher$AppClassLoader@58644d46
sun.misc.Launcher$ExtClassLoader@62150f9e
从而得出Groovy的ClassLoader体系:

Bootstrap ClassLoader

sun.misc.Launcher.ExtClassLoader // 即Extension ClassLoader

sun.misc.Launcher.AppClassLoader // 即System ClassLoader

org.codehaus.groovy.tools.RootLoader // 以下为User Custom ClassLoader

groovy.lang.GroovyClassLoader

groovy.lang.GroovyClassLoader.InnerLoader

三、调用groovy脚本实现方式
1.使用GroovyClassLoader
private static void invoke(String scriptText, String function, Object... objects) throws Exception {
GroovyClassLoader classLoader = new GroovyClassLoader();
Class groovyClass = classLoader.parseClass(scriptText);
try {
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
groovyObject.invokeMethod(function,objects);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
2.使用ScriptEngine
private static final GroovyScriptEngineFactory scriptEngineFactory = new GroovyScriptEngineFactory();

private static <T> T invoke(String script, String function, Object... objects) throws Exception {
ScriptEngine scriptEngine = scriptEngineFactory.getScriptEngine();
scriptEngine.eval(script);
return (T) ((Invocable) scriptEngine).invokeFunction(function, objects);
}
3.使用GroovyShell
private static GroovyShell groovyShell = new GroovyShell();

private static <T> T invoke(String scriptText, String function, Object... objects) throws Exception {
Script script= groovyShell.parse(scriptText);
return (T) InvokerHelper.invokeMethod(script, function, objects);
}
四、性能优化
项目在测试时发现,加载的类随着程序运行越来越多,而且垃圾收集也非常频繁。

回过头来看看,groovy脚本执行的过程:

GroovyClassLoader classLoader = new GroovyClassLoader();
Class groovyClass = classLoader.parseClass(scriptText);
try {
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
groovyObject.invokeMethod(function,objects);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

查看GroovyClassLoader.parseClass方法,发现如下代码:

public Class parseClass(String text) throws CompilationFailedException {
return parseClass(text, "script" + System.currentTimeMillis() +
Math.abs(text.hashCode()) + ".groovy");
}

protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) {
InnerLoader loader = AccessController.doPrivileged(new PrivilegedAction<InnerLoader>() {
public InnerLoader run() {
return new InnerLoader(GroovyClassLoader.this);
}
});
return new ClassCollector(loader, unit, su);
}

这两处代码的意思是:
groovy每执行一次脚本,都会生成一个脚本的class对象,这个class对象的名字由 “script” + System.currentTimeMillis() +
Math.abs(text.hashCode()组成,对于问题1:每次订单执行同一个StrategyLogicUnit时,产生的class都不同,每次执行规则脚本都会产品一个新的class。
接着看问题2InnerLoader部分:
groovy每执行一次脚本都会new一个InnerLoader去加载这个对象,而对于问题2,我们可以推测:InnerLoader和脚本对象都无法在fullGC的时候被回收,因此运行一段时间后将PERM占满,一直触发fullGC。

五、解决方案
把每次脚本生成的对象缓存起来,用md5算法生成脚本的md5作为key,缓存groovyClass 对象。调整之后的方式:

private static GroovyShell groovyShell = new GroovyShell();

private static Map<String, Script> scriptCache = new ConcurrentHashMap<>();

private static <T> T invoke(String scriptText, String function, Object... objects) throws Exception {
Script script;
String cacheKey = DigestUtils.md5Hex(scriptText);

if (scriptCache.containsKey(cacheKey)) {
script = scriptCache.get(cacheKey);
} else {
script = groovyShell.parse(scriptText);
scriptCache.put(cacheKey, script);
}

return (T) InvokerHelper.invokeMethod(script, function, objects);
}

 

Java执行Groovy脚本方式

一,导入相关maven

<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.21</version>
</dependency>

二,Java调用Groovy的三种方式

public class GroovyTest<T> {

    // 方式一,使用GroovyClassLoader调用
    @Test
    public <T> T invoke01(String scriptText, String func, Object... objs) throws IllegalAccessException, InstantiationException {
        GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
        Class groovyClazz = groovyClassLoader.parseClass(scriptText);

        GroovyObject groovyObject = (GroovyObject) groovyClazz.newInstance();
        Object result = groovyObject.invokeMethod(func, objs);
        return (T) result;
    }

    // 方式二,使用ScriptEngine调用
    @Test
    public <T> T invoke02(String scriptText, String func, Object... objs) throws ScriptException, NoSuchMethodException {
        ScriptEngine scriptEngine = new GroovyScriptEngineFactory().getScriptEngine();
        scriptEngine.eval(scriptText);
        Object result = ((Invocable) scriptEngine).invokeFunction(func, objs);
        return (T) result;
    }

    // 方式三,使用GroovyShell调用(推荐)
    public <T> T invoke03(String scriptText, String func, Object... objs){
        GroovyShell groovyShell = new GroovyShell();
        Script script = groovyShell.parse(scriptText);
        Object result = InvokerHelper.invokeMethod(script, func, objs);
        return (T) result;
    }
}

三,Groovy工具类

groovy每执行一次脚本都会new一个InnerLoader去加载这个对象,而对于问题2,我们可以推测:InnerLoader和脚本对象都无法在fullGC的时候被回收,因此运行一段时间后将PERM占满,一直触发fullGC。
优化方案:把每次脚本生成的对象缓存起来,用md5算法生成脚本的md5作为key,缓存groovyClass 对象。

public class GroovyUtils {
    private static GroovyShell groovyShell = new GroovyShell();

    private static Map<String, Script> scriptCache = new ConcurrentHashMap<>();

    private static <T> T invoke(String scriptText, String function, Object... objects) throws Exception {
        Script script;
        String cacheKey = DigestUtils.md5Hex(scriptText);

        if (scriptCache.containsKey(cacheKey)) {
            script = scriptCache.get(cacheKey);
        } else {
            script = groovyShell.parse(scriptText);
            scriptCache.put(cacheKey, script);
        }
        
        return (T) InvokerHelper.invokeMethod(script, function, objects);
    }
}

四,Groovy工具类(2)

/**
 * Groovy脚本执行类
 */
public class GroovyScriptEngineUtil {

    private static final Logger logger = LoggerFactory.getLogger(GroovyScriptEngineUtil.class);
    private static final String EngineName = "groovy";
    private static ScriptEngine engine = null;
    static{
        ScriptEngineManager factory = new ScriptEngineManager();
        engine = factory.getEngineByName(EngineName);
    }


    public static Object runGroovyScript(String script, Map<String, Object> params) {
        try {
            Bindings bindings = engine.createBindings();
            bindings.putAll(params);
            Map<String,Object> contextParams = new HashMap<>();
            contextParams.putAll(params);
            bindings.put("contextParams",contextParams);
            return engine.eval(script,bindings);
        } catch (Exception e) {
            logger.error("script脚本执行异常,script:{},params:{}",script,params);
            logger.error("script脚本执行异常:",e);
            return null;
        }
    }
}
posted @ 2022-04-11 20:26  hanease  阅读(5920)  评论(0编辑  收藏  举报