澳门新葡萄京官网首页 21

Velocity引擎原理探究

前言

不知不觉就很长时间没造过什么轮子了,以前一直想自己实现一个模板引擎,只是没付诸于行动,最近终于在业余时间里抽了点时间写了一下。因为我们的项目大部分用的是
swig 或者 nunjucks ,于是就想实现一个类似的模板引擎。

至于为什么要做这么一个东西?基本上每一个做前端的人都会有自己的一个框架梦,而一个成熟的前端框架,模板编译能力就是其中很重要的一环,虽然目前市面上的大部分框架
vue、angular 这些都是属于 dom base 的,而 swig nunjucks ejs这些都是属于
string base
的,但是其实实现起来都是差不多的。不外乎都是 Template =parse=> Ast =render=>String

再者,做一个模板引擎,个人感觉还是对自身的编码能力的提升还是很有帮助的,在性能优化、正则、字符解析上尤为明显。在日后的业务需求中,如果有一些需要解析字符串相关的需求,也会更得心应手。

小刘是一名出色的软件工程师,能流畅的使用5种编程语言打印 hello
world。一天他的准岳父(养老院院长)找到他,拜托他一件事:教养老院的老人们编程,不用太难,体验一把思想就行了

一、前言

常见的Java模板引擎有JSP、Freemark,Velocity。在MVC三层框架中,模板引擎属于view层,实质是把model层内容展现到前台页面的一个引擎,velocity以其前后端解耦使前后台可以同时开发和其语法的简易性得到了广泛的应用,集团WebX框架就建议使用它作为模板引擎。

功能分析

一个模板引擎,在我看来,就是由两块核心功能组成,一个是用来将模板语言解析为
ast(抽象语法树)。还有一个就是将 ast 再编译成 html。

先说明一下 ast 是什么,已知的可以忽略。

抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax
tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于if-condition-then这样的条件跳转语句,可以使用带有两个分支的节点来表示。

在实现具体逻辑之前,先决定要实现哪几种 tag
的功能,在我看来,forif elsesetraw还有就是基本的变量输出,有了这几种,模板引擎基本上也就够用了。除了
tag,还有就是 filter 功能也是必须的。

澳门新葡萄京官网首页 1

二、原理

构建 AST

我们需要把模板语言解析成一个又一个的语法节点,比如下面这段模板语言:

<div>
    {% if test > 1 %}
        {{ test }}
    {% endif %}
</div>

很明显,div 将会被解析为一个文本节点,然后接着是一个块级节点 if ,然后
if 节点下又有一个变量子节点,再之后有是一个 的文本节点,用 json
来表示这个模板解析成的 ast 就可以表示为:

[
    {
        type: 1,
        text: '<div>'
    },
    {
        type: 2,
        tag: 'if',
        item: 'test > 1',
        children: [{
           type: 3,
           item: 'test'
        }]
    },
    {
        type: 1,
        text: '</div>'
    }
]

基本上就分成三种类型了,一种是普通文本节点,一种是块级节点,一种是变量节点。那么实现的话,就只需要找到各个节点的文本,并且抽象成对象即可。一般来说找节点都是根据模板语法来找,比如上面的块级节点以及变量节点的开始肯定是{%或者{{,那么就可以从这两个关键字符下手:

...
const matches = str.match(/{{|{%/);
const isBlock = matches[0] === '{%';
const endIndex = matches.index;
...

通过上面一段代码,就可以获取到处于文本最前面的{{或者{%位置了。

既然获取到了第一个非文本类节点的位置,那么该节点位置以前的,就都是文本节点了,因此就已经可以得到第一个节点,也就是上面的<div>了。

获取到 div
文本节点后,我们也可以知道获取到的第一个关键字符是{%,也就是上面的endIndex是我们要的索引,记得要更新剩余的字符,直接通过
slice 更新即可:

// 2 是 {% 的长度
str = str.slice(endIndex + 2);

而此时我们就可以知道匹配到的当前关键字符是{%,那么他的闭合处就肯定是%},因此就可以再通过

const expression = str.slice(0, str.indexOf('%}'))

获取到 if test > 1 这个字符串了。然后我们再通过正则/^ifs+([sS]+)$/匹配,就可以知道这个字符串是
if
的标签,同时可以获得test > 1这一个捕获组,然后就可以创建我们的第二个节点,if
的块级节点了。

因为 if
是个块级节点,那么继续往下匹配的时候,在遇到 {% endif %} 之前的所有节点,都是属于
if
节点的子节点,所以我们在创建节点时要给它一个children数组属性,用来保存子节点。

紧接着再重复上面的操作,获取下一个{%以及{{的位置,跟上面的逻辑差不多,获取到{{的位置后再判断}}的位置,就可以创建第三个节点,test
的变量节点,并且 push 到 if 节点的子节点列表中。

创建完变量节点后继续重复上述操作,就能够获取到{% endif %}这个闭合节点,当遇到该节点之后的节点,就不能保存到
if 节点的子节点列表中了。紧接着就又是一个文本节点。

相对比较完整的实现如下:

const root = [];
let parent;
function parse(str){
    const matches = str.match(/{{|{%/);
    const isBlock = matches[0] === '{%';
    const endIndex = matches.index;

    const chars = str.slice(0, matches ? endIndex : str.length);
    if(chars.length) {
     ...创建文本节点 
    }

    if(!matches) return;

    str = str.slice(endIndex + 2);
    const leftStart = matches[0];
    const rightEnd = isBlock ? '%}' : '}}';
    const rightEndIndex = str.indexOf(rightEnd);
    const expression = str.slice(0, rightEndIndex)

    if(isBlock) {
        ...创建块级节点 el

        parent = el;
    } else {
        ...创建变量节点 el
    }

    (parent ? parent.children : root).push(el);
    parse(str.slice(rightEndIndex + 2));
}

当然,具体实现起来还是有其他东西要考虑的,比如一个文本是{% {{ test }},就要考虑到{%的干扰等。还有比如
else 还有 elseif 节点的处理,这两个是需要关联到 if
标签上的,这个也是需要特殊处理的。不过大概逻辑基本上就是以上。

院长,别说了,拔刀吧

2.1 架构介绍

澳门新葡萄京官网首页 2

screenshot.png

打开velocity的源码包,从代码结构看velocity主要包括app、context、runtime、event、texen和一些util类

** 1)、app模块 **
源码org.apache.velocity.app下面主要有两个类Velocity和VelocityEngine。

  • Velocity
    ,主要对外提供一些static方法,可以通过类名直接调用,只要通过Velocity创建一个模块,在创建一个存放变量的context,就可以渲染,如下:

澳门新葡萄京官网首页 3

screenshot.png

另外Velocity功能是委托给RuntimeInstance来具体实现的,并且维护的是一个单件实例,就是说在同一个jvm中,只有一个Velocity的实例,这给资源共享和配置本地化带来的方便,这为在通一个JVM的不同应用见共享模块提供了方便。

  • **VelocityEngine **
    ,相比于Velocity提供了更加强大的功能,框架开发者一般使用这个类在框架中使用velocity模板渲染功能,内部也是是委托给RuntimeInstance来具体实现的,但是每个VelocityEngine都有一个自己的RuntimeInstance实例。也就是说在一个JVM中可以有多个VelocityEngine实例,每个实例都可以定制化自己的配置,这为在同一个应用中配置不同的模板路径和logger提供了方便。
    例如springmvc中初始化一个veloctiy引擎方式如下:

澳门新葡萄京官网首页 4

screenshot.png

** 2)、Context模块 **
源码org.apache.velocity.context包下的Context,AbstractContext,还有org.apache.velocity下的VelocityContext。主要功能是提供对模板渲染所需要的变量的封装管理.

澳门新葡萄京官网首页 5

screenshot.png

Context设计目的:

  • 作为一个适配器,便于与其他框架集成
    例如SpringMVC传递参数的是一个Map的数据结构,那么如果springmvc中使用velocity则需要把map里面存放的变量适配到context中,这个是直接把map作为VelocityContext构造函数参数适配的。但是webx使用的是自己的context,PullableMappedContext存放变量,那么就需要继承velocity的AbstractContext实现一个适配器TemplateContextAdapter来把自己的context转换为velocity所需要的context.
  • Velocity内部数据隔离,Velocity不同模块通过传递参数方式进行处理,利于模块之间的解耦。

** 3)、RunTime模块 **
源码org.apache.velocity.runtime包下:
澳门新葡萄京官网首页 ,负责加载模板文件,解析为JavaCC语法树,使用深度遍历算法渲染语法书节点,生成渲染结果。

** 4)、RuntimeInstance **
负责解析模板文件为AST结构,velocity和velocityengine内部都是委托给它来实现功能。

** 5)、util模块 **
一些工具类,例如SimplePool是一个对象池,里面默认缓存20个Parser。CalssUtiles是一个简单的从classloader操作类和资源的函数类。

组合 html

创建好 ast 后,要渲染 html
的时候,就只需要遍历语法树,根据节点类型做出不同的处理即可。

比如,如果是文本节点,就直接html += el.text即可。如果是if节点,则判断表达式,比如上面的test > 1,有两种办法可以实现表达式的计算,一种就是eval,还有一种就是new Function了,eval
会有安全性问题,因此就不考虑了,而是使用new Function的方式来实现。变量节点的计算也一样,用new Function来实现。

封装后具体实现如下:

function computedExpression(obj, expression) {
  const methodBody = `return (${expression})`;
  const funcString = obj ? `with(__obj__){ ${methodBody} }` : methodBody;
  const func = new Function('__obj__', funcString);
  try {
    let result = func(obj);
    return (result === undefined || result === null) ? '' : result;
  } catch (e) {
    return '';
  }
}

使用 with ,可以让在 function 中执行的语句关联对象,比如

with({ a: '123' }) {
    console.log(a); // 123
}

虽然 with 不推荐在编写代码的时候使用,因为会让 js
引擎无法对代码进行优化,但是却很适合用来做这种模板编译,会方便很多。包括
vue 中的 render function 也是用 with 包裹起来的。不过 nunjucks 是没有用
with 的,它是自己来解析表达式的,因此在 nunjucks
的模板语法中,需要遵循它的规范,比如最简单的条件表达式,如果用 with
的话,直接写{{ test ? 'good' : 'bad' }},但是在 nunjucks
中却要写成�{{ 'good' if test else 'bad' }}

anyway,各有各的好吧。

小刘内心是拒绝的,一些不可抗拒的原因,让他强忍住内心的翻滚,“嗯,没啥问题”。小刘神志有些恍惚,准备离开院长办公室。院长补充道:“对了小刘,老人们都不会英语,但ABCD还是认识的,这个你知道的吧,知道的吧
吧 吧 …”

2.2 源码分析

实现多级作用域

在将 ast 转换成 html
的时候,有一个很常见的场景就是多级作用域,比如在一个 for
循环中再嵌套一个 for
循环。而如何在做这个作用域分割,其实也是很简单,就是通过递归。

比如我的对一个 ast
树的处理方法命名为:processAst(ast, scope),再比如最初的 scope 是

{ 
  list: [
   { subs: [1, 2, 3] },
   { subs: [4, 5, 6] } 
  ] 
 }

那么 processAst 就可以这么实现:

function processAst(ast, scope) {
    ...
    if(ast.for) {
        const list = scope[ast.item]; // ast.item 自然就是列表的 key ,比如上面的 list
        list.forEach(item => {
            processAst(ast.children, Object.assign({}, scope, {
                [ast.key]: item,  // ast.key 则是 for key in list 中的 key
            }))
        })
    }
    ...
}

就简单通过一个递归,就可以把作用域一直传递下去了。

澳门新葡萄京官网首页 6

2.2.1 试验准备

pom中添加velocity依赖

<dependency>
  <groupId>velocity-tools</groupId>
  <artifactId>velocity-tools-generic</artifactId>
  <version>1.4</version>
</dependency>

测试java代码:

public static void main(String[] args) {

try {
    // 初始化(1)
    Velocity.init("velocity.properties");

    // 创建context,存放变量(2)
    VelocityContext context = new VelocityContext();
    Person person = new Person();
    person.setName("jiaduo");
    context.put("person", person);

    // 加载模板文件到内存(3)
    Template template = null;
    String templateFile = "healthview.vm";
    template = Velocity.getTemplate(templateFile);

    // 渲染(4)
    StringWriter stringWriter = new StringWriter();
    template.merge(context, stringWriter);

    // 打印结果
    System.out.println(stringWriter.toString());

} catch (Exception e) {
    e.printStackTrace();
}
}

healthview.vm内容:

<html>
   <div>$!{person.sayHello()}:$!{person.name}</div>
</html>

velocity.properties内容:

file.resource.loader.path = /Users/zhuizhumengxiang/workspace/mytool/SpringLean/src/

Filter 功能实现

实现上面功能后,组件就已经具备基本的模板渲染能力,不过在用模板引擎的时候,还有一个很常用的功能就是
filter 。一般来说 filter
的使用方式都是这这样 {{ test | filter1 | filter2 }},这个的实现也说一下,这一块的实现我参考了
vue 的解析的方式,还是蛮有意思的。

还是举个例子:

{{ test | filter1 | filter2 }}

在构建 AST
的时候,就可以获取到其中的test | filter1 | filter2,然后我们可以很简单的就获取到
filter1 和 filter2 这两个字符串。起初我的实现方式,是把这些 filter
字符串扔进 ast 节点的 filters 数组中,在渲染的时候再一个一个拿出来处理。

不过后来又觉得为了性能考虑,能够在 AST
阶段就能做完的工作就不要放到渲染阶段了。因此就改成 vue
的方法组合方式。也就是把上面字符串变成:

_$f('filter2', _$f('filter1', test))

预先用个方法包裹起来,在渲染的时候,就不需要再通过循环去获取 filter
并且执行了。具体实现如下:

const filterRE = /(?:|s*w+s*)+$/;
const filterSplitRE = /s*|s*/;
function processFilter(expr, escape) {
  let result = expr;
  const matches = expr.match(filterRE);
  if (matches) {
    const arr = matches[0].trim().split(filterSplitRE);
    result = expr.slice(0, matches.index);

    // add filter method wrapping
    utils.forEach(arr, name => {
      if (!name) {
        return;
      }

      // do not escape if has safe filter
      if (name === 'safe') {
        escape = false;
        return;
      }

      result = `_$f('${name}', ${result})`;
    });
  }

  return escape ? `_$f('escape', ${result})` : result;
}

上面还有一个就是对 safe 的处理,如果有 safe 这个 filter ,就不做 escape
了。完成这个之后,有 filter 的 variable
都会变成_$f('filter2', _$f('filter1', test))这种形式了。因此,此前的
computedExpression 方法也要做一些改造了。

function processFilter(filterName, str) {
  const filter = filters[filterName] || globalFilters[filterName];

  if (!filter) {
    throw new Error(`unknown filter ${filterName}`);
  }

  return filter(str);
}

function computedExpression(obj, expression) {
  const methodBody = `return (${expression})`;
  const funcString = obj ? `with(_$o){ ${methodBody} }` : methodBody;
  const func = new Function('_$o', '_$f', funcString);
  try {
    const result = func(obj, processFilter);
    return (result === undefined || result === null) ? '' : result;
  } catch (e) {
    // only catch the not defined error
    if (e.message.indexOf('is not defined') >= 0) {
      return '';
    } else {
      throw e;
    }
  }
}

其实也是很简单,就是在 new Function 的时候,多传入一个获取 filter
的方法即可,然后有 filter 的 variable 就能被正常识别解析了。

至此,AST 构建、AST 到 html 的转换、多级作用域以及 Filter
的实现,都已经基本讲解完成。

贴一下自己实现的一个模板引擎轮子:

算是实现了大部分模板引擎该有的功能,欢迎各路豪杰 star 。

三天狂风暴雨般的发泄后,憔悴的小刘坐在台阶上吸着烟,开始想怎么去做这个事。有两种方案:

2.2.2 源码分析

先看下(1)Velocity.init()时序图:

澳门新葡萄京官网首页 7

screenshot.png

从时序图可知Velocity是委托给RuntimeInstance去实现初始化工作,RuntimeSingleton则是保证RuntimeInstance的单例。init里面首先解析用户传递的配置文件,然后解析:

澳门新葡萄京官网首页 8

screenshot.png

最后使用用户配置文件配置项覆盖默认配置项。

public synchronized void init()
    {
        if (!initialized && !initializing)
        {
            log.debug("Initializing Velocity, Calling init()...");
            initializing = true;

            log.trace("*******************************************************************");
            log.debug("Starting Apache Velocity v1.7 (compiled: 2010-11-19 12:14:37)");
            log.trace("RuntimeInstance initializing.");

            initializeProperties();//配置文件解析
            initializeLog();//初始化日志
            initializeResourceManager();//初始化资源管理器和资源加载器
            initializeDirectives();//初始化Directives
            initializeEventHandlers();// 初始化事件处理器
            initializeParserPool();//初始化解析器 对象池

            initializeIntrospection();// 初始化自省
            initializeEvaluateScopeSettings();
            /*
             *  initialize the VM Factory.  It will use the properties
             * accessable from Runtime, so keep this here at the end.
             */
            vmFactory.initVelocimacro();

            log.trace("RuntimeInstance successfully initialized.");

            initialized = true;
            initializing = false;
        }
    }

initializeResourceManager的代码逻辑:

 private void initializeResourceManager()
    {
        /*
         * org.apache.velocity.runtime.resource.ResourceManagerImpl
         */      
        String rm = getString(RuntimeConstants.RESOURCE_MANAGER_CLASS);

        if (rm != null && rm.length() > 0)
        {

            Object o = null;
            //创建资源管理器实例
            try
            {
               o = ClassUtils.getNewInstance( rm );
            }
            ...

            resourceManager = (ResourceManager) o;
            //初始化资源管理器
            resourceManager.initialize(this);
         }
        ...
 }

//初始化资源管理器
public synchronized void initialize(final RuntimeServices rsvc)
{   ...
    ResourceLoader resourceLoader = null;

    this.rsvc = rsvc;
    log = rsvc.getLog();

    log.trace("Default ResourceManager initializing. (" + this.getClass() + ")");

    assembleResourceLoaderInitializers();

    //创建资源加载器
    for (Iterator it = sourceInitializerList.iterator(); it.hasNext();)
    {
        /**
         * Resource loader can be loaded either via class name or be passed
         * in as an instance.
         */
        ExtendedProperties configuration = (ExtendedProperties) it.next();

        String loaderClass = StringUtils.nullTrim(configuration.getString("class"));
        ResourceLoader loaderInstance = (ResourceLoader) configuration.get("instance");

        if (loaderInstance != null)
        {
            resourceLoader = loaderInstance;
        }
        else if (loaderClass != null)
        {
            resourceLoader = ResourceLoaderFactory.getLoader(rsvc, loaderClass);
        }
        ...
        resourceLoader.commonInit(rsvc, configuration);
        resourceLoader.init(configuration);
        resourceLoaders.add(resourceLoader);
    }


    //org.apache.velocity.runtime.resource.ResourceCacheImpl
    String cacheClassName = rsvc.getString(RuntimeConstants.RESOURCE_MANAGER_CACHE_CLASS);

    Object cacheObject = null;
    //创建缓存实例
    if (org.apache.commons.lang.StringUtils.isNotEmpty(cacheClassName))
    {
        try
        {
            cacheObject = ClassUtils.getNewInstance(cacheClassName);
        }
        ...
    }

    /*
     *  if we didn't get through that, just use the default.
     */
    if (cacheObject == null)
    {
        cacheObject = new ResourceCacheImpl();
    }

    globalCache = (ResourceCache) cacheObject;
    //初始化缓存
    globalCache.initialize(rsvc);

}

 //初始化缓存
 public void initialize( RuntimeServices rs )
{
    rsvc = rs;
    //默认配置文件里没这个变量,所以默认最多缓存89个模板文件
    int maxSize =
        rsvc.getInt(RuntimeConstants.RESOURCE_MANAGER_DEFAULTCACHE_SIZE, 89);
    if (maxSize > 0)
    {
        // Create a whole new Map here to avoid hanging on to a
        // handle to the unsynch'd LRUMap for our lifetime.
        Map lruCache = Collections.synchronizedMap(new LRUMap(maxSize));
        lruCache.putAll(cache);
        cache = lruCache;
    }
    rsvc.getLog().debug("ResourceCache: initialized ("+this.getClass()+") with "+
           cache.getClass()+" cache map.");
}

initializeParserPool逻辑,目的应该是为了提高性能:

 private void initializeParserPool()
    {
        /*
         * 配置中获取,org.apache.velocity.runtime.ParserPoolImpl
         */
        String pp = getString(RuntimeConstants.PARSER_POOL_CLASS);

        if (pp != null && pp.length() > 0)
        {

            Object o = null;

            try
            {//实例化
                o = ClassUtils.getNewInstance( pp );
            }
            ...

            parserPool = (ParserPool) o;
           //调用初始化方法,创建parser对象池
            parserPool.initialize(this);
        }
        ...

}
//创建Parser对象池
 public void initialize(RuntimeServices rsvc)
    {   //默认为20个
        max = rsvc.getInt(RuntimeConstants.PARSER_POOL_SIZE, RuntimeConstants.NUMBER_OF_PARSERS);
        pool = new SimplePool(max);

        for (int i = 0; i < max; i++)
        {
            pool.put(rsvc.createNewParser());
        }

        if (rsvc.getLog().isDebugEnabled())
        {
            rsvc.getLog().debug("Created '" + max + "' parsers.");
        }
}

initializeIntrospection的逻辑:

 private void initializeIntrospection()
    {//[org.apache.velocity.util.introspection.UberspectImpl]
        String[] uberspectors = configuration.getStringArray(RuntimeConstants.UBERSPECT_CLASSNAME);
        for (int i=0; i <uberspectors.length;i++)
        {
            String rm = uberspectors[i];
            Object o = null;

            try
            {
               o = ClassUtils.getNewInstance( rm );
            }
            。。。

            Uberspect u = (Uberspect)o;


            {
                if (u instanceof ChainableUberspector)
                {
                    ((ChainableUberspector)u).wrap(uberSpect);
                    uberSpect = u;
                }
                else
                {
                    uberSpect = new LinkingUberspector(uberSpect,u);
                }
            }
        }

        if(uberSpect != null)
        {
            uberSpect.init();
        }
       ...
    }

    public void init()
    {
        introspector = new Introspector(log);
    }

在看下(2)代码如下:

public Object put(String key, Object value)
    {

        if (key == null)
        {
            return null;
        }

        return internalPut(key.intern(), value);
    }

    public Object internalPut( String key, Object value )
    {  //context是一个HashMap
        return context.put( key, value );
    }

在看下(3)时序图

澳门新葡萄京官网首页 9

screenshot.png

从时序图知道首先去加载模板文件到内存,代码如下:

 public Resource getResource(final String resourceName, final int resourceType, final String encoding)
        throws ResourceNotFoundException,
            ParseErrorException
    {

        //先从缓存里面查找
        String resourceKey = resourceType + resourceName;
        Resource resource = globalCache.get(resourceKey);

        if (resource != null)
        {
            try
            {
                // 缓存命中,则看是否开定时从磁盘加载,定时到了则从磁盘加载
                if (resource.requiresChecking())
                {
                    //从磁盘加载
                    resource = refreshResource(resource, encoding);
                }
            }
            ...
        }
        else
        {
            try
            {
                //从磁盘加载  
                resource = loadResource(resourceName, resourceType, encoding);

                //开启了缓存,则放入缓存
                if (resource.getResourceLoader().isCachingOn())
                {
                    globalCache.put(resourceKey, resource);
                }
            }
           ...
        }

        //返回资源
        return resource;
    }

file.resource.loader.cache = false
file.resource.loader.modificationCheckInterval = 2
默认不开启缓存,CheckInterval = 2。

然后解析模板文件为ast结构

loadResource->()
{
    resource.process()
    {
        RuntimeInstance.parse();//解析模板文件为AST node结构
    }
}

public SimpleNode parse(Reader reader, String templateName, boolean dumpNamespace)
        throws ParseException
    {
        requireInitialization();

        Parser parser = (Parser) parserPool.get();
        boolean keepParser = true;
        if (parser == null)
        {
            //没有可用的则创建
            if (log.isInfoEnabled())
            {
                log.info("Runtime : ran out of parsers. Creating a new one. "
                      + " Please increment the parser.pool.size property."
                      + " The current value is too small.");
            }
            parser = createNewParser();
            keepParser = false;
        }

        try
        {
            ...
            return parser.parse(reader, templateName);
        }
        finally
        {
            //如果从对象池获取则使用后归还
            if (keepParser)
            {
                parserPool.put(parser);
            }

        }
    }

目前template里面的data对应内容:

澳门新葡萄京官网首页 10

screenshot.png

再看下(4)时序图为:

澳门新葡萄京官网首页 11

screenshot.png

如图debug可知velocity把healthview.vm解析为了5段:

澳门新葡萄京官网首页 12

screenshot.png

画出ast树图如下:

澳门新葡萄京官网首页 13

screenshot.png

其中从左向右第一个节点是vm中 <html> <div>
解析为ASTText文本节点内容为:[<html> <div>]
第二个节点是对$!{person.sayHello()}的解析,是一个ASTReference节点,该节点有一个子节点ASTmethod,
第三个节点是对vm中:解析为ASTText文本节点内容为:[ :]
第四个节点是对vm中$!{person.name}的解析,是是一个ASTReference节点,该节点子节点是ASTIdentifier
第五个节点是VM中</div></html>的解析,解析为ASTText文本节点内容为:[</div></html>]]

ASTProcess的render方法是采用树的深度遍历算法来渲染节点的,具体代码:

   public boolean render( InternalContextAdapter context, Writer writer)
        throws IOException, MethodInvocationException, ParseErrorException, ResourceNotFoundException
    {
        int i, k = jjtGetNumChildren();

        for (i = 0; i < k; i++)
            jjtGetChild(i).render(context, writer);

        return true;
    }

不同类型子节点渲染方法不一样,下面看下ASTText类型,可知只是简单的把文本写入writer:

    public boolean render( InternalContextAdapter context, Writer writer)
        throws IOException
    {
        writer.write(ctext);
        return true;
    }

再看下有子节点ASTmethod的ASTReference的渲染:
ASTReference.render()

 public boolean render(InternalContextAdapter context, Writer writer) throws IOException,
            MethodInvocationException
    {
        ...
        {
          //执行execute方法
          value = execute(null, context);
        }

        String localNullString = null;
        ...
        value = EventHandlerUtil.referenceInsert(rsvc, context, literal, value);

        String toString = null;
        if (value != null)
        {          
            if (value instanceof Renderable)
            {
                Renderable renderable = (Renderable)value;
                try
                {
                    if (renderable.render(context,writer))
                      return true;
                }
                catch(RuntimeException e)
                {
                    // We commonly get here when an error occurs within a block reference.
                    // We want to log where the reference is at so that a developer can easily
                    // know where the offending call is located.  This can be seen
                    // as another element of the error stack we report to log.
                    log.error("Exception rendering "
                        + ((renderable instanceof Reference)? "block ":"Renderable ")
                        + rootString + " at " + Log.formatFileString(this));
                    throw e;
                }
            }

            toString = value.toString();
        }
        ...
        {
            //person.sayHello()结果写入writer
            writer.write(escPrefix);
            writer.write(morePrefix);
            writer.write(toString);

            return true;
        }
    }

 ASTReference.execute
 public Object execute(Object o, InternalContextAdapter context)
        throws MethodInvocationException
    {

        ...
        //获取person对象
        Object result = getVariableValue(context, rootString);

        try
        {
            Object previousResult = result; 
            int failedChild = -1;
            for (int i = 0; i < numChildren; i++)
            {
                ...
                previousResult = result;
                //递归解析,调用AstMethod.execute()反射调用person.sayHello();
                result = jjtGetChild(i).execute(result,context);
                if (result == null && !strictRef)  // If strict and null then well catch this
                                                   // next time through the loop
                {
                    failedChild = i;
                    break;
                }
            }

            ...



            return result;
        }
        catch(MethodInvocationException mie)
        {
            mie.setReferenceName(rootString);
            throw mie;
        }
    }

 public Object execute(Object o, InternalContextAdapter context)
        throws MethodInvocationException
    {

        Object [] params = new Object[paramCount];


        final Class[] paramClasses =       
            paramCount > 0 ? new Class[paramCount] : ArrayUtils.EMPTY_CLASS_ARRAY;

        for (int j = 0; j < paramCount; j++)
        {
            params[j] = jjtGetChild(j + 1).value(context);
            if (params[j] != null)
            {
                paramClasses[j] = params[j].getClass();
            }
        }

        VelMethod method = ClassUtils.getMethod(methodName, params, paramClasses, 
            o, context, this, strictRef);
        if (method == null) return null;

        try
        {
            //反射调用person.sayHello()
            Object obj = method.invoke(o, params);

            if (obj == null)
            {
                if( method.getReturnType() == Void.TYPE)
                {
                    return "";
                }
            }

            return obj;
        }
        ....
    }

同理看下ASTIdentifier.execute

 public Object execute(Object o, InternalContextAdapter context)
        throws MethodInvocationException
    {

        VelPropertyGet vg = null;

        try
        {
            /*
             *  first, 是否在缓存里面.
             */

            IntrospectionCacheData icd = context.icacheGet(this);
            if ( icd != null && (o != null) && (icd.contextData == o.getClass()) )
            {
                vg = (VelPropertyGet) icd.thingy;
            }
            else
            {
                //自省获取,默认开启缓存
                vg = rsvc.getUberspect().getPropertyGet(o,identifier, uberInfo);
                if (vg != null && vg.isCacheable() && (o != null))
                {
                    icd = new IntrospectionCacheData();
                    icd.contextData = o.getClass();
                    icd.thingy = vg;
                    context.icachePut(this,icd);
                }
            }
        }
        ...

        try
        {   //反射调用get方法
            return vg.invoke(o);
        }
        。。。
    }

另外ASTIdentifier.execute中的
rsvc.getUberspect().getPropertyGet(o,identifier,
uberInfo);的逻辑有必要单独说下:

澳门新葡萄京官网首页 14

screenshot.png

总结:velocity渲染引擎首先磁盘加载模板文件到内存,然后解析模板模板文件为AST结构,并对AST中每个节点进行初始化,第二次加载同一个模板文件时候如果开启了缓存则直接返回模板资源,通过使用资源缓存节省了从磁盘加载并重新解析为AST的开销。

然后配合context里面的变量值深度变量渲染AST节点到writer,对应TExt节点直接写入writer,对应引用节点则先从context获取对象实例,然后通过反射调用指定的方法,调用方法时候没有缓存,每调用一次就反射一次,但是使用对象.属性名方式第一次要使用自省功能找到getMethod,然后在反射调用,但是第二次调用同一个属性时候由于使用了缓存就省去了自省的过程,但是反射还是要的。所以在编写velocity模板时候尽可能使用临时变量保存反射调用结果,减少反射调用次数,降低页面渲染时间。

另外如果开启了资源缓存,并且file.resource.loader.modificationCheckInterval
>0还会实现hot
deploy也就是会每隔一段时间从磁盘获取最新的模板,重新生成AST结构,即使使用了缓存。

  • 1:选一门现成的编程语言,但大部分都是老外写的,语言关键字和规则繁多,老人们吃不消
  • 2:自己设计一门中文的编程语言,实现简单的输入输出,告诉老人什么是编程就行了

三、webx中veloctiy的使用

小刘选择自己设计一门编程语言,提笔一挥,寥寥的在小字本上涂涂画画。

3.1 webx集成veloctiy配置入口

在webx的webx-component.xml里面有一个配置如下:

<services:template
        xmlns="http://www.alibaba.com/schema/services/template/engines"
        searchExtensions="true">
        <velocity-engine templateEncoding="UTF-8"
            strictReference="false" path="/${component}/templates">
            <plugins>
                <vm-plugins:escape-support defaultEscape="html">
                    <vm-plugins:noescape>
                        <vm-plugins:if-matches pattern="^control." />
                        <vm-plugins:if-matches pattern="^screen_placeholder" />
                        <vm-plugins:if-matches pattern="^stringEscapeUtil.escape" />
                        <vm-plugins:if-matches pattern="^csrfToken.(get)?(w*)hiddenField" />
                    </vm-plugins:noescape>
                </vm-plugins:escape-support>
            </plugins>
        </velocity-engine>
        <freemarker-engine templateEncoding="UTF-8"
            path="/${component}/templates" />
        <jsp-engine path="/${component}/templates" />
    </services:template>

而解析该配置的解析器为:TemplateServiceDefinitionParser
首先说下,该配置目的是创建一个beanname=templateService的TemplateServiceImpl类,并实例化三种渲染引擎。下面看下解析代码:

public class TemplateServiceDefinitionParser extends AbstractNamedBeanDefinitionParser<TemplateServiceImpl> implements
                                                                                                            ContributionAware {
    private ConfigurationPoint templateEnginesConfigurationPoint;

    //(1) 解析插件等配置
    public void setContribution(Contribution contrib) {
        this.templateEnginesConfigurationPoint = getSiblingConfigurationPoint("services/template/engines", contrib);
    }

    @Override
    protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
        parseBeanDefinitionAttributes(element, parserContext, builder);

        Map<Object, Object> engines = createManagedMap(element, parserContext);
        Map<Object, Object> mappings = createManagedMap(element, parserContext);

        ElementSelector engineSelector = ns(templateEnginesConfigurationPoint.getNamespaceUri());
        ElementSelector mappingSelector = and(sameNs(element), name("template-mapping"));

        for (Element subElement : subElements(element)) {
            //(2) engine,解析并创建三个渲染引擎的bean定义
            if (engineSelector.accept(subElement)) {
                BeanDefinitionHolder engine = parseConfigurationPointBean(subElement,
                                                                          templateEnginesConfigurationPoint, parserContext, builder);
                engines.put(engine.getBeanName(), engine);
            }

            // mapping
            else if (mappingSelector.accept(subElement)) {
                String ext = normalizeExtension(subElement.getAttribute("extension"));
                String engineName = assertNotNull(trimToNull(subElement.getAttribute("engine")), "engine");

                assertNotNull(ext, "extension");
                assertTrue(!mappings.containsKey(ext), "duplicated extension: %s", ext);

                mappings.put(ext, engineName);
            }
        }

        //设置TemplateServiceImpl的engines和engineNameMappings属性
        builder.addPropertyValue("engines", engines);
        builder.addPropertyValue("engineNameMappings", mappings);

        attributesToProperties(element, builder, "defaultExtension", "searchExtensions", "searchLocalizedTemplates",
                               "cacheEnabled");
    }

    @Override
    protected String getDefaultName() {
        return "templateService";
    }
}

其中(2)创建了velocity的渲染引擎实例VelocityEngineImpl,解析器为VelocityEngineDefinitionParser为:

protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
    attributesToProperties(element, "configuration.", builder, "path", "cacheEnabled", "modificationCheckInterval",
                           "strictReference", "templateEncoding");

    ElementSelector globalMacros = and(sameNs(element), name("global-macros"));
    ElementSelector plugins = and(sameNs(element), name("plugins"));
    ElementSelector advancedProperties = and(sameNs(element), name("advanced-properties"));

    for (Element subElement : subElements(element)) {
        if (globalMacros.accept(subElement)) {
            //设置VelocityEngineImpl的成员变量configuration的属性值macros
            parseGlobalMacros(subElement, parserContext, builder);
        } else if (plugins.accept(subElement)) {
            //设置VelocityEngineImpl的成员变量configuration的属性值properties
            parsePlugins(subElement, parserContext, builder);
        } else if (advancedProperties.accept(subElement)) {
            parseAdvancedProperties(subElement, parserContext, builder);
        }
    }
}
定义 A,B,C;
B 等于 3;
输入 A ;
C 等于 A乘B;
输出 C;

3.2 velocity初始化

上节讲了集成velocity需要的一些bean的定义,下面看下如何实例并初始化:

澳门新葡萄京官网首页 15

screenshot.png

SpringIOC首先创建TemplateServiceImpl对象,但是要首先创建成员变量engines里面的VelocityEngineImpl,所以首先创建了它,然后调用init初始化,初始化过程首先获取webx-compoment.xml里面配置的插件等用户自定义属性,然后初始化默认的velocity.properties,然后用户属性覆盖默认属性,这也说明webx框架还是留有口子,让用户自定义veloctiy行为。然后回到
TemplateServiceImpl的创建,调用init方法设置映射,也就是后缀名与模板引擎的映射如下:

    ftl=com.alibaba.citrus.service.freemarker.impl.FreeMarkerEngineImpl#684bd25b:FreeMarkerEngine, jsp=JspEngine[/common/templates/], 
jspx=JspEngine[/common/templates/], vm=com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#5c6c2c1c:VelocityEngine

}```
webx是基于模块的,并且所有模块都import了webx-component.xml ,所以每个模块都有自己的veloctiyengine,例如我的webx例子里面有mouduleA,mouduleB,mouduleC,mouduleD模块,所以如下创建了四个引擎对象,对应不同的模板文件路径:

{com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#3fdeabbc=com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#3fdeabbc:VelocityEngine,
[/mouduleA/templates/]}

{com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#5783395d=com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#5783395d:VelocityEngine,
[/mouduleB/templates/]}

{com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#2d4f8a40=com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#2d4f8a40:VelocityEngine,
[/mouduleC/templates/]}

{com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#2ee2af0=com.alibaba.citrus.service.velocity.impl.VelocityEngineImpl#2ee2af0:VelocityEngine,
[/mouduleD/templates/]}

## 3.3一次渲染过程
![screenshot.png](http://upload-images.jianshu.io/upload_images/5879294-eff147da16c484ad.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
首先通过pipeline里面的PerformScreenValve调用screen对应的Java类设置变量,然后invoke RenderTemplateValve委托给veloctiy引擎VelocityEngineImpl执行模板获取与渲染。
# 四、SpringMVC中veloctiy的使用
## 4.1 SpringMVC集成veloctiy配置入口
在DispatcherServlet的配置文件里面添加如下:
```Java
    <!--1、对模型视图名称的解析,即在模型视图名称添加前后缀 -->
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.velocity.VelocityLayoutViewResolver">
        <property name="allowSessionOverride" value="true" />
        <property name="exposeSessionAttributes" value="true" />
        <property name="cache" value="true" />
        <property name="prefix" value="" />
        <property name="layoutUrl" value="layout.vm" />
        <property name="suffix" value=".vm" />
        <property name="contentType">
            <value>text/html;charset=UTF-8</value>
        </property>
    </bean>

    <!--2、velocity的一些设置 -->
    <bean id="velocityConfig"
        class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath">
            <value>/WEB-INF/templates/</value>
        </property>
        <property name="velocityProperties">
            <props>
                <prop key="input.encoding">UTF-8</prop>
                <prop key="output.encoding">UTF-8</prop>
                <prop key="contentType">text/html;charset=UTF-8</prop>

            </props>
        </property>
    </bean>

当然,上面的例子,这只是小刘写在小字本上的草稿,具体语言规则怎么定义?怎么解析这个语言?怎么执行这个语言?懵逼的小刘开始查阅一些资料。了解市面上的语言是如何实现的。

4.2 veloctiy初始化

先上一个时序图:

澳门新葡萄京官网首页 16

screenshot.png

如图DispatcherServlet的init方法中创建了一个spring的context,去解析springDispatcher-servlet.xml里面配置的bean,首先实例并初始化了VelocityLayoutViewResolver,然后实例初始化了VelocityConfigurer,在它的afterPropertiesSet方法创建了veloctiy引擎并初始化。

小刘的故事先说到这儿。我们开始严肃一点…

4.3 一次渲染过程

先看下时序图

澳门新葡萄京官网首页 17

screenshot.png

首先找到对应的controller返回包含参数的mv,
然后通过VelocityLayoutViewResolver去创建view:

public View resolveViewName(String viewName, Locale locale) throws Exception {
        //是否开启缓存,没开启则创建
        if (!isCache()) {
            return createView(viewName, locale);
        }
        else {
            //开启缓存,首先看缓存
            Object cacheKey = getCacheKey(viewName, locale);
            View view = this.viewAccessCache.get(cacheKey);
            if (view == null) {
                synchronized (this.viewCreationCache) {
                    view = this.viewCreationCache.get(cacheKey);
                    if (view == null) {
                        // Ask the subclass to create the View object.
                        view = createView(viewName, locale);
                        if (view == null && this.cacheUnresolved) {
                            view = UNRESOLVED_VIEW;
                        }
                        if (view != null) {
                            this.viewAccessCache.put(cacheKey, view);
                            this.viewCreationCache.put(cacheKey, view);
                            if (logger.isTraceEnabled()) {
                                logger.trace("Cached view [" + cacheKey + "]");
                            }
                        }
                    }
                }
            }
            return (view != UNRESOLVED_VIEW ? view : null);
        }
    }

由于配置时候cache=true所以打开了cache,这里有两级缓存,定义如下:

/** 快速从Cache中获取view, 不加全局锁 */
    private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT);

    /** 创建view时候加全局锁 */
    @SuppressWarnings("serial")
    private final Map<Object, View> viewCreationCache =
            new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
                @Override
                protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {
                    if (size() > getCacheLimit()) {
                        viewAccessCache.remove(eldest.getKey());
                        return true;
                    }
                    else {
                        return false;
                    }
                }
            };

为了性能,对已经创建的view直接从viewAccessCache中就可以快速获取,这避免了在创建view期间其他线程等待的现象,只有两个线程都要创建view时候才会等待。也就说当一个线程进入同步块创建view时候,其他线程获取已经存在的view不需要等待。

获取velocityview后,会委托给velocityengine获取模板,然后调用velocityview的render方法调用template.merge实际对AST进行渲染

『设计模式』中有一个模式可以解释特定的语法规则,它就是解释器模式(Interpreter
Pattern),不同于的工厂模式或者策略模式,解释器模式在java或者.net中并不常见,业务中很少用去解释特定的语法,所以并不被广泛的使用。一个解释器可大可小,大可为复杂的编译器,小可以是一个简单的字符串解析。但本质都是对特定语法做出合理解释。

五、参考

1.http://velocity.apache.org/
2.http://velocity.apache.org/engine/1.7/user-guide.html

阿里巴巴长期招聘Java研发工程师p6,p7,p8等上不封顶级别,有意向的可以发简历给我,注明想去的部门和工作地点:1064454834@qq.com

欢迎关注微信公众号:技术原始积累 获取更多技术干货

欢迎关注微信公众号:技术原始积累 获取更多技术干货

澳门新葡萄京官网首页 18

image.png

假设输入一个公式字符串: 1+2*3
注意这是一个字符串,要解析这个公式字符串,得到最终的值我们有两种方案:

  • 循环遍历字符串,将括号,运算符,数字提取出来,然后根据运算符左右结合以及优先级来计算
  • 将表达式转换为树结构的对象,树结构的每个节点,可以是数字,可以是运算符,每个节点类型不
    同,然后递归遍历这个树结构,遇到运算符号节点,递归求运算符节点下的左右节点值,然后将两个节点值做相应的运算。
    很显然,第一种方案简单直白易理解,但实现起来相当繁重,代码可读性也不佳。第二种则是目前最好的解决方式,将表达式字符串解析为一个对象树。

所以我们的第一个难点是如何将表达式转换为一颗树

对于算术表达式而言,比如1+3-2,6-1,语法是两个数字之间必须出现+,-,*,/,如果出现1+-2,6–1那这就是错误的语法
那我们怎么来制定语法呢,在编译原理领域,有一个通用的方法来描述语法,叫是BNF范式


语言的描述——BNF范式

为什么不用自然语言来定义我们的语言的规范?很难啊!,现在自然语言处理,仍然是世界性的难题,目前还没有哪种程序能够将自然语言处理的很好。

描述一个文法,我们常常使用巴斯克范式(BNF范式)来描述一个文法的结构

科普一下,世间万语,皆可用归纳为4种文法,计算机编程主要使用 2型,3型
文法。(0,1型交给我们语文老师处理了)

  • 0型即自然语言文法
  • 1型称为上下文相关文法
  • 2型称为上下文无关文法
  • 3型称为正则文法

简单介绍一下bnf范式文件格式

  • < > 尖括号内为必选项
  • | 竖线表示在其左右两边任选一项,相当于”OR”的意思
  • ::= 是“被定义为”的意思
    一条BNF的生成规则形如:

<char>  ::= A|B|C|D|E|F|G|H|... 偷个懒

更详细的规则,可参考巴科斯范式

针对上面四则运算,简单的 bnf 文件内容如下:

<exp>  ::= <fac>|<fac>+<exp>|<fac>-<exp>
<fac>  ::= <int>|<int>*<int>|<int>/<int>
<int>  ::= <digit><int>|<digit>
<digit>  ::= 0|1|2|3|4|5|6|7|8|9

digt: 数字,int 整形,fac:优先级高的运算,exp:表达式

有了bnf范式规则,我们需要表达式字符串1+3-2构造成一个符合bnf规则的数据结构,如下图:

澳门新葡萄京官网首页 19

四则运算_ast.png

我们需要自己写解析函数,然后构造成上图所示的数据结构,在编译原理领域这种结构叫
抽象语法树(AST)

简单介绍一下抽象语法树

抽象语法树(AST)

语言解释分为前后端,前端语言 java,c,php,js。后端主要是指编译器 例如
GJC,GCC 等。

例如实现一个c 版的 for 循环,需要用GCC 将c
源代码编译成一个GCC语言版抽象语法树,然后GCC 在解释执行这个抽象语法树。

抽象语法树特点

  • 不依赖源语言文法
    如果按bnf
    文法解析源代码,解析为一个自定义的结构,那在解释这个自定义结构的时候,肯定是为bnf
    文法量身定制的。一旦这个语言有了升级,bnf
    文法发生变动,相应的,后端解释器也会做相应的改动,十分麻烦。
    抽象语法树,相当于一个后端解释器给前端定制的一个规范,无论语言规范怎么变动,只需要改变将源代码解析为抽象语法树的解析器就行了,解析抽象语法树的解释器并不用改动。

  • 不依赖细节
    不同语言实现一个for 循环
    在c中为:

if(condition){
  dosomthing();
}

在fortran中为:

If condition then
    dosomthing()
end if

语法树只需要两个分支节点来表示

澳门新葡萄京官网首页 20

AST_IF.png

在源程序中出现的关键字 if
括号等,都将忽略,两种语言最终生成一样的抽象语法树

抽象语法树作用

前端领域使用抽象语法树极为广泛,将js代码转换为抽象语法树后,可以很轻松的对语法树进行分析与优化,语法树带给我们的便利充斥在开发过程中的方方面面,例如IDE对代码进行格式化缩进。
简单列举抽象语法树,的作用:

  • 格式化存储(存储网页,存储sql 语句,描述json 的json)
  • 语法检查、格式化、高亮、错误提示、代自动补全
    • ide 功能糖
  • 代码混淆
    • uglifyjs
    • CssMinify
  • 代码优化
    • webpack 梳理依赖关系
    • amd,cmd 规范相互转换
    • bable 编译 es6
    • CoffeeScript、TypeScript、JSX等转化为原生Javascript

抽象语法树结构

javascript 源代码 1+1 转换为抽象语法树(AST)
源代码

1+1

将js 源代码转换为AST 抽象语法树,

{
    "type": "Program",
    "body": [
        {
            "type": "ExpressionStatement",
            "expression": {
                "type": "BinaryExpression",
                "operator": "+",
                "left": {
                    "type": "Literal",
                    "value": 1,
                    "raw": "1"
                },
                "right": {
                    "type": "Literal",
                    "value": 1,
                    "raw": "1"
                }
            }
        }
    ],
    "sourceType": "script"
}

转换为AST语法树的工具比较多,v8,Esprima,UglifyJS2,Acorn
等。能转换抽象语法树的也不只是 java,js ,php
等,html,css,sql,json,这些都可转换为抽象语法树

这种树结构为js
通用的AST树结构,基本上主流开源的解析器都解析为这种结构,当然也可以转换为其他格式的AST树结构,相应的,解析器就需要自己去实现。


回到正题,我们有了语法树,接下来要做啥?

我们需要去遍历这个树,然后对树的每个节点,做相应的处理,例如,遇到 int
节点,直接返回他的值,遇到 exp节点,则先计算 exp
左右节点的值,然后根据自身的运算符号(+,-等)做相应的加减操作。

遍历节点,并处理节点这个过程,我们叫文法识别(解释器),也是解释器模式中的解释过程。在编程语言领域,做这方面工作的我们叫编译器

这儿就一笔带过说一下解释器,我了解的并不深入。

文法识别 (解释器)

解释器,输入为ast抽象语法树,然后遍历ast的每个节点,针对每个节点做响应处理,直到节点遍历完成。

  • 任何语言的抽象语法树和解释器都是基于他底层语言的,例如v8引擎
    js解析的底层就是c
    ,他的AST抽象语法树,也是c语言版本的语法树,解释器也是c
    语言版本的解释器。而c语言的抽象语法树则是GCC编译器了。

  • 像ide,uglifyjs,bable 等虽然也可以解析js
    为抽象语法树,但这个语法树都是js
    版本的语法树,并不需要解释器,语法树只是辅助他们做一些js
    语法的优化等。如果用js 写一个java 抽象语法树的解释器,那就可以在node
    里面执行 java 了(意义何在?)

自定义编程语言

上面的描述,我已经写懵逼了,估计看的人也是懵逼的,接下来我们要用解释器模式,实现一套自己的编程语言
,bnf+ast+ 解释器。取个好听的名字 懵逼 语言

首先定义我们语言的bnf 范式,如下:

    <程序>     ::= <语句 块>
    <定义>     ::= 定义  <变量 组> ;
    <变量 组>  ::= <变量> , <变量 组> | <变量>
    <语句 块>  ::= <语句> <语句 块> | <语句>
    <语句>     ::= <赋值>|<判断>|<循环>|<输出>|<输入>|<定义> 
    <输入>     ::= 输入 <变量 组> ;
    <输出>     ::= 输出 <变量 组> ;    
    <判断>     ::= 如果 <条件> { <语句 块> } ;
    <条件>     ::= <比较> | ! <条件> | [ <条件> && <条件> ] | [ <条件> or <条件> ]   
    <比较>     ::= ( <值> <比较 符> <值> )
    <比较 符>  ::= != | == | < | > | <= | >=   
    <值>       ::= <整形> | <变量> | ( <表达式> )|<字符串>
    <变量>     ::= <字符 <变量> | <字符><整形> | <字符>
    <字符>     ::= A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z
    <整形>     ::= <数字><整形> | <数字>
    <数字>     ::= 0|1|2|3|4|5|6|7|8|9

然后编写符合上述范式规则的程序源代码
源代码

定义 Y;
输入 Y;
如果 ( Y > 0 ) {
   输出 Y ;
};

然后是写解析器,将源代码按bnf 规则,解析为抽象语法树。

解析器代码这儿就不粘贴,可以在git 项目中查看
(解析器源代码)

语法树可视化后如下

澳门新葡萄京官网首页 21

火星语言_AST.png

此时 ast
构造已经完成,剩下的就是实现解释器。解释器,输入为AST树对象,然后对树进行递归遍历,针对不同节点,做不同处理。最终将整个AST
树执行完。后续再粘代码…
demo 地址
git
地址

发表评论

电子邮件地址不会被公开。 必填项已用*标注