點滴悟透設計思想,Tiny模板引擎優化實錄!
Tiny模板引擎的實現方式原來是採用的編譯方式,最近發生了一些問題,因此我覺得有必要把編譯方式調整爲解釋方式,爲此就開始了此次實現活動。
編譯方式存在的問題
當時採用編譯方式,主要是考慮到編譯方式在運行時不必再去遍歷語法樹,因此就採用了編譯方式。但是在實際應用當中,出現瞭如下問題:
文件路徑衝突的問題
由於採用的是編譯方式,這個時候就存在在一個選擇,即:Java源代碼落地或不落地的選擇。如果Java文件不落地,則在有問題的時候,如果想要進行代碼調試(雖然這種場景並不多見),那麼就沒有源代碼可供調試。如果Java代碼落地,則會存在一個問題,那就是資源文件在磁盤文件中產生衝突的問題。
同樣的問題對於class文件也存在,如果不落地,那麼每次應用重啓動的時候,都要重新編譯這些文件以產生class文件;如果落地,則也會產生衝突的問題。
當然,Tiny模板引擎通過增加一個配置項,解決了這個衝突的問題,但是由於增加了一個配置項,從客觀上增加了維護人員的工作量,也容易造成當維護人員不瞭解這裏面的道道,忘記設置從而導致在一臺服務器中部署多個Tiny應用時多個應用中的模板文件生成的java文件和class文件的衝突,從而導致出現問題。
PermSize內存佔用問題
採用編譯方式的時候,由於每個模板文件都要生成一個類,每個宏也要生成一個類,在宏調用過程中,也要生成一些類。(本來是可以不生成這些類的,但是由於Tiny模板引擎支持了一些非常有用的特性,所以宏調用時時採用編譯方式,就要生成一些內嵌類來完成)。這樣,就會生成大量的Java類,從工程非常大的時候,就會導致PermSize戰勝非常大。尤其是在系統還在調試的時候,模板文件變化的時候,就要重新編譯生成新的類,爲了避免必須重新啓動應用服務器才能生生效,因此採用了自己編寫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模板引擎定製編寫的,因此如果有同學要拿來改造,請照葫蘆畫瓢即可。由於類不大,我就直接貼源碼上來,以便親們理解和我下面講解。
[java] view plaincopyprint?
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();
}
}
}
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行而已,從類的職能來看,主要完成如下事宜:
管理了TerminalNodeProcessor和ParserRuleContext
parserTemplateTree:解析文本內容獲取語法樹
interpret:解釋執行語法樹
interpret:遍歷所有節點並解釋執行之
interpretTree:如果是TerminalNode那麼找到合適的TerminalNode執行器去執行,如果找不到,則由OtherTerminalNodeProcessor去處理--實際上就是返回字符串了;如果是ParserRuleContext節點,那麼就由對應的執行器去執行,執行完了看看是不是要執行子節點,如果需要,那麼就繼續執行子節點,否則就返回。如果這兩種都不是,那就遍歷所有子節點去解釋執行了。
所以邏輯還是比較清晰,最複雜的核心算法也只有30行,不管是什麼樣層級的同學,看這些代碼都沒有任何難度了。
需要交待的一件事情是:爲什麼ContextProcessor的處理類是用Map保存的,而TerminalNodeProcessor則是用數組?這裏主要是爲了考慮到TerminalNode都有一個類型,用數據的方式速度更快一些。
上面說到有兩個接口,一個是處理TerminalNodeProcessor,另外一個是處理ContextProcessor的,下面交待一下這兩個接口。
TerminalNodeProcessor
[java] view plaincopyprint?
public interface TerminalNodeProcessor<T extends ParseTree> {
int getType();
Object process(T parseTree, TemplateContext context, Writer writer) throws Exception;
}
public interface TerminalNodeProcessor<T extends ParseTree> { int getType(); Object process(T parseTree, TemplateContext context, Writer writer) throws Exception; }
getType:用於返回處理器可處理的類型,用於解釋引擎檢查是不是你的菜
process:真正的處理邏輯實現的地方
ContextProcessor
[java] view plaincopyprint?
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;
}
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; }
getType:用於返回處理器可處理的類型,用於解釋引擎檢查是不是你的菜
processChildren:用於告訴引擎,你的兒子們是自己處理好了,還是讓解釋引擎繼續執行。返回true表示讓引擎繼續處理
process:真正的處理邏輯實現的地方
至此,整個解析引擎的框架就搭好了,剩下要做的就是去寫這些處理器了。
TerminalNodeProcessor實現類示例
其實這些實現類真的太簡單了,我都不好意思貼出來,爲了讓大家看明白,貼幾個說說意思就好
DoubleNodeProcessor
[java] view plaincopyprint?
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);
}
}
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
[java] view plaincopyprint?
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);
}
}
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); } }
這貨的意思是,如果是雙引號引住的字符串,那麼就把裏面的一些轉義字符處理掉,然後把外面的雙引號也去掉後返回。
其它的和這個大同小異,總之非常簡單,想看的同學可以自己去看源碼,這裏就不貼了。
ContextProcessor類的實現示例
這裏面的處理,說實際的也沒有什麼複雜的,主要原因是原來在寫模板引擎的時候,把運行時的一些東西,進行良好的抽象,因此這裏只是個簡單的調用而已。這裏貼2個稍微複雜的示範一下:
ForProcessor
[java] view plaincopyprint?
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;
}
}
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; } }
這裏解釋一下它的執行邏輯:
首先獲取循環變量名
接下來獲取要循環的對象
然後構建一個循環迭代器,並在上下文中放一個循環變量進去
然後真正執行循環,如果有在循環過程中有break或continue指令,那麼就執行之
如果最後一個循環也沒有執行,那麼檢查 else 指令是否存在,如果存在就執行之
是不是非常簡單?
MapProcessor
[java] view plaincopyprint?
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;
}
}
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的處理器,它的執行邏輯是:
新建個MAP對象,然後循環往MAP裏put數據即可以了。
最後返回map對象
我已經拿了最複雜的兩個來講了,其它的就更簡單了,因此就不再貼了,關心的同學們可以去看源代碼。
總結
實際上用Java寫個新的語言啥的,沒有什麼難的,難的是你心頭的那種恐懼,畢竟現在的一些開源框架如Antlr等的支持下,做詞法分析,語法樹構建是非常容易的一件事情,只要規劃並定義好語法規則,後面的實現並沒有多複雜。
好的設計會讓你受益頗多,Tiny模板引擎由編譯換成解釋執行,沒有什麼傷筋動骨的變化,只是用新的方式實現了原有接口而已
對問題的分析的深入程度決定了你代碼編寫的複雜程度,上次和一個人討論時有說過:之所以你寫不簡單,是因爲你考慮得還不夠多,分析的還不夠細
至此此次重構完成,正在測試當中,將在近日推出。