首页 > 代码库 > 《开源框架那点事儿25》:对框架模板引擎实现方式的改造实录

《开源框架那点事儿25》:对框架模板引擎实现方式的改造实录

点滴悟透设计思想,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 = http://www.mamicode.com/null;>

 

 
这个类,所以行数是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 = http://www.mamicode.com/forIterator.next();>

这里解释一下它的运行逻辑: 

  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 = http://www.mamicode.com/interpreter.interpretTree(engine, templateFromContext, expressions.get(i + 1),pageContext, context, writer);>


 

 

这个是个构建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》:对框架模板引擎实现方式的改造实录