乐趣区

关于java:Java-热更新-Groovy-实践及踩坑指南

Groovy 是什么?

Apache 的 Groovy 是 Java 平台上设计的面向对象编程语言。这门动静语言领有相似 Python、Ruby 和 Smalltalk 中的一些个性,能够作为 Java 平台的脚本语言应用,Groovy 代码动静地编译成运行于 Java 虚拟机(JVM)上的 Java 字节码,并与其余 Java 代码和库进行互操作。

因为其运行在 JVM 上的个性,Groovy 能够应用其余 Java 语言编写的库。Groovy 的语法与 Java 十分类似,大多数 Java 代码也合乎 Groovy 的语法规定,只管可能语义不同。Groovy 1.0 于 2007 年 1 月 2 日公布,并于 2012 年 7 月公布了 Groovy 2.0。从版本 2 开始,Groovy 也能够动态编译,提供类型推论和 Java 相近的性能。Groovy 2.4 是 Pivotal 软件资助的最初一个次要版本,截止于 2015 年 3 月。Groovy 曾经将其治理构造更改为 Apache 软件基金会的项目管理委员会(PMC)[1]。

Java 为何须要 Groovy ?

Groovy 个性如下:

  • 语法上反对动静类型,闭包等新一代语言个性
  • 无缝集成所有曾经存在的 Java 类库
  • 既反对面向对象编程也反对面向过程编程
  • 执行形式能够将 groovy 编写的源文件编译成 class 字节码文件,而后交给 JVM 去执行,也能够间接将 groovy 源文件解释执行。
  • Groovy 能够与 Java 完满联合,而且能够应用 java 所有的库

Groovy 劣势如下:

  • 麻利

    • groovy 在语法上退出了很多语法糖,很多 Java 严格的书写语法,在 Groovy 中只须要大量的语法糖即可实现
  • Groovy 的灵活性是的它既能够作为变成语言,亦可作为脚本语言
  • 0 老本学习 Groovy,完满适配 Java 语法

热部署技术设计及实现

应用场景

我将介绍如下几种罕用的适宜 Groovy 脚本热更新的场景,供您学习

风控平安——规定引擎

风控的规定引擎非常适合用 groovy 来实现,反抗黑产,策略人员每天都都会产出拦挡规定,如果每次都须要发版,可能发完观测完后,该薅的羊毛都被黑产薅没了。

所以利用 groovy 脚本引擎的动静解析执行,应用规定脚本将查拦挡规定形象进去,疾速部署,晋升效率。

监控核心

大型互联网零碎,随同着海量数据进入,各个层级的人员须要时时刻刻关注业务的各个维度指标,此时某个指标异样光靠人肉是没方法实现的。此时须要监控核心染指,提前部署好异动规定,当异样产生时,监控核心收回告警告诉到对应的规定创立人员,从而尽快查明起因,挽回资损。

此时要保障监控核心异样灵便,能够随时随地满足业务人员或者研发人员配置监控指标,测试咱们能够应用 Groovy 条件表达式,满足灵便监控规定配置需要。

流动营销

营销流动配置是我集体感觉最简单的业务之一。流动模板多样,千人千面,不同人群看到的流动款式或者“奖品”不一。且流动上线要快,成果回收,投入产出比等要能立刻观测。

此时须要工程侧形象出整个流动模板,在须要变动的中央嵌入 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);
        }
    }
}

重点关注:

  • 脚本开启缓存解决:否则屡次会更新可能会导致 Metaspace OutOfMemery

    脚本执行
// 程序外部须要关联出待执行的脚本即可
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);
}

生产踩坑指南

Java8 lambda 与 Groovy 语法问题

都说 Groovy 能完满兼容 Java 语法,即间接复制 Java 代码到 Groovy 文件内,亦能编译胜利。
事实真的如此么,咱们看如下执行的代码:

Set<String> demo = new HashSet<>();
demo.add("111");
demo.add("222");

for (String s : demo) {
    executor.submit({ -> 
        println "submit:" + s;                 
    });
}

for (String s in demo) {
    executor.submit({ -> 
        println "sp submit:" + s;                 
    });
}


// 输入后果
// submit: 222
// sp submit: 222
// submit: 222
// sp submit: 222

此时代码并没有依照预期的后果输入 111,222,这是为什么呢?

答:lambda 语法在 Groovy 中语义和在 Java 中不统一,尽管编译不出错,但表白的语义不统一
在 Groovy 中示意闭包概念,此处不相熟的能够 Google 具体理解 Groovy 语法。

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

通常加载 Groovy 类代码如下:

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

每次执行 groovyLoader.parseClass(groovyScript),Groovy 为了保障每次执行的都是新的脚本内容,会每次生成一个新名字的 Class 文件,这个点曾经在前文中阐明过。当对同一段脚本每次都执行这个办法时,会导致的景象就是装载的 Class 会越来越多,从而导致 PermGen 被用满。

同时这里也存在性能瓶颈问题,如果去剖析这段代码会发现 90% 的耗时占用在 Class。

如上实战过程中,曾经给出了解决办法:

  • 对于 parseClass 后生成的 Class 对象进行 cache,key 为 groovyScript 脚本的 md5 值

脚本首次执行耗时高

在初期计划上线时,压测后显示,首次加载脚本性能较慢,后续脚本执行速度十分快,猜想可能是 Groovy 外部在首次脚在脚本时还做了其余的校验(自己还没跟进这块,如果有读者感兴趣,能够断点具体看下链路耗时在哪里)

正对首次加载迟缓问题,解决办法如下:

// 1. 加载脚本,并缓存
GroovyObject object = loadClass(classSeq);
cacheMap.put(md5(classSeq), object);

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

// 3. 凋谢给线上流量应用

往期精彩

集体技术博客:https://jifuwei.github.io/
公众号:是咕咕鸡

  • 性能调优——小小的 log 大大的坑
  • 性能优化必备——火焰图
  • Flink 在风控场景实时特色落地实战

参考:
[1] Groovy Wiki

退出移动版