Groovy脚本极限优化

本文分享了在项目中使用Groovy脚本进行业务逻辑处理的经验,并针对Groovy脚本的调用和解析进行了优化。介绍了如何利用GroovyShell和缓存技术减少脚本重复编译的时间消耗,同时确保多线程环境下数据一致性;并通过ClassCodeVisitorSupport分析脚本中的变量,提前获取所需业务数据。
摘要由CSDN通过智能技术生成

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

    因为团队主要开发语言是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;
    }

}

以上为本次对Groovy脚本执行性能和易用性综合取舍后的一些优化,其实,还有其它一些方面的优化,比如,Groovy脚本里面尽量都用Java静态类型,可能减少Groovy动态类型检查等。 具体关于Groovy 、 Java 性能对比优化的文章可以参见这篇  https://dzone.com/articles/groovy-20-performance-compared

Groovy解析优化

下面说说我们怎么对Groovy脚本进行解析优化,结合我们的业务需求,我们的业务Groovy脚本就是大段大段对业务域操作的脚本,我们需要对一大段脚本分析出来里面包含所有我们的业务域,然后,针对业务域,我们从对应接口获取业务数据,然后执行最新修改业务脚本执行我们对应操作。我以淘宝可能情况为例说明,比如,淘宝上线优惠活动,对应脚本如下:

def getMaxVipCoupon(def CUSTOMER, List COUPONS) {
            Boolean isVip = CUSTOMER.get('IS_VIP')
            if (isVip) {
                // 假设会员可以选取优惠券中最高的一张折扣
                def coupon = COUPONS.findAll { it.get('isValid') == 1 }.max { it.get('AMOUNT') }
                if (coupon.get('AMOUNT') > 0) {
                    OUT.errNo = 0
                    OUT.expanding.put("COUPON_AMOUNT", coupon.get('AMOUNT'))
                    OUT.expanding.put("COUPON_DESC", "会员最高优惠")
                }
            } else {
                OUT.errNo = 400
                OUT.expanding.put("COUPON_AMOUNT", 0)
                OUT.expanding.put("COUPON_DESC", "关注成为会员即可享受优惠")
            }
}

getMaxVipCoupon(CUSTOMER,COUPONS)

根据脚本的优惠信息,我们要获取至少三个外部业务域 CUSTOMER 、COUPONS和OUT ,然后根据业务域获取对应的数据,给用户选出满足条件的最大优惠信息。

针对以上需求,Groovy基础库内置强大的脚本语法分析相关辅助类,通过查看官方类库,我们看到 ClassCodeVisitorSupport 提供强大对Groovy脚本解析功能,我们通过集成ClassCodeVisitorSupport抽象类,自定义重写提供的方法,我们可以对Groovy高级定制分析。比如,针对业务脚本解析包含业务域需求,我们做了针对ClassCodeVisitorSupport类做了如下实现:

class GroovyShellVisitor extends ClassCodeVisitorSupport implements GroovyClassVisitor {

        private static List<String> EXCLUDE_IN_PARAM
                = ImmutableList.of("args", "context", "this", "super");

        private Map<String, Class> dynamicVariables = new HashMap<>();

        private Set<String> declarationVariables = new HashSet<>();

        /**
         * 记录Groovy解析过程的变量
         **/
        @Override
        public void visitVariableExpression(VariableExpression expression) {    //变量表达式分析
            super.visitVariableExpression(expression);
            if (EXCLUDE_IN_PARAM.stream().noneMatch(x -> x.equals(expression.getName()))) {

                if (!declarationVariables.contains(expression.getName())) {

                    if (expression.getAccessedVariable() instanceof DynamicVariable) { // 动态类型,变量类型都是Object
                        dynamicVariables.put(expression.getName(), expression.getOriginType().getTypeClass());
                    } else {
                        // 静态类型 Groovy支持静态类型
                        dynamicVariables.put(expression.getName(), expression.getOriginType().getTypeClass());
                    }
                }
            }
        }

        /**
         * 获取脚本内部声明的变量
         */
        @Override
        public void visitDeclarationExpression(DeclarationExpression expression) {
            // 保存脚本内部定义变量
            declarationVariables.add(expression.getVariableExpression().getName());
            super.visitDeclarationExpression(expression);
        }

        /**
         * 忽略对语法树闭包的访问
         */
        @Override
        public void visitClosureExpression(ClosureExpression expression) {
            // ignore 
        }

        public Set<String> getDynamicVariables() {
            return dynamicVariables.keySet();
        }

        public Map<String, Class> getDynamicVarAndClass() {
            return dynamicVariables;
        }

        @Override
        protected SourceUnit getSourceUnit() {
            return null;
        }
    }

其实个人从开始选型脚本调研,简单翻了了下源码的一些设计和看了几个使用Groovy的例子,很快就确定下来Groovy做为脚本能满足项目需求,佩服Groovy官方类库提供的强大支持和Groovy跟Java深度结合的易用性,据说某大厂的风控系统就是基于Groovy一点点写出来。

然后,具体获取脚本对应的业务域和业务域对应的类型实现如下:

/**
     *  从缓存获取脚本的变量和变量类型 (Groovy2.0 支持Java强类型定义)
     */
    public static Map<String, Class> getCacheBoundVarAndClassMap(final String scriptText, ClassLoader parent) {
        String scriptMd5 = null;
        try {
            scriptMd5 = Md5Util.encryptForHex(scriptText);
        } catch (Exception e) {

        }
        GroovyShellVisitor visitor;
        Map<String, Class> dynamicVariableAndClass;
        if (scriptMd5 == null) {
            visitor = analyzeScriptVariables(scriptText, parent);
            dynamicVariableAndClass = visitor.getDynamicVarAndClass();
        } else {
            dynamicVariableAndClass = GroovyCache.getValue(GroovyCache.SCRIPT_SHELL_KEY_PREFIX + scriptMd5,
                    () -> Optional.ofNullable(analyzeScriptVariables(scriptText, parent).getDynamicVarAndClass()),
                    new TypeReference<Map<String, Class>>() {
                    });

            if (dynamicVariableAndClass == null) {
                visitor = analyzeScriptVariables(scriptText, parent);
                dynamicVariableAndClass = visitor.getDynamicVarAndClass();
            }
        }
        return dynamicVariableAndClass;
    }

    /**
     * 获取脚本的外部参数类型
     */
    public static Set<String> getBoundVars(final String scriptText, ClassLoader parent) {
        GroovyShellVisitor visitor = analyzeScriptVariables(scriptText, parent);
        return visitor.getDynamicVariables();
    }

    /**
     *  解析脚本得到Visitor 
     */
    private static GroovyShellVisitor analyzeScriptVariables(String scriptText, ClassLoader parent) {
        assert scriptText != null;

        GroovyClassVisitor visitor = new GroovyShellVisitor();

        ScriptVariableAnalyzer.VisitorClassLoader myCL = new ScriptVariableAnalyzer.VisitorClassLoader(visitor, parent);
        // simply by parsing the script with our classloader
        // our visitor will be called and will visit all the variables
        myCL.parseClass(scriptText);
        return (GroovyShellVisitor) visitor;
    }

类似上面对Groovy脚本调用缓存优化,我们对调用getCacheBoundVarAndClassMap方法同样通过缓存优化,我们可以高效获取脚本的包含业务域和对应业务域的类型。

以上是对自己使用Groovy脚本在调用和解析层面做的些许优化,还有考虑不周路过请不吝指点。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值