Java 热更新 Groovy 实践及踩坑指南(推荐)

 更新时间:2022年09月13日 10:40:46   作者:是咕咕鸡  
Apache的Groovy是Java平台上设计的面向对象编程语言,这门动态语言拥有类似Python、Ruby和Smalltalk中的一些特性,可以作为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.开放给线上流量使用

到此这篇关于Java 热更新 Groovy 实践及踩坑指南的文章就介绍到这了,更多相关Java 热更新 Groovy内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 面试题:Java中如何停止线程的方法

    面试题:Java中如何停止线程的方法

    这篇文章主要介绍了Java中如何停止线程的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • 解决SpringBoot整合ElasticSearch遇到的连接问题

    解决SpringBoot整合ElasticSearch遇到的连接问题

    这篇文章主要介绍了解决SpringBoot整合ElasticSearch遇到的连接问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • springboot实现上传并解析Excel过程解析

    springboot实现上传并解析Excel过程解析

    这篇文章主要介绍了springboot实现上传并解析Excel过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • Java调用Pytorch模型实现图像识别

    Java调用Pytorch模型实现图像识别

    这篇文章主要为大家详细介绍了Java如何调用Pytorch实现图像识别功能,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解一下
    2023-06-06
  • Java的AQS基本原理详细分析

    Java的AQS基本原理详细分析

    这篇文章主要介绍了Java的AQS基本原理详细分析,AQS是Abstract Queued Synchronizer的简称,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,本文主要讲解分析其基本原理,需要的朋友可以参考下
    2024-01-01
  • 详解springboot之jackson的两种配置方式

    详解springboot之jackson的两种配置方式

    这篇文章主要介绍了详解springboot之jackson的两种配置方式,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-11-11
  • 微信公众号服务号推送模板消息设置方法(后端java)

    微信公众号服务号推送模板消息设置方法(后端java)

    公众号时经常会需要写推送消息,从网上找了一大堆,都不是很全,所以这篇文章主要介绍了微信公众号服务号推送模板消息设置方法的相关资料,需要的朋友可以参考下
    2023-02-02
  • @Valid注解的作用及@Valid注解与@Validated的区别

    @Valid注解的作用及@Valid注解与@Validated的区别

    这篇文章主要介绍了@Valid注解的作用及@Valid注解与@Validated的区别,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • spring容器初始化遇到的死锁问题解决

    spring容器初始化遇到的死锁问题解决

    这篇文章主要给大家介绍了关于spring容器初始化时候遇到的死锁问题的解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-07-07
  • Java完美实现2048小游戏

    Java完美实现2048小游戏

    本文给大家分享的是一则根据网友的代码改编的2048小游戏的源码,个人认为已经非常完美了,推荐给大家,有需要的小伙伴可以参考下。
    2015-03-03

最新评论