Java?熱更新?Groovy?實(shí)踐及踩坑指南(推薦)
Groovy 是什么?
Apache的Groovy是Java平臺(tái)上設(shè)計(jì)的面向?qū)ο缶幊陶Z(yǔ)言。這門動(dòng)態(tài)語(yǔ)言擁有類似Python、Ruby和Smalltalk中的一些特性,可以作為Java平臺(tái)的腳本語(yǔ)言使用,Groovy代碼動(dòng)態(tài)地編譯成運(yùn)行于Java虛擬機(jī)(JVM)上的Java字節(jié)碼,并與其他Java代碼和庫(kù)進(jìn)行互操作。
由于其運(yùn)行在JVM上的特性,Groovy可以使用其他Java語(yǔ)言編寫(xiě)的庫(kù)。Groovy的語(yǔ)法與Java非常相似,大多數(shù)Java代碼也符合Groovy的語(yǔ)法規(guī)則,盡管可能語(yǔ)義不同。 Groovy 1.0于2007年1月2日發(fā)布,并于2012年7月發(fā)布了Groovy 2.0。從版本2開(kāi)始,Groovy也可以靜態(tài)編譯,提供類型推論和Java相近的性能。Groovy 2.4是Pivotal軟件贊助的最后一個(gè)主要版本,截止于2015年3月。Groovy已經(jīng)將其治理結(jié)構(gòu)更改為Apache軟件基金會(huì)的項(xiàng)目管理委員會(huì)(PMC)[1]。
Java 為何需要 Groovy ?
Groovy 特性如下:
- 語(yǔ)法上支持動(dòng)態(tài)類型,閉包等新一代語(yǔ)言特性
- 無(wú)縫集成所有已經(jīng)存在的Java類庫(kù)
- 既支持面向?qū)ο缶幊桃仓С置嫦蜻^(guò)程編程
- 執(zhí)行方式可以將groovy編寫(xiě)的源文件編譯成class字節(jié)碼文件,然后交給JVM去執(zhí)行,也可以直接將groovy源文件解釋執(zhí)行。
- Groovy可以與Java完美結(jié)合,而且可以使用java所有的庫(kù)
Groovy 優(yōu)勢(shì)如下:
- 敏捷
- groovy 在語(yǔ)法上加入了很多語(yǔ)法糖,很多 Java 嚴(yán)格的書(shū)寫(xiě)語(yǔ)法,在 Groovy 中只需要少量的語(yǔ)法糖即可實(shí)現(xiàn)
- Groovy 的靈活性是的它既可以作為變成語(yǔ)言,亦可作為腳本語(yǔ)言
- 0成本學(xué)習(xí) Groovy,完美適配 Java 語(yǔ)法
熱部署技術(shù)設(shè)計(jì)及實(shí)現(xiàn)
使用場(chǎng)景
我將介紹如下幾種常用的適合 Groovy 腳本熱更新的場(chǎng)景,供您學(xué)習(xí)
風(fēng)控安全——規(guī)則引擎
風(fēng)控的規(guī)則引擎非常適合用 groovy 來(lái)實(shí)現(xiàn),對(duì)抗黑產(chǎn),策略人員每天都都會(huì)產(chǎn)出攔截規(guī)則,如果每次都需要發(fā)版,可能發(fā)完觀測(cè)完后,該薅的羊毛都被黑產(chǎn)薅沒(méi)了。
所以利用 groovy 腳本引擎的動(dòng)態(tài)解析執(zhí)行,使用規(guī)則腳本將查攔截規(guī)則抽象出來(lái),快速部署,提升效率。
監(jiān)控中心
大型互聯(lián)網(wǎng)系統(tǒng),伴隨著海量數(shù)據(jù)進(jìn)入,各個(gè)層級(jí)的人員需要時(shí)時(shí)刻刻關(guān)注業(yè)務(wù)的各個(gè)維度指標(biāo),此時(shí)某個(gè)指標(biāo)異常光靠人肉是沒(méi)辦法實(shí)現(xiàn)的。此時(shí)需要監(jiān)控中心介入,提前部署好異動(dòng)規(guī)則,當(dāng)異常發(fā)生時(shí),監(jiān)控中心發(fā)出告警通知到對(duì)應(yīng)的規(guī)則創(chuàng)建人員,從而盡快查明原因,挽回資損。
此時(shí)要保證監(jiān)控中心異常靈活,可以隨時(shí)隨地滿足業(yè)務(wù)人員或者研發(fā)人員配置監(jiān)控指標(biāo),測(cè)試我們可以使用 Groovy 條件表達(dá)式,滿足靈活監(jiān)控規(guī)則配置需求。
活動(dòng)營(yíng)銷
營(yíng)銷活動(dòng)配置是我個(gè)人覺(jué)得最復(fù)雜的業(yè)務(wù)之一?;顒?dòng)模板多樣,千人千面,不同人群看到的活動(dòng)樣式或者“獎(jiǎng)品”不一。且活動(dòng)上線要快,效果回收,投入產(chǎn)出比等要能立即觀測(cè)。
此時(shí)需要工程側(cè)抽象出整個(gè)活動(dòng)模板,在需要變化的地方嵌入 Groovy 腳本,這樣就減少了測(cè)試和發(fā)版的時(shí)間,做到活動(dòng)可線上配置化。
技術(shù)實(shí)現(xiàn)
腳本加載/更新
代碼實(shí)現(xiàn)展示:
/**
* 加載腳本
* @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);
}
}
}重點(diǎn)關(guān)注:
- 腳本開(kāi)啟緩存處理:否則多次會(huì)更新可能會(huì)導(dǎo)致 Metaspace OutOfMemery
腳本執(zhí)行
// 程序內(nèi)部需要關(guān)聯(lián)出待執(zhí)行的腳本即可
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);
}
// 三種執(zhí)行方式,看 腳本內(nèi)部返回的結(jié)果是什么
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);
}生產(chǎn)踩坑指南
Java8 lambda 與 Groovy 語(yǔ)法問(wèn)題
都說(shuō) Groovy 能完美兼容 Java 語(yǔ)法,即直接復(fù)制 Java 代碼到 Groovy 文件內(nèi),亦能編譯成功。
事實(shí)真的如此么,我們看如下執(zhí)行的代碼:
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;
});
}
// 輸出結(jié)果
// submit: 222
// sp submit: 222
// submit: 222
// sp submit: 222此時(shí)代碼并沒(méi)有按照預(yù)期的結(jié)果輸出 111, 222,這是為什么呢?
答:lambda 語(yǔ)法在 Groovy 中語(yǔ)義和在Java 中不一致,雖然編譯不出錯(cuò),但表達(dá)的語(yǔ)義不一致
在 Groovy 中表示閉包概念,此處不熟悉的可以 Google 詳細(xì)了解 Groovy 語(yǔ)法。
GroovyClassLoader 加載機(jī)制導(dǎo)致頻繁gc問(wèn)題
通常加載 Groovy 類代碼如下:
GroovyClassLoader groovyLoader = new GroovyClassLoader(); Class<Script> groovyClass = (Class<Script>) groovyLoader.parseClass(groovyScript); Script groovyScript = groovyClass.newInstance();
每次執(zhí)行 groovyLoader.parseClass(groovyScript),Groovy 為了保證每次執(zhí)行的都是新的腳本內(nèi)容,會(huì)每次生成一個(gè)新名字的Class文件,這個(gè)點(diǎn)已經(jīng)在前文中說(shuō)明過(guò)。當(dāng)對(duì)同一段腳本每次都執(zhí)行這個(gè)方法時(shí),會(huì)導(dǎo)致的現(xiàn)象就是裝載的Class會(huì)越來(lái)越多,從而導(dǎo)致PermGen被用滿。
同時(shí)這里也存在性能瓶頸問(wèn)題,如果去分析這段代碼會(huì)發(fā)現(xiàn)90%的耗時(shí)占用在Class。
如上實(shí)戰(zhàn)過(guò)程中,已經(jīng)給出了解決辦法:
- 對(duì)于 parseClass 后生成的 Class 對(duì)象進(jìn)行cache,key 為 groovyScript 腳本的md5值
腳本首次執(zhí)行耗時(shí)高
在初期方案上線時(shí),壓測(cè)后顯示,首次加載腳本性能較慢,后續(xù)腳本執(zhí)行速度非??欤聹y(cè)可能是 Groovy 內(nèi)部在首次腳在腳本時(shí)還做了其他的校驗(yàn)(本人還沒(méi)跟進(jìn)這塊,如果有讀者感興趣,可以斷點(diǎn)詳細(xì)看下鏈路耗時(shí)在哪里)
正對(duì)首次加載緩慢問(wèn)題,解決方法如下:
// 1.加載腳本,并緩存 GroovyObject object = loadClass(classSeq); cacheMap.put(md5(classSeq), object); // 2.預(yù)熱 // 模擬方法調(diào)用 cacheMap.get(md5(classSeq)).invoke(); // 3.開(kāi)放給線上流量使用
到此這篇關(guān)于Java 熱更新 Groovy 實(shí)踐及踩坑指南的文章就介紹到這了,更多相關(guān)Java 熱更新 Groovy內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot+mybatis plus實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)查詢
實(shí)際開(kāi)發(fā)過(guò)程中經(jīng)常需要查詢節(jié)點(diǎn)樹(shù),根據(jù)指定節(jié)點(diǎn)獲取子節(jié)點(diǎn)列表,本文主要介紹了springboot+mybatis plus實(shí)現(xiàn)樹(shù)形結(jié)構(gòu)查詢,感興趣的可以了解一下2021-07-07
java實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07
Java常用類庫(kù)Apache Commons工具類說(shuō)明及使用實(shí)例詳解
這篇文章主要介紹了Java常用類庫(kù)Apache Commons工具類說(shuō)明及使用實(shí)例詳解,需要的朋友可以參考下2020-02-02
Java實(shí)現(xiàn)權(quán)重隨機(jī)算法詳解
平時(shí),經(jīng)常會(huì)遇到權(quán)重隨機(jī)算法,從不同權(quán)重的N個(gè)元素中隨機(jī)選擇一個(gè),并使得總體選擇結(jié)果是按照權(quán)重分布的。本文就詳細(xì)來(lái)介紹如何實(shí)現(xiàn),感興趣的可以了解一下2021-07-07
Java字符串拼接+和StringBuilder的比較與選擇
Java 提供了兩種主要的方式:使用 "+" 運(yùn)算符和使用 StringBuilder 類,本文主要介紹了Java字符串拼接+和StringBuilder的比較與選擇,感興趣的可以了解一下2023-10-10
springboot項(xiàng)目部署在linux上運(yùn)行的兩種方式小結(jié)
這篇文章主要介紹了springboot項(xiàng)目部署在linux上運(yùn)行的兩種方式小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
java數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)機(jī)器人行走
這篇文章主要為大家詳細(xì)介紹了java數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)機(jī)器人行走,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01
SpringBoot3集成iText實(shí)現(xiàn)PDF導(dǎo)出功能
不知道小伙伴們?cè)陧?xiàng)目中有沒(méi)有遇到過(guò)導(dǎo)出 PDF 的需求,小編在之前的 tienchin 項(xiàng)目中有一個(gè)合同導(dǎo)出的功能,需要將文檔導(dǎo)出為PDF,將文檔導(dǎo)出為 PDF 有很多方案,不同方案的優(yōu)缺點(diǎn)也各不相同,今天小編就和大家演示一個(gè),感興趣的小伙伴跟著小編一起來(lái)看看吧2024-10-10
18個(gè)Java8日期處理的實(shí)踐(太有用了)
這篇文章主要介紹了18個(gè)Java8日期處理的實(shí)踐(太有用了),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01

