概述
Groovy作为一门脚本语言可兼容Java大部分的语法、具有动态性等特点被越来越多的项目所使用。在Java Web项目中我们通常将Groovy作为动态规则表达式。最近接触一个项目,允许使用者采用Groovy脚本编写个性化的数据加工的逻辑,然后系统调用对应的Groovy脚本完成数据加工的操作。针对Groovy脚本在项目中的使用,在此做个小结。
String script = "class GroovyScript{def execute(int a, int b){return a+b}}";
GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
Class<?> clazz = groovyClassLoader.parseClass(script);
我们使用GroovyClassLoader加载一个脚本时,通常如上代码所示。在代码的背后GroovyClassLoader都做了啥?
1. 首先我们创建一个GroovyClassLoader用于加载脚本。默认的GroovyClassLoader构造方法会调用GroovyClassLoader(ClassLoader loader)这个构造方法,同时指定Parent ClassLoader为Thread.currentThread().getContextClassLoader()。由ClassLoader加载特性我们可以知道,如果在Groovy脚本中调用我们项目中自定义的方法时,GroovyClassLoader需要通过其Parent ClassLoader进行加载对应的Java类。此外,我们在创建GroovyClassLoader时,我们可以指定脚本编译时的配置信息(如脚本编译后字节码保存的路径等参数)。
2. 在解析Groovy脚本时,都会创建一个新的InnerLoader对象加载编译后的字节码信息(如下代码所示)。我们会不禁问,我们已经创建了GroovyClassLoader为什么还要通过InnerLoader来加载脚本呢?主要原因是因为在不同的脚本中我们可能定义相同类名的类,如果采用GroovyClassLoader进行加载类,只能加载其中一个脚本的类信息,另一个脚本类信息无法加载。此外,Java垃圾回收机制中要回收持久代中无用的类信息时,前提是加载该类的ClassLoader被GC。因而使用新的InnerLoader加载时,只要没有其他类依赖它加载的类,则InnerLoader和它加载的类都可以被GC。
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类加载中所涉及到的2个ClassLoader。下面我们讨论下在使用GroovyClassLoader时,我们会遇到什么问题。
1.当我们采用parseClass()解析Groovy脚本时,同样的脚本调用该方法都会产生一个新类,如果我们对该脚本执行多次时会导致加载的Class越来越多,最终可能会导致Perm被占满,出现OOM。为避免这种情况的发生,我们可以对脚本内容与编译后的类信息做一个缓存。如下所示:
Map<String, Class<?>> codeClazzCache = new HashMap<String, Class<?>>();
/*其中String为Groovy脚本的md5值,Class<?>为脚本编译后的类信息*/
2.可能导致CodeCache被用满,在自定义函数使用较多的Groovy脚本中由于Groovy执行时不断的抛出MissMethodExceptionNoStack异常,导致cpu在handle_exception上被消耗(此部分内容还未研究,mark下)。
最后我们来看一个问题。如果我们在自定义的Groovy脚本中不小心写了个死循环,那么将会导致CPU负载飙高。那么我们如何在业务系统来避免这个问题呢。有个比较靠谱的解决方案是采用线程池执行Groovy脚本,同时设置线程执行脚本的超时时间。大体思路如下所示:
package groovy;
import groovy.lang.GroovyClassLoader;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
public class AvoidDeathLoop {
private static final Integer THREAD_NUM = 4; //线程数目
private static final Integer CAPACITY = 50; //任务队列容量
private static final Integer WAIT_TIME = 10; //线程超时等待时间
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(THREAD_NUM, THREAD_NUM, 0L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
/**
* 定义Groovy脚本处理任务
*/
class GroovyTask implements Callable<Object> {
private String script;
public GroovyTask(String script) {
this.script = script;
}
public Object call() throws Exception {
GroovyClassLoader loader = new GroovyClassLoader();
Class<?> clazz = loader.parseClass(script);
//若每个脚本中都存在一个无入参的execute方法,可以根据脚本格式自定义以下处理逻辑
Method method = clazz.getMethod("execute", new Class[] {});
return method.invoke(clazz.newInstance(), new Object[] {});
}
}
public Object parseScript(String script) throws Exception {
Future<Object> future = executor.submit(new GroovyTask(script));
try {
return future.get(WAIT_TIME, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("cancel the task");
future.cancel(true);
return null;
} finally {
/*
* 采用不推荐使用的stop方法线程终止该线程。 此处也可以利用Groovy AST抽象语法树,在Groovy脚本循环中加入中断检测,通过线程中断停止死循环运行
*/
Thread.currentThread().stop();
}
}
@Test
public void testDeathLoop() {
try {
String script = "class GroovyScript{" + "def execute(){while(true);}}";
parseScript(script);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
最后
以上就是时尚乌龟为你收集整理的Groovy使用小结的全部内容,希望文章能够帮你解决Groovy使用小结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复