Groovy脚本极限优化

 前段时间开发的项目,项目需求要求支持业务人员频繁业务需求变更,业务要求每次策略变更第一时间线上生效。结合项目业务需要,我们选择进行业务领域抽象,把业务变更的需求提炼成为脚本操作,每次业务人员对业务的操作变成为业务域的逻辑操作,针对业务流程上的不同需求变更就变成一条条脚本规则的动态变更。

    因为团队主要开发语言是java,我们调研了QL Express 和 Groovy等脚本,最终选定Groovy脚本作为我们的脚本语言。我们使用Groovy支持业务人员频繁需求变更方案,首先对相关需求抽象出业务域,业务需求开发变成Groovy脚本,开发获取(转换)业务域数据接口。每次业务人员需求变更,我们修改业务脚本,线上获取到脚本变化,解析脚本语法树分析脚本依赖业务域,通过对应的业务域数据接口获取数据,然后加载数据执行对应脚本得到结果。

    本文主要关注对Java调用Groovy脚本所做的优化,本文的优化重点并不是对Groovy脚本执行性能的极致优化,就像我们调研选取Groovy脚本支持我们的业务需求综合性能和易用性综合考量的结果。

Groovy调用优化


下面说的所有关于Groovy优化都是基于GroovyShell执行Groovy脚本的极限优化

1.因为我们的业务流程涉及大量脚本调用,Groovy作为脚本语言,每次Java调用业务变更需求的Groovy脚本,Groovy都要经过重新编译生成Class,并new一个ClassLoader去加载一个对象,导致每次调用Groovy脚本执行时间大部分花在脚本编译上,而且也会导致大量的编译脚本Class对账,运行一段时间后将perm暴涨。

2.高并发情况下,执行赋值binding对象后,真正执行run操作时,拿到的Binding对象可能是其它线程赋值的对象,会出现执行脚本结果混乱的情况。

针对以上存在的问题,我对Groovy脚本调用进行了优化解决以上问题。

1.首先我们通过给每个脚本生成一个md5,每次脚本首次执行,我们会把Groovy脚本生成的Script对象进行缓存,缓存设置一定的过期时间,保证下次同一个脚本执行直接调用Script就行。

2. 我们对每次Script执行通过锁保证每次执行的Binding不会出现多线程混乱的情况。

以上优化对应的代码如下:

public class GroovyUtil {
 
    private static GroovyShell groovyShell;
 
    static {
        groovyShell = new GroovyShell();
    }
    
    
    public static Object execute(String ruleScript, Map<String, Object> varMap) {
 
        String scriptMd5 = null;
        try {
            scriptMd5 = Md5Util.encryptForHex(ruleScript);
        } catch (Exception e) {
 
        }
        Script script;
        if (scriptMd5 == null) {
            script = groovyShell.parse(ruleScript);
        } else {
            String finalScriptMd5 = scriptMd5;
            script = GroovyCache.getValue(GroovyCache.GROOVY_SHELL_KEY_PREFIX + scriptMd5,
                    () -> Optional.ofNullable(groovyShell.parse(ruleScript, generateScriptName(finalScriptMd5))),
                    new TypeReference<Script>() {
                    });
            if (script == null) {
                script = groovyShell.parse(ruleScript, generateScriptName(finalScriptMd5));
            }
        }
 
        // 此处锁住script,为了防止多线程并发执行Binding数据混乱
        synchronized(script) {
 
            Binding binding = new Binding(varMap);
            script.setBinding(binding);
            return script.run();
        }
    }
    
    private static String generateScriptName(String scriptName) {
        return "Script" + scriptName + ".groovy";
    }
 
}

缓存类:

// 缓存类
public class GroovyCache {
 
    private static Cache<String, Optional<Object>> localMemoryCache =
            CacheBuilder.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).build();
 
    private static FLogger LOGGER = FLoggerFactory.getLogger(GroovyCache.class);
 
    public static String GROOVY_SHELL_KEY_PREFIX = "GROOVY_SHELL#";
 
    public static <T> T getValue(String key, Callable<Optional<Object>> load, TypeReference<T> typeReference) {
 
        try {
            Optional<Object> value = localMemoryCache.get(key, load);
            if (value.isPresent()) {
                return (T) value.get();
            }
            return null;
        } catch (Exception ex) {
            LOGGER.error("获取缓存异常,key:{} ", key, ex);
        }
        return null;
    }
 
}

MD5Util

public class MD5Util {

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};

    /**
     * 将1个字节(1 byte = 8 bit)转为 2个十六进制位
     * 1个16进制位 = 4个二进制位 (即4 bit)
     * 转换思路:最简单的办法就是先将byte转为10进制的int类型,然后将十进制数转十六进制
     */
    private static String byteToHexString(byte b) {
        // byte类型赋值给int变量时,java会自动将byte类型转int类型,从低位类型到高位类型自动转换
        int n = b;

        // 将十进制数转十六进制
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;

        // d1和d2通过访问数组变量的方式转成16进制字符串;比如 d1 为12 ,那么就转为"c"; 因为int类型不会有a,b,c,d,e,f等表示16进制的字符
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 将字节数组里每个字节转成2个16进制位的字符串后拼接起来
     */
    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++){
            resultSb.append(byteToHexString(b[i]));
        }
        return resultSb.toString();
    }

    /**
     * MD5算法,统一返回大写形式的摘要结果,默认固定长度是 128bit 即 32个16进制位
     * String origin :需要进行MD5计算的字符串
     * String charsetname :MD5算法的编码
     */
    public static String MD5_32(String origin, String charsetname) {
        String resultString = null;
        try {
            // 1,创建MessageDigest对象
            MessageDigest md = MessageDigest.getInstance("MD5");
            // 2,向MessageDigest传送要计算的数据;传入的数据需要转化为指定编码的字节数组
            md.update(origin.getBytes( charsetname ));
            // 3,计算摘要
            byte[] bytesResult = md.digest();

            // 第2步和第3步可以合并成下面一步
            // byte[] bytesResult = md.digest(origin.getBytes(charsetname));

            // 4,将字节数组转换为16进制位
            resultString = byteArrayToHexString( bytesResult );
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 统一返回大写形式的字符串摘要
        return resultString.toUpperCase();

    }

    /**
     * 获取 16位的MD5摘要,就是截取32位结果的中间部分
     */
    public static String MD5_16(String origin, String charsetname) {
        return MD5_32(origin, charsetname).substring(8,24);
    }

    public static void main(String[] args){
        String origin = "1234567890";

        // 默认MD5计算得到128 bit的摘要,即32个 16进制位
        String result_32 = MD5_32(origin, "utf-8");
        System.out.println(result_32);  // E807F1FCF82D132F9BB018CA6738A19F

        // 默认MD5计算得到即16个 16进制位
        String result_16 = MD5_16(origin, "utf-8");
        System.out.println(result_16);  // F82D132F9BB018CA
    }

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值