TFdream / blog

个人技术博客,博文写在 Issues 里。
Apache License 2.0
129 stars 18 forks source link

Groovy与Java集成 #405

Open TFdream opened 2 years ago

TFdream commented 2 years ago

groovy

Groovy是一门基于JVM的动态语言,同时也是一门面向对象的语言,语法上和Java非常相似。它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。

1.在Java项目中集成Groovy

添加依赖:

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

1.1 ScriptEngineManager

按照JSR223,使用标准接口ScriptEngineManager调用。

import javax.script.*;
import java.util.Date;

    public void scriptEngine() throws ScriptException, NoSuchMethodException {
        ScriptEngineManager factory = new ScriptEngineManager();
        // 每次生成一个engine实例
        ScriptEngine engine = factory.getEngineByName("groovy");
        Bindings binding = engine.createBindings();
        // 入参
        binding.put("date", new Date());
        // 如果script文本来自文件,请首先获取文件内容
        engine.eval("def getTime(){return date.getTime();}", binding);
        engine.eval("def sayHello(name,age){return 'Hello,I am ' + name + ',age' + age;}");
        // 反射到方法
        Long time = (Long) ((Invocable) engine).invokeFunction("getTime", null);
        System.out.println(time);
        String message = (String) ((Invocable) engine).invokeFunction("sayHello", "zhangsan", 12);
        System.out.println(message);
    }

1.2 GroovyShell

Groovy官方提供GroovyShell,执行Groovy脚本片段,GroovyShell每一次执行时代码时会动态将代码编译成Java Class,然后生成Java对象在Java虚拟机上执行,所以如果使用GroovyShell会造成Class太多,性能较差。

final String script = "Runtime.getRuntime().availableProcessors()";
Binding intBinding = new Binding();
GroovyShell shell = new GroovyShell(intBinding);
final Object eval = shell.evaluate(script);
System.out.println(eval);

1.3 GroovyClassLoader

Groovy官方提供GroovyClassLoader类,支持从文件、url或字符串中加载解析Groovy Class,实例化对象,反射调用指定方法。

GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
  String helloScript = "package io.github.groovy.util" +  // 可以是纯Java代码
          "class Hello {" +
            "String say(String name) {" +
              "System.out.println(\"hello, \" + name)" +
              " return name;"
            "}" +
          "}";
Class helloClass = groovyClassLoader.parseClass(helloScript);
GroovyObject object = (GroovyObject) helloClass.newInstance();
Object ret = object.invokeMethod("say", "github"); // 控制台输出"hello, github"
System.out.println(ret.toString()); // 打印github

2.性能优化

当JVM中运行的Groovy脚本存在大量并发时,如果按照默认的策略,每次运行都会重新编译脚本,调用类加载器进行类加载。不断重新编译脚本会增加JVM内存中的CodeCache和Metaspace,引发内存泄露,最后导致Metaspace内存溢出;类加载过程中存在同步,多线程进行类加载会造成大量线程阻塞,那么效率问题就显而易见了。

为了解决性能问题,最好的策略是对编译、加载后的Groovy脚本进行缓存,避免重复处理,可以通过计算脚本的MD5值来生成键值对进行缓存。

解决方案

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author fengbingbing
 */
public class GroovyScriptCache {
    private static final Map<String, Class<?>> CLASS_CACHE = new ConcurrentHashMap<>();

    /** singleton **/
    public static GroovyScriptCache getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public GroovyObject newInstance(String script) throws InstantiationException, IllegalAccessException {
        return newInstance(script, GroovyScriptCache.class.getClassLoader());
    }

    public GroovyObject newInstance(String script, ClassLoader parent) throws InstantiationException, IllegalAccessException {
        Class<?> groovyClass = CLASS_CACHE.get(script);
        if(groovyClass == null){
            GroovyClassLoader loader = new GroovyClassLoader(parent);
            Class<?> tmpClass = loader.parseClass(script);
            CLASS_CACHE.putIfAbsent(script, tmpClass);
            //重新获取
            groovyClass = CLASS_CACHE.get(script);
        }
        return (GroovyObject) groovyClass.newInstance();
    }

    private static class SingletonHolder {
        private static final GroovyScriptCache INSTANCE = new GroovyScriptCache();
    }
}

zebra示例:

/**
 *
 * @author danson.liu, Dozer
 */
public class GroovyRuleEngine implements RuleEngine {

    private final GroovyObject engineObj;

    private static final Map<String, Class<?>> RULE_CLASS_CACHE = new ConcurrentHashMap<String, Class<?>>();

    public GroovyRuleEngine(String rule) {
        try {
            engineObj = getGroovyObject(rule);
        } catch (Exception e) {
            throw new ZebraConfigException("Construct groovy rule engine failed, cause: ", e);
        }
    }

    @SuppressWarnings("resource")
    private static final GroovyObject getGroovyObject(String rule)
            throws IllegalAccessException, InstantiationException {
        if (!RULE_CLASS_CACHE.containsKey(rule)) {
            synchronized (GroovyRuleEngine.class) {
                if (!RULE_CLASS_CACHE.containsKey(rule)) {
                    Matcher matcher = DimensionRule.RULE_COLUMN_PATTERN.matcher(rule);
                    StringBuilder engineClazzImpl = new StringBuilder(200)
                            .append("class RuleEngineBaseImpl extends " + RuleEngineBase.class.getName() + "{")
                            .append("Object execute(Map context) {").append(matcher.replaceAll("context.get(\"$1\")"))
                            .append("}").append("}");
                    GroovyClassLoader loader = new GroovyClassLoader(AbstractDimensionRule.class.getClassLoader());
                    Class<?> engineClazz = loader.parseClass(engineClazzImpl.toString());
                    RULE_CLASS_CACHE.put(rule, engineClazz);
                }
            }
        }
        return (GroovyObject) RULE_CLASS_CACHE.get(rule).newInstance();
    }

    @Override
    public Object eval(Map<String, Object> valMap) {
        return engineObj.invokeMethod("execute", valMap);
    }
}
public class DefaultDimensionRule extends AbstractDimensionRule {

    private static final String shardByMonthPrefix = "shardByMonth";

    private static final String shardByLongPrefix = "shardByLong";

    private RuleEngine dbRuleEngine;

    private RuleEngine tableRuleEngine;

    private List<DimensionRule> whiteListRules;

    private TableSetsManager tablesMappingManager;

    private Map<String, Set<String>> allDBAndTables = new HashMap<String, Set<String>>();

    public DefaultDimensionRule(TableShardDimensionConfig dimensionConfig) {
        this.isMaster = dimensionConfig.isMaster();
        this.needSync = dimensionConfig.isNeedSync();

        this.tablesMappingManager = new DefaultTableSetsManager(dimensionConfig.getTableName(),
                dimensionConfig.getDbIndexes(), dimensionConfig.getTbSuffix(), dimensionConfig.isTbSuffixZeroPadding());
        this.allDBAndTables.putAll(this.tablesMappingManager.getAllTableSets());

        // for new unified rule and range
        String dbRuleStr = dimensionConfig.getDbRule();
        String tbRuleStr = dimensionConfig.getTbRule();
        if(dbRuleStr != null && (dbRuleStr.contains(shardByMonthPrefix) || dbRuleStr.contains(shardByLongPrefix))) {
            this.isRange = true;
        }
        this.dbRuleEngine = new GroovyRuleEngine(dbRuleStr);
        if(tbRuleStr != null && tbRuleStr.length() > 0) {
            this.tableRuleEngine = new GroovyRuleEngine(dimensionConfig.getTbRule());
        }

        this.initShardColumn(dimensionConfig.getDbRule(), dimensionConfig.getTbRule());
    }

}

参考资料