1. 程式人生 > >《開源框架那點事兒25》:對框架模板引擎實現方式的改造實錄

《開源框架那點事兒25》:對框架模板引擎實現方式的改造實錄

port 內嵌 代碼調試 iter put 文件路徑 children nts fault

點滴悟透設計思想,Tiny模板引擎優化實錄! 增加框架設計興趣小組:http://bbs.tinygroup.org/group-113-1.html

Tiny模板引擎的實現方式原來是採用的編譯方式,近期發生了一些問題。因此我認為有必要把編譯方式調整為解釋方式,為此就開始了此次實現活動。

編譯方式存在的問題

當時採用編譯方式。主要是考慮到編譯方式在執行時不必再去遍歷語法樹。因此就採用了編譯方式。

可是在實際應用其中,出現了例如以下問題:

文件路徑沖突的問題

因為採用的是編譯方式,這個時候就存在在一個選擇。即:Java源碼落地或不落地的選擇。

假設Java文件不落地。則在有問題的時候,假設想要進行代碼調試(盡管這樣的場景並不多見),那麽就沒有源碼可供調試。假設Java代碼落地,則會存在一個問題,那就是資源文件在磁盤文件裏產生沖突的問題。

相同的問題對於class文件也存在,假設不落地,那麽每次應用重新啟動動的時候。都要又一次編譯這些文件以產生class文件;假設落地。則也會產生沖突的問題。

當然,Tiny模板引擎通過添加一個配置項,攻克了這個沖突的問題,可是因為添加了一個配置項。從客觀上添加了維護人員的工作量,也easy造成當維護人員不了解這裏面的道道,忘記設置從而導致在一臺server中部署多個Tiny應用時多個應用中的模板文件生成的java文件和class文件的沖突。從而導致出現故障。

PermSize內存占用問題

採用編譯方式的時候。因為每一個模板文件都要生成一個類,每一個宏也要生成一個類,在宏調用過程中,也要生成一些類。(本來是能夠不生成這些類的,可是因為Tiny模板引擎支持了一些很實用的特性。所以宏調用時時採用編譯方式。就要生成一些內嵌類來完畢)。這樣,就會生成大量的Java類,從project很大的時候,就會導致PermSize戰勝很大。尤其是在系統還在調試的時候,模板文件變化的時候,就要又一次編譯生成新的類。為了避免必須又一次啟動應用server才幹生生效,因此採用了自己編寫ClassLoader的方式來達到即時刷新的問題。可是因為Java的垃圾回收機制,決定了垃圾不是及時回收的,可是因為每一個類都要有一個ClassLoader來支持,以便及時替換,因此這會進一步放大內存的占用。

載入速度比較長的問題

因為Tiny模板引擎中提供了宏,而這些宏能夠獨立存在。因此在應用啟動的時候就必須載入全部的宏到內存中。以便查找。

所以就導致第一次啟動的時候。因為要編譯全部的宏文件並載入之,導致啟動速度很慢。

在以後的啟動的時候,也必須檢測模板文件與生成的類是否一致。是否有被改動過。當a項目規模比較大的時候,這個時間也是比較長的。

尤其是在開發期。啟動時間添加10秒,都會讓開發者感覺到難以忍受。

訪問速度的問題

採用編譯方式的問題,在訪問上也有一些問題。

為了提升應用啟動時間,僅僅有宏文件是在啟動時預選編譯好並載入了的,而模板文件和布局文件則沒有這樣的待遇,這就導致假設在訪問的時候。第一次訪問的時候,須要編譯模板文件為java文件,再把java文件編譯為class文件,假設這次訪問還用到了布局文件,還import了其他的模板文件,那麽悲劇了。第一個訪問者可能要多等待幾秒鐘的時間。同一時候。為了避免多次編譯情況的地生,還要添加同步鎖。這樣會進一步影響到訪問的效率。

詳細還沒有測試過ClassLoader太多對性能有多大的影響,可是毛估估是有一定影響的,畢竟要添加查找的層數。幹的活多了。幹的活慢了也是自然的。人是這樣,計算機也是相同的道理。

採用解釋方式帶來的優點

因為採用解釋方式,因此不必生成java源文件和class文件,因此也就不存在文件路徑沖突的問題;相同也不存在PermSize和眾多ClassLoader大量占用內存的問題。

因為採用解釋方式,第一次載入,僅僅定性掃描部分關系的內容就可以。因此掃描速度很快;僅僅有在直接運行的時候,才須要更具體的處理,同一時候因為不須要進行編譯,不須要做同步處理,因此載入速度會比編譯方式高很多。尤其是和編譯方式的第一次載入時間相比。

訪問速度方面的問題。我原來的感覺來說,感覺編譯方式會快一些,畢竟它不用再雲遍歷語法樹,可是實際運行下來,感覺解釋方式大致有一倍左右的提升,我分析了一下原因。大致能夠覺得是例如以下原因:1.因為Java的優化策略。導致使用頻率高的訪問會進行深度性能優化,採用解釋方式。因為用到的就是那幾個函數,因此能夠非常快滿足Java虛擬機的要求,更早的進行深度優化。2.因為解釋方式和編譯方式相比,能夠採用更優化的解決方式,因此遍歷語法樹的時間由避免做一些事情彌補回來了,因此感受性能反而更高一點點。總之,這次編譯改解釋,的效果還是明顯的,各方面全面讓我愜意,尤其是前面操心的運行效率方面也有大概50%左右的提升是讓我喜出望外的。

另一個意外之喜是通過把編譯方式改成解釋運行方式,代碼規模縮小了近一半,由原來的8000+行,變成4000+行。同一時候。因為不必要依賴JDT,antlr也僅僅要依賴runtime包就可以,還順便降低了3M的WAR包大小。

OK,說了這麽多,那就說說這次改造過程。

因為團隊去島國旅遊,當時把這個任務交給一個留守同學來完畢,可是前後兩周的時候。沒有提交出我愜意的結果,因為看不到興許完畢的時間節點,沒有辦法,僅僅好我老先生親自己主動手來完畢了,OK開工,相信細致閱讀以下一節內容的同學。會對ANTLR解釋引擎的開發有深入了解,甚至拿我的代碼照葫蘆畫瓢,直接就可用。

解釋引擎改造實錄

解釋引擎總控類

解釋引擎總控類是解釋引擎的核心,因為這個東東是為了Tiny模板引擎定制編寫的,因此假設有同學要拿來改造,請照葫蘆畫瓢就可以。

因為類不大,我就直接貼源代碼上來,以便親們理解和我以下解說。

public class TemplateInterpreter {

    TerminalNodeProcessor[] terminalNodeProcessors = new TerminalNodeProcessor[200];

    Map<Class<ParserRuleContext>, ContextProcessor> contextProcessorMap = new HashMap<Class<ParserRuleContext>, ContextProcessor>();

    OtherTerminalNodeProcessor otherNodeProcessor = new OtherTerminalNodeProcessor();

 

 

    public void addTerminalNodeProcessor(TerminalNodeProcessor processor) {

        terminalNodeProcessors[processor.getType()] = processor;

    }

 

    public void addContextProcessor(ContextProcessor contextProcessor) {

        contextProcessorMap.put(contextProcessor.getType(), contextProcessor);

    }

 

    public TinyTemplateParser.TemplateContext parserTemplateTree(String sourceName, String templateString) {

        char[] source = templateString.toCharArray();

        ANTLRInputStream is = new ANTLRInputStream(source, source.length);

        // set source file name, it will be displayed in error report.

        is.name = sourceName;

        TinyTemplateParser parser = new TinyTemplateParser(new CommonTokenStream(new TinyTemplateLexer(is)));

        return parser.template();

    }

 

    public void interpret(TemplateEngineDefault engine, TemplateFromContext templateFromContext, String templateString, String sourceName, TemplateContext pageContext, TemplateContext context, Writer writer) throws Exception {

        interpret(engine, templateFromContext, parserTemplateTree(sourceName, templateString), pageContext, context, writer);

        writer.flush();

    }

 

    public void interpret(TemplateEngineDefault engine, TemplateFromContext templateFromContext, TinyTemplateParser.TemplateContext templateParseTree, TemplateContext pageContext, TemplateContext context, Writer writer) throws Exception {

        for (int i = 0; i < templateParseTree.getChildCount(); i++) {

            interpretTree(engine, templateFromContext, templateParseTree.getChild(i), pageContext, context, writer);

        }

    }

 

    public Object interpretTree(TemplateEngineDefault engine, TemplateFromContext templateFromContext, ParseTree tree, TemplateContext pageContext, TemplateContext context, Writer writer) throws Exception {

        Object returnValue = null;

        if (tree instanceof TerminalNode) {

            TerminalNode terminalNode = (TerminalNode) tree;

            TerminalNodeProcessor processor = terminalNodeProcessors[terminalNode.getSymbol().getType()];

            if (processor != null) {

                returnValue = processor.process(terminalNode, context, writer);

            } else {

                returnValue = otherNodeProcessor.process(terminalNode, context, writer);

            }

        } else if (tree instanceof ParserRuleContext) {

            ContextProcessor processor = contextProcessorMap.get(tree.getClass());

            if (processor != null) {

                returnValue = processor.process(this, templateFromContext, (ParserRuleContext) tree, pageContext, context, engine, writer);

            }

            if (processor == null || processor != null && processor.processChildren()) {

                for (int i = 0; i < tree.getChildCount(); i++) {

                    Object value = interpretTree(engine, templateFromContext, tree.getChild(i), pageContext, context, writer);

                    if (value != null) {

                        returnValue = value;

                    }

                }

            }

 

        } else {

            for (int i = 0; i < tree.getChildCount(); i++) {

                Object value = interpretTree(engine, templateFromContext, tree.getChild(i), pageContext, context, writer);

                if (returnValue == null && value != null) {

                    returnValue = value;

                }

            }

        }

        return returnValue;

    }

 

    public static void write(Writer writer, Object object) throws IOException {

        if (object != null) {

            writer.write(object.toString());

            writer.flush();

        }

    }

}

這個類,所以行數是80行。去掉15行的import和package,也就是65行而已。從類的職能來看。主要完畢例如以下事宜:
  1. 管理了TerminalNodeProcessor和ParserRuleContext
  2. parserTemplateTree:解析文本內容獲取語法樹
  3. interpret:解釋運行語法樹
  4. interpret:遍歷全部節點並解釋運行之
  5. interpretTree:假設是TerminalNode那麽找到合適的TerminalNode運行器去運行。假設找不到,則由OtherTerminalNodeProcessor去處理--實際上就是返回字符串了。假設是ParserRuleContext節點,那麽就由相應的運行器去運行。運行完了看看是不是要運行子節點。假設須要,那麽就繼續運行子節點,否則就返回。假設這兩種都不是,那就遍歷全部子節點去解釋運行了。

所以邏輯還是比較清晰。最復雜的核心算法也僅僅有30行,無論是什麽樣層級的同學。看這些代碼都沒有不論什麽難度了。

須要交待的一件事情是:為什麽ContextProcessor的處理類是用Map保存的。而TerminalNodeProcessor則是用數組?這裏主要是為了考慮到TerminalNode都有一個類型。用數據的方式速度更快一些。

上面說到有兩個接口,一個是處理TerminalNodeProcessor,另外一個是處理ContextProcessor的。以下交待一下這兩個接口。

TerminalNodeProcessor

public interface TerminalNodeProcessor<T extends ParseTree> {

    int getType();

    Object process(T parseTree, TemplateContext context, Writer writer) throws Exception;

}


getType:用於返回處理器可處理的類型。用於解釋引擎檢查是不是你的菜
  1. process:真正的處理邏輯實現的地方

ContextProcessor

public interface ContextProcessor<T extends ParserRuleContext> {

    Class<T> getType();

 

    boolean processChildren();

 

    Object process(TemplateInterpreter interpreter, TemplateFromContext templateFromContext, T parseTree, TemplateContext pageContext, TemplateContext context, TemplateEngineDefault engine, Writer writer) throws Exception;

 

}


  1. getType:用於返回處理器可處理的類型,用於解釋引擎檢查是不是你的菜
  2. processChildren:用於告訴引擎,你的兒子們是自己處理好了,還是讓解釋引擎繼續運行。返回true表示讓引擎繼續處理
  3. process:真正的處理邏輯實現的地方

至此,整個解析引擎的框架就搭好了。剩下要做的就是去寫這些處理器了。

TerminalNodeProcessor實現類演示樣例

事實上這些實現類真的太簡單了。我都不好意思貼出來。為了讓大家看明確。貼幾個說說意思就好

DoubleNodeProcessor
public class DoubleNodeProcessor implements TerminalNodeProcessor<TerminalNode> {

    public int getType() {

        return TinyTemplateParser.FLOATING_POINT;

    }

 

    public boolean processChildren() {

        return false;

    }

 

    public Object process(TerminalNode terminalNode, TemplateContext context, Writer writer) {

        String text=terminalNode.getText();

        return Double.parseDouble(text);

    }

}


這貨的意思是:假設是Double類型的數據。就把字符串轉換成Double值返回。

StringDoubleNodeProcessor

public class StringDoubleNodeProcessor implements TerminalNodeProcessor<TerminalNode> {

    public int getType() {

        return TinyTemplateParser.STRING_DOUBLE;

    }

    public boolean processChildren() {

        return false;

    }

    public Object process(TerminalNode terminalNode, TemplateContext context, Writer writer) {

        String text=terminalNode.getText();

        text=text.replaceAll("\\\\\"","\"");

        text=text.replaceAll("[\\\\][\\\\]","\\\\");

        return text.substring(1, text.length() - 1);

    }

}

這貨的意思是,假設是雙引號引住的字符串,那麽就把裏面的一些轉義字符處理掉,然後把外面的雙引號也去掉後返回。

其他的和這個大同小異,總之很easy。想看的同學能夠自己去看源代碼。這裏就不貼了。

ContextProcessor類的實現演示樣例

這裏面的處理,說實際的也沒有什麽復雜的,主要原因是原來在寫模板引擎的時候,把執行時的一些東西。進行良好的抽象,因此這裏僅僅是個簡單的調用而已。這裏貼2個略微復雜的示範一下:

ForProcessor

public class ForProcessor implements ContextProcessor<TinyTemplateParser.For_directiveContext> {

 

    public Class<TinyTemplateParser.For_directiveContext> getType() {

        return TinyTemplateParser.For_directiveContext.class;

    }

    public boolean processChildren() {

        return false;

    }

    public Object process(TemplateInterpreter interpreter, TemplateFromContext templateFromContext, TinyTemplateParser.For_directiveContext parseTree, TemplateContext pageContext, TemplateContext context, TemplateEngineDefault engine, Writer writer) throws Exception {

        String name = parseTree.for_expression().IDENTIFIER().getText();

        Object values = interpreter.interpretTree(engine, templateFromContext, parseTree.for_expression().expression(),pageContext, context, writer);

        ForIterator forIterator = new ForIterator(values);

        context.put("$"+name + "For", forIterator);

        boolean hasItem = false;

        while (forIterator.hasNext()) {

            TemplateContext forContext=new TemplateContextDefault();

            forContext.setParent(context);

            hasItem = true;

            Object value = forIterator.next();

            forContext.put(name, value);

            try {

                interpreter.interpretTree(engine, templateFromContext, parseTree.block(),pageContext, forContext, writer);

            } catch (ForBreakException be) {

                break;

            } catch (ForContinueException ce) {

                continue;

            }

        }

        if (!hasItem) {

            TinyTemplateParser.Else_directiveContext elseDirectiveContext = parseTree.else_directive();

            if (elseDirectiveContext != null) {

                interpreter.interpretTree(engine, templateFromContext, elseDirectiveContext.block(), pageContext,context, writer);

            }

        }

        return null;

    }

}

這裏解釋一下它的運行邏輯:

  1. 首先獲取循環變量名
  2. 接下來獲取要循環的對象
  3. 然後構建一個循環叠代器,並在上下文中放一個循環變量進去
  4. 然後真正運行循環。假設有在循環過程中有break或continue指令,那麽就運行之
  5. 假設最後一個循環也沒有運行。那麽檢查 else 指令是否存在。假設存在就運行之

是不是很easy?

MapProcessor

public class MapProcessor implements ContextProcessor<TinyTemplateParser.Expr_hash_mapContext> {

    public Class<TinyTemplateParser.Expr_hash_mapContext> getType() {

        return TinyTemplateParser.Expr_hash_mapContext.class;

    }

    public boolean processChildren() {

        return false;

    }

    public Object process(TemplateInterpreter interpreter, TemplateFromContext templateFromContext, TinyTemplateParser.Expr_hash_mapContext parseTree, TemplateContext pageContext, TemplateContext context, TemplateEngineDefault engine, Writer writer) throws Exception {

        List<TinyTemplateParser.ExpressionContext> expressions = parseTree.hash_map_entry_list().expression();

        List<TinyTemplateParser.ExpressionContext> expressionContexts = expressions;

        Map<String, Object> map = new HashMap<String, Object>();

        if (expressions != null) {

            for (int i = 0; i < expressions.size(); i += 2) {

                String key = interpreter.interpretTree(engine, templateFromContext, expressions.get(i), pageContext,context, writer).toString();

                Object value = interpreter.interpretTree(engine, templateFromContext, expressions.get(i + 1),pageContext, context, writer);

                map.put(key, value);

            }

        }

        return map;

    }

}


這個是個構建MAP的處理器。它的運行邏輯是:

  1. 新建個MAP對象。然後循環往MAP裏put數據即能夠了。
  2. 最後返回map對象

我已經拿了最復雜的兩個來講了,其他的就更簡單了。因此就不再貼了,關心的同學們能夠去看源碼。

總結

  1. 實際上用Java寫個新的語言啥的。沒有什麽難的,難的是你心頭的那種恐懼,畢竟如今的一些開源框架如Antlr等的支持下,做詞法分析,語法樹構建是很easy的一件事情,僅僅要規劃並定義好語法規則,後面的實現並沒有多復雜。
  2. 好的設計會讓你受益頗多,Tiny模板引擎由編譯換成解釋運行。沒有什麽傷筋動骨的變化,僅僅是用新的方式實現了原有接口而已
  3. 對問題的分析的深入程度決定了你代碼編寫的復雜程度,上次和一個人討論時有說過:之所以你寫不簡單,是由於你考慮得還不夠多。分析的還不夠細
  4. 至此此次重構完畢,正在測試其中,將在近日推出。


歡迎訪問開源技術社區:http://bbs.tinygroup.org

本例涉及的代碼和框架資料。將會在社區分享。《自己動手寫框架》成員QQ群:228977971,一起動手。了解開源框架的奧秘!或點擊增加QQ群:http://jq.qq.com/?_wv=1027&k=d0myfX

《開源框架那點事兒25》:對框架模板引擎實現方式的改造實錄