Groovy 热更新

110 阅读2分钟
风控安全——规则引擎

风控的规则引擎非常适合用groovy来实现,对抗黑产,策略人员每天都会产生出拦截规则,如果每次都要发版,可能发完观测完后,羊毛都被薅秃了

监控中心

大型互联网,海量数据进入,关注业务的各种维度指标,需要部署好异动规则

活动营销

活动模版是多样的,千人千面,不同人群看到的活动或者奖品不一,且活动上线要快,效果回收,投入产出比等要立即观测

脚本加载/更新

/**
 * 加载脚本
 * @param script
 * @return
 */
public static GroovyObject buildScript(String script) {
    if (StringUtils.isEmpty(script)) {
        throw new RuntimeException("script is empty");
    }

    String cacheKey = DigestUtils.md5DigestAsHex(script.getBytes());
    if (groovyObjectCache.containsKey(cacheKey)) {
        log.debug("groovyObjectCache hit");
        return groovyObjectCache.get(cacheKey);
    }

    GroovyClassLoader classLoader = new GroovyClassLoader();
    try {
        Class<?> groovyClass = classLoader.parseClass(script);
        GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
        classLoader.clearCache();

        groovyObjectCache.put(cacheKey, groovyObject);
        log.info("groovy buildScript success: {}", groovyObject);
        return groovyObject;
    } catch (Exception e) {
        throw new RuntimeException("buildScript error", e);
    } finally {
        try {
            classLoader.close();
        } catch (IOException e) {
            log.error("close GroovyClassLoader error", e);
        }
    }
}

脚本执行

// 程序内部需要关联出待执行的脚本即可
try {
    Map<String, Object> singleMap = GroovyUtils.invokeMethod2Map(s.getScriptObject(), s.getInvokeMethod(), params);
    data.putAll(singleMap);
} catch (Throwable e) {
    log.error(String.format("RcpEventMsgCleanScriptGroovyHandle groovy error, guid: %d eventCode: %s",
            s.getGuid(), s.getEventCode()), e);
}

// 三种执行方式,看 脚本内部返回的结果是什么
public static Map<String, Object> invokeMethod2Map(GroovyObject scriptObject, String invokeMethod, Object[] params) {
    return (Map<String, Object>) scriptObject.invokeMethod(invokeMethod, params);
}

public static boolean invokeMethod2Boolean(GroovyObject scriptObject, String invokeMethod, Object[] params) {
    return (Boolean) scriptObject.invokeMethod(invokeMethod, params);
}

public static String invokeMethod2String(GroovyObject scriptObject, String invokeMethod, Object[] params) {
    log.debug("GroovyObject class: {}", scriptObject.getClass().getSimpleName());
    return (String) scriptObject.invokeMethod(invokeMethod, params);
}

GroovyClassLoader 加载机制导致频繁gc问题

GroovyClassLoader groovyLoader = new GroovyClassLoader();
Class<Script> groovyClass = (Class<Script>) groovyLoader.parseClass(groovyScript);
Script groovyScript = groovyClass.newInstance();

每次执行groovyLoader.parseClass(groovyScript),Groovy为了保证每次执行的都是新的脚本内容,每次生成一个新名字的Class文件,当对同一段脚本执行时,会导致装载的Class会越来越多,导致PermGen被用满。

解决方法:

  • 对于parseClass后生成的Class对象进行cache,key为groovyScript脚本md5值
// 1.加载脚本,并缓存
GroovyObject object = loadClass(classSeq);
cacheMap.put(md5(classSeq), object);

// 2.预热
// 模拟方法调用
cacheMap.get(md5(classSeq)).invoke();

// 3.开放给线上流量使用