Groovy使用小结

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();
        }
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值