一、使用
用 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; } } }