溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Groovy的規(guī)則腳本引擎怎么應(yīng)用

發(fā)布時間:2023-03-13 11:17:48 來源:億速云 閱讀:266 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“Groovy的規(guī)則腳本引擎怎么應(yīng)用”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

    1.為什么用groovy作為規(guī)則引擎

    互聯(lián)網(wǎng)時代隨著業(yè)務(wù)的飛速發(fā)展,迭代和產(chǎn)品接入的速度越來越快,需要一些靈活的配置。

    辦法通常有如下幾個方面:

    1、最為傳統(tǒng)的方式是java程序直接寫死提供幾個可調(diào)節(jié)的參數(shù)配置然后封裝成為獨立的業(yè)務(wù)模塊組件,在增加參數(shù)或簡單調(diào)整規(guī)則后,重新調(diào)上線。
     

    2、使用開源方案,例如drools規(guī)則引擎,此類引擎適合業(yè)務(wù)較復(fù)雜的系統(tǒng)
     

    3、使用動態(tài)腳本引擎:groovy,simpleEl,QLExpress

    引入規(guī)則腳本對業(yè)務(wù)進行抽象可以大大提升效率,例如:

    在貸款審核系統(tǒng)中,貸款的訂單在收單后會經(jīng)過多個流程扭轉(zhuǎn):收單后需要根據(jù)風(fēng)控系統(tǒng)給出結(jié)果決定訂單的流程,而不同的產(chǎn)品訂單的扭轉(zhuǎn)是不一致的,每接入一個新產(chǎn)品,碼農(nóng)都要寫一堆對于此產(chǎn)品的流程邏輯;現(xiàn)有的產(chǎn)品規(guī)則也經(jīng)常需要更換。

    所以想利用腳本引擎動態(tài)解析執(zhí)行,到使用規(guī)則腳本將流程的扭轉(zhuǎn)抽象出來,提升效率

    groovy的優(yōu)勢:

    1.歷史悠久,使用范圍大,坑少

    2.和java兼容性強:可以無縫銜接,即使不懂groovy語法也沒有關(guān)系

    3.語法糖

    4.項目周期短,上線時間急

    項目流轉(zhuǎn)的抽象:

    因為不同的業(yè)務(wù)在流轉(zhuǎn)的過程中對于邏輯處理是不一致的,我們先考慮一種簡單的情況:

    本身的項目在業(yè)務(wù)上會對不同的貸款訂單進行流程扭轉(zhuǎn),例如訂單可以從流程a扭轉(zhuǎn)到流程b或者流程c,取決于每一個Strategy unit(策略單位)的執(zhí)行情況:每個strategy unit執(zhí)行后都會返回一個boolean值,具體邏輯可以自己定義,在這里我們假設(shè):如果滿足所有的strategy unit a的條件(既每個執(zhí)行單元都返回true),那么訂單就會扭轉(zhuǎn)到Scenario B;如果滿足所有的strategy unit b的條件那么訂單就會扭轉(zhuǎn)到scenario c。

    那為什么要設(shè)計成多個strategy unit呢?是因為我的項目中,為了方便配置,將整個流程的strategyunit的配置展示在ui上,可讀性強,修改時也只需要修改某一個unit中的執(zhí)行邏輯

    Groovy的規(guī)則腳本引擎怎么應(yīng)用

    每個strategy unit執(zhí)行時依賴的數(shù)據(jù)我們可以抽象成為一個context,context中包含兩部分數(shù)據(jù):

    一部分是業(yè)務(wù)上的數(shù)據(jù):例如訂單的產(chǎn)品,訂單依賴的風(fēng)控數(shù)據(jù)等

    一部分是規(guī)則執(zhí)行的數(shù)據(jù):包括當前執(zhí)行的node、所屬的策略組信息、當前的流程、下一個流程等

    這一部分規(guī)則引擎執(zhí)行數(shù)據(jù)的context可以根據(jù)不同的業(yè)務(wù)進行設(shè)計,設(shè)計時主要考慮斷點重跑,策略組等:比如可以設(shè)計不同的策略組與產(chǎn)品進行關(guān)聯(lián),這一部分業(yè)務(wù)耦合性比較大,本文主要focus在groovy上

    可以把Context理解為Strategy Unit的輸入和輸出,Strategy Unit在Groovy中進行執(zhí)行,我們可以對每一個執(zhí)行的Strategy Unit進行可配置化的展示和配置。執(zhí)行過程中可以根據(jù)context中含有的不同的信息進行邏輯判斷,也可以改變context對象中的值。

    基于流程將Groovy與Java的集成

    基于上面的設(shè)計,groovy腳本的執(zhí)行本質(zhì)上只是接受context對象,并且基于context對象中的關(guān)鍵信息進行邏輯判斷,輸出結(jié)果,而結(jié)果也保存在context中。

     先看看Groovy與java集成的方式:

    1.GroovyClassLoader

    用 Groovy 的 GroovyClassLoader ,它會動態(tài)地加載一個腳本并執(zhí)行它。GroovyClassLoader是一個Groovy定制的類裝載器,負責(zé)解析加載Java類中用到的Groovy類。

    2.GroovyShell

    GroovyShell允許在Java類中(甚至Groovy類)求任意Groovy表達式的值。您可使用Binding對象輸入?yún)?shù)給表達式,并最終通過GroovyShell返回Groovy表達式的計算結(jié)果。

    3.GroovyScriptEngine

    GroovyShell多用于推求對立的腳本或表達式,如果換成相互關(guān)聯(lián)的多個腳本,使用GroovyScriptEngine會更好些。GroovyScriptEngine從您指定的位置(文件系統(tǒng),URL,數(shù)據(jù)庫,等等)加載Groovy腳本,并且隨著腳本變化而重新加載它們。如同GroovyShell一樣,GroovyScriptEngine也允許您傳入?yún)?shù)值,并能返回腳本的值。

    現(xiàn)在我們以GroovyClassLoader為例,展示一下如何實現(xiàn)與java的集成:

    例如:我們假設(shè)申請金額大于20000的訂單進入流程B

    1.在SpringBoot項目中maven中引入

    <dependency>
                <groupId>org.codehaus.groovy</groupId>
                <artifactId>groovy-all</artifactId>
                <version>2.4.10</version>
    </dependency>

    2.定義groovy執(zhí)行的java接口

    public interface EngineGroovyModuleRule {
        boolean run(Object context);
    }

    3.抽象出一個Groovy模板文件,放在resource下面以便加載:

    import com.groovyexample.groovy.*
    class %s implements EngineGroovyModuleRule {
        boolean run(Object context){
            %s //業(yè)務(wù)執(zhí)行邏輯:可配置化
        }
    }

    4.解析groovy的模板文件,可以將模板文件緩存起來,解析通過spring的PathMatchingResourcePatternResolver進行

    下面的Strategy Unit這個String就是具體的業(yè)務(wù)規(guī)則的邏輯,把這一部分的邏輯進行一個配置化

            //解析Groovy模板文件
            ConcurrentHashMap<String,String> concurrentHashMap = new ConcurrentHashMap(128);
            final String path = "classpath*:*.groovy_template";
            PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
            Arrays.stream(resolver.getResources(path))
                    .parallel()
                    .forEach(resource -> {
                        try {
                            String fileName = resource.getFilename();
                            InputStream input = resource.getInputStream();
                            InputStreamReader reader = new InputStreamReader(input);
                            BufferedReader br = new BufferedReader(reader);
                            StringBuilder template = new StringBuilder();
                            for (String line; (line = br.readLine()) != null; ) {
                                template.append(line).append("\n");
                            }
                            concurrentHashMap.put(fileName, template.toString());
                        } catch (Exception e) {
                            log.error("resolve file failed", e);
                        }
                    });
            String scriptBuilder = concurrentHashMap.get("ScriptTemplate.groovy_template");
            String scriptClassName = "testGroovy";
            //這一部分String的獲取邏輯進行可配置化
            String StrategyLogicUnit = "if(context.amount>=20000){\n" +
                    "            context.nextScenario='A'\n" +
                    "            return true\n" +
                    "        }\n" +
                    "        ";
            String fullScript = String.format(scriptBuilder, scriptClassName, StrategyLogicUnit);
        GroovyClassLoader classLoader = new GroovyClassLoader();
        Class<EngineGroovyModuleRule> aClass = classLoader.parseClass(fullScript);
        Context context = new Context();
        context.setAmount(30000);
        try {
            EngineGroovyModuleRule engineGroovyModuleRule = aClass.newInstance();
            log.info("Groovy Script returns:{} "+engineGroovyModuleRule.run(context));
            log.info("Next Scenario is {}"+context.getNextScenario());
        }
        catch (Exception e){
           log.error("error...")
        }

    5.執(zhí)行上述代碼:

    Groovy Script returns: true
    Next Scenario is A

    關(guān)鍵的部分是Strategy Unit這個部分的可配置化,我們是通過管理端UI上展示不同產(chǎn)品對應(yīng)的StrategyLogicUnit,并可進行CRUD,為了方便配置同時引進了策略組、產(chǎn)品策略復(fù)制關(guān)聯(lián)、一鍵復(fù)制模板等功能。

    集成過程中的坑和性能優(yōu)化

    項目在測試時就發(fā)現(xiàn)隨著收單的數(shù)量增加,進行頻繁的Full GC,測試環(huán)境復(fù)現(xiàn)后查看日志顯示:

    [Full GC (Metadata GC Threshold) [PSYoungGen: 64K->0K(43008K)] 
    [ParOldGen: 3479K->3482K(87552K)] 3543K->3482K(130560K), 
    [Metaspace: 15031K->15031K(1062912K)], 0.0093409 secs] 
    [Times: user=0.03 sys=0.00, real=0.01 secs] 

    日志中可以看出是mataspace空間不足,并且無法被full gc回收。 通過JVisualVM可以查看具體的情況:

    Groovy的規(guī)則腳本引擎怎么應(yīng)用

    發(fā)現(xiàn)class太多了,有2326個,導(dǎo)致metaspace滿了。我們先回顧一下metaspace ##metaspace和permgen 這是jdk在1.8中才有的東西,并且1.8講將permgen去除了,其中的方法區(qū)移到non-heap中的Metaspace。

    Groovy的規(guī)則腳本引擎怎么應(yīng)用

     這個區(qū)域主要存放:存儲類的信息、常量池、方法數(shù)據(jù)、方法代碼等。 分析主要問題有兩方面:

    問題1:Class數(shù)量問題:可能是引入groovy導(dǎo)致加載的類過多了,但實際上項目只配置了10個StrategyLogicUnit,不同的訂單執(zhí)行同一個StrategyLogicUnit時應(yīng)該對應(yīng)同一個class。class的數(shù)量過于異常。

    問題2:就算Class數(shù)量過多,F(xiàn)ull GC為何沒有辦法回收?

    GroovyClassLoader的加載

    我們先分析Groovy執(zhí)行的過程,最關(guān)鍵的代碼是如下幾部分:

     GroovyClassLoader classLoader = new GroovyClassLoader();
     Class<EngineGroovyModuleRule> aClass = classLoader.parseClass(fullScript);
     EngineGroovyModuleRule engineGroovyModuleRule = aClass.newInstance();
    engineGroovyModuleRule.run(context)

    GroovyClassLoader是一個定制的類裝載器,在代碼執(zhí)行時動態(tài)加載groovy腳本為java對象。

    大家都知道classloader的雙親委派,我們先來分析一下這個GroovyClassloader,看看它的祖先分別是啥:

    def cl = this.class.classLoader  
    while (cl) {  
        println cl  
        cl = cl.parent  
    }

    輸出:

    groovy.lang.GroovyClassLoader$InnerLoader@13322f3  
    groovy.lang.GroovyClassLoader@127c1db  
    org.codehaus.groovy.tools.RootLoader@176db54  
    sun.misc.Launcher$AppClassLoader@199d342  
    sun.misc.Launcher$ExtClassLoader@6327fd  

    從而得出:

        Bootstrap ClassLoader  
                 &uarr;  
    sun.misc.Launcher.ExtClassLoader      // 即Extension ClassLoader  
                 &uarr;  
    sun.misc.Launcher.AppClassLoader      // 即System ClassLoader  
                 &uarr;  
    org.codehaus.groovy.tools.RootLoader  // 以下為User Custom ClassLoader  
                 &uarr;  
    groovy.lang.GroovyClassLoader  
                 &uarr;  
    groovy.lang.GroovyClassLoader.InnerLoader  

    查看關(guān)鍵的GroovyClassLoader.parseClass方法,發(fā)現(xiàn)如下代碼:

        public Class parseClass(String text) throws CompilationFailedException {
            return parseClass(text, "script" + System.currentTimeMillis() +
                    Math.abs(text.hashCode()) + ".groovy");
        }
        protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) {
            InnerLoader loader = AccessController.doPrivileged(new PrivilegedAction<InnerLoader>() {
                public InnerLoader run() {
                    return new InnerLoader(GroovyClassLoader.this);
                }
            });
            return new ClassCollector(loader, unit, su);
        }

    這兩處代碼的意思是: groovy每執(zhí)行一次腳本,都會生成一個腳本的class對象,這個class對象的名字由 "script" + System.currentTimeMillis() + Math.abs(text.hashCode()組成,對于問題1:每次訂單執(zhí)行同一個StrategyLogicUnit時,產(chǎn)生的class都不同,每次執(zhí)行規(guī)則腳本都會產(chǎn)品一個新的class。

    接著看問題2InnerLoader部分: groovy每執(zhí)行一次腳本都會new一個InnerLoader去加載這個對象,而對于問題2,我們可以推測:InnerLoader和腳本對象都無法在fullGC的時候被回收,因此運行一段時間后將PERM占滿,一直觸發(fā)fullGC。

    為什么需要有innerLoader呢?

    結(jié)合雙親委派模型,由于一個ClassLoader對于同一個名字的類只能加載一次,如果都由GroovyClassLoader加載,那么當一個腳本里定義了C這個類之后,另外一個腳本再定義一個C類的話,GroovyClassLoader就無法加載了。

    由于當一個類的ClassLoader被GC之后,這個類才能被GC。

    如果由GroovyClassLoader加載所有的類,那么只有當GroovyClassLoader被GC了,所有這些類才能被GC,而如果用InnerLoader的話,由于編譯完源代碼之后,已經(jīng)沒有對它的外部引用,除了它加載的類,所以只要它加載的類沒有被引用之后,它以及它加載的類就都可以被GC了。

    Class回收的條件(摘自《深入理解JVM虛擬機》)

    JVM中的Class只有滿足以下三個條件,才能被GC回收,也就是該Class被卸載(unload): 

    1、該類所有的實例都已經(jīng)被GC,也就是JVM中不存在該Class的任何實例。 

    2、加載該類的ClassLoader已經(jīng)被GC。 

    3、該類的java.lang.Class 

    對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法. 

    第一點被排除

    查看GroovyClassLoader.parseClass()代碼,總結(jié):Groovy會把腳本編譯為一個名為Scriptxx的類,這個腳本類運行時用反射生成一個實例并調(diào)用它的MAIN函數(shù)執(zhí)行,這個動作只會被執(zhí)行一次,在應(yīng)用里面不會有其他地方引用該類或它生成的實例;

    第二點被排除:

    關(guān)于InnerLoader:Groovy專門在編譯每個腳本時new一個InnerLoader就是為了解決GC的問題,所以InnerLoader應(yīng)該是獨立的,并且在應(yīng)用中不會被引用;

    只剩下第三種可能:

    該類的Class對象有被引用,繼續(xù)查看代碼:

        /**
         * sets an entry in the class cache.
         *
         * @param cls the class
         * @see #removeClassCacheEntry(String)
         * @see #getClassCacheEntry(String)
         * @see #clearCache()
         */
        protected void setClassCacheEntry(Class cls) {
            synchronized (classCache) {
                classCache.put(cls.getName(), cls);
            }
        }

    可以復(fù)現(xiàn)問題并查看原因:具體思路是無限循環(huán)解析腳本,jmap -clsstat查看classloader的情況,并結(jié)合導(dǎo)出dump查看引用關(guān)系。

    所以總結(jié)原因是:每次groovy parse腳本后,會緩存腳本的Class,下次解析該腳本時,會優(yōu)先從緩存中讀取。這個緩存的Map由GroovyClassLoader持有,key是腳本的類名,value是class,class對象的命名規(guī)則為

    "script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy"

    因此,每次編譯的對象名都不同,都會在緩存中添加一個class對象,導(dǎo)致class對象不可釋放,隨著次數(shù)的增加,編譯的class對象將PERM區(qū)撐滿。

    解決方案

    大多數(shù)的情況下,Groovy都是編譯后執(zhí)行的,實際在本次的應(yīng)用場景中,雖然是腳本是以參數(shù)傳入,但其實大多數(shù)腳本的內(nèi)容是相同的。

    解決方案就是在項目啟動時通過InitializingBean接口對于 parseClass 后生成的 Class 對象進行緩存,key 為 groovyScript 腳本的md5值,并且在配置端修改配置后可進行緩存刷新。 這樣做的好處有兩點:

    1、解決metaspace爆滿的問題

    2、因為不需要在運行時編譯加載,所以可以加快腳本執(zhí)行的速度

    “Groovy的規(guī)則腳本引擎怎么應(yīng)用”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

    向AI問一下細節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI