Groovy實現代碼熱載的機制和原理


 

前言:
  真的很久沒在博客園上更新博客了, 現在趁這段空閑的時間, 對之前接觸的一些工程知識做下總結. 先來講下借用Groovy如何來實現代碼的熱載, 以及其中涉及到的原理和需要注意的點.
  總的來說, Groovy作為一本動態編譯語言, 其對標應該是c/c++體系中的lua, 在一些業務邏輯變動頻繁的場景, 其意義非常的重大.

 

簡單入門:
  本文的主題是Groovy實現代碼熱載, 其他大背景是java實現主干代碼, groovy實現易變動的邏輯代碼. 先來看下java是如何調用的groovy腳本的.

import groovy.lang.Binding;
import groovy.lang.GroovyShell;

public class GroovyTest {

    public static void main(String[] args) {
        // *) groovy 代碼
        String script = "println 'hello'; 'name = ' + name;";

        // *) 傳入參數
        Binding binding = new Binding();
        binding.setVariable("name", "lilei");

        // *) 執行腳本代碼
        GroovyShell shell = new GroovyShell(binding);
        Object res = shell.evaluate(script);
        System.out.println(res);
    }

}

  這段代碼的輸出為:

hello
name = lilei

  Binding類主要用於傳遞參數集, 而GroovyShell則主要用於編譯執行Groovy代碼. 是不是比想象中的要簡答, ^_^.
  當然java調用groovy還有其他的方式, 下文會涉及到.

 

原理分析:
  下面這段其實大有文章.

GroovyShell shell = new GroovyShell(binding);
Object res = shell.evaluate(script);

  對於函數evaluate, 我們追蹤進去, 會有不少的重新認識.

    public Object evaluate(GroovyCodeSource codeSource) throws CompilationFailedException {
        Script script = this.parse(codeSource);
        return script.run();
    }

    public Script parse(GroovyCodeSource codeSource) throws CompilationFailedException {
        return InvokerHelper.createScript(this.parseClass(codeSource), this.context);
    }

  其大致的思路, 為Groovy腳本代碼包裝生成class, 然后產生該類實例對象, 在具體執行其包裝的邏輯代碼.
  但是這邊需要注意的情況:

    public Class parseClass(String text) throws CompilationFailedException {
        return this.parseClass(text, "script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy");
    }

  對於groovy腳本, 它默認會生成名字為script + System.currentTimeMillis() + Math.abs(text.hashCode())的class類, 也就是說傳入腳本, 它都會生成一個新類, 就算同一段groovy腳本代碼, 每調用一次, 都會生成一個新類.

陷阱評估:
  原理我們基本上理解了, 但是讓我們來構造一段代碼, 看看是否有哪些陷阱.

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;

import java.util.Map;
import java.util.TreeMap;

public class GroovyTest2 {

    private static GroovyShell shell = new GroovyShell();

    public static Object handle(String script, Map<String, Object> params) {
        Binding binding = new Binding();
        for ( Map.Entry<String, Object> ent : params.entrySet() ) {
            binding.setVariable(ent.getKey(), ent.getValue());
        }
        Script sci = shell.parse(script);
        sci.setBinding(binding);
        return sci.run();
    }

    public static void main(String[] args) {
        String script = "println 'hello'; 'name = ' + name;";
        Map<String, Object> params = new TreeMap<String, Object>();
        params.put("name", "lilei");
        while(true) {
            handle(script, params);
        }
    }

}

  這段代碼執行到最后的結果為, 頻繁觸發full gc, 究其原因為PermGen區爆滿. 這是為何呢?
  如上所分析的, 雖然是同一份腳本代碼, 但是都為其每次調用, 間接生成了一個class類. 對於full gc, 除了清理老年代, 也會順便清理永久代(PermGen), 但為何不清理這些一次性的class呢? 答案是gc條件不成立.
  引用下class被gc, 需滿足的三個條件:
  1). 該類所有的實例都已經被GC
  2). 加載該類的ClassLoader已經被GC
  3). 該類的java.lang.Class對象沒有在任何地方被引用
  加載類的ClassLoader實例被GroovyShell所持有, 作為靜態變量(gc root), 條件2不成立, GroovyClassLoader有個map成員, 會緩存編譯的class, 因此條件3都不成立.
  有人會問, 為何不把GroovyShell對象, 作為一個臨時變量呢?

    public static Object handle(String script, Map<String, Object> params) {
        Binding binding = new Binding();
        for ( Map.Entry<String, Object> ent : params.entrySet() ) {
            binding.setVariable(ent.getKey(), ent.getValue());
        }
        GroovyShell shell = new GroovyShell();
        Script sci = shell.parse(script);
        sci.setBinding(binding);
        return sci.run();
    }

  實際上, 還是治標不治本, 只是說class能被gc掉, 但是清理的速度可能趕不上產生的速度, 依舊頻繁觸發full gc.

 

推薦做法:
  解決上述問題很簡單, 就是引入緩存, 當然緩存的對象不上Script實例(在多線程環境下, 會遇到數據混亂的問題, 對象有狀態), 而是Script.class本身. 對應的key為腳本代碼的指紋.
  大致的代碼如下所示:

    private static ConcurrentHashMap<String, Class<Script>> zlassMaps
            = new ConcurrentHashMap<String, Class<Script>>();

    public static Object invoke(String scriptText, Map<String, Object> params) {
        String key = fingerKey(scriptText);
        Class<Script> script = zlassMaps.get(key);
        if ( script == null ) {
            synchronized (key.intern()) {
                // Double Check
                script = zlassMaps.get(key);
                if ( script == null ) {
                    GroovyClassLoader classLoader = new GroovyClassLoader();
                    script = classLoader.parseClass(scriptText);
                    zlassMaps.put(key, script);
                }
            }
        }

        Binding binding = new Binding();
        for ( Map.Entry<String, Object> ent : params.entrySet() ) {
            binding.setVariable(ent.getKey(), ent.getValue());
        }
        Script scriptObj = InvokerHelper.createScript(script, binding);
        return scriptObj.run();

    }

    // *) 為腳本代碼生成md5指紋
    public static String fingerKey(String scriptText) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(scriptText.getBytes("utf-8"));

            final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
            StringBuilder ret = new StringBuilder(bytes.length * 2);
            for (int i=0; i<bytes.length; i++) {
                ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]);
                ret.append(HEX_DIGITS[bytes[i] & 0x0f]);
            }
            return ret.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

  這邊會為每個新類單獨創建一個GroovyClassLoader對象, 也是巧妙地回避之前的陷阱.

 

總結:
  這邊沒有深入研究java中類的加載機制, 只是涉及class被gc的先決條件, 同時提供了一種思路, 如何借助groovy實現代碼熱加載, 同時又規避其中的陷阱.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM