Velocity工做原理解析和优化

在MVC开发模式下,View离不开模板引擎,在Java语言中模板引擎使用得最可能是JSP、Velocity和FreeMarker,在MVC编程开发模式中,必不可少的一个部分是V的部分。V负责前端的页面展现,也就是负责生产最终的HTML,V部分一般会对应一个编码引擎,当前众多的MVC框架都已经能够将V部分独立开来,能够与众多的模板引擎集成。前端

Velocity整体架构

从代码结构上看,Velocity主要分为app、context、runtime和一些辅助util几个部分。java

APP模块

其中app主要封装了一些接口,暴露给使用者使用。主要有两个类,分别是Velocity(单例)和VelocityEngine。程序员

前者主要封装了一些静态接口,能够直接调用,帮助你渲染模板,只要传给Velocity一个模板和模板中对应的变量值就能够直接渲染。web

VelocityEngine类主要是供一些框架开发者调用的,它提供了更加复杂的接口供调用者选择,MVC框架中初始化一个VelocityEngine: 算法

 

以上是Spring MVC建立Velocity模板引擎的VelocityEngine实例的代码段,先建立一个VelocityEngine实例,再将配置参数设置到VelocityEngine的Property中,最终调用init方法初始化。数据库

Context模块

Context模块主要封装了模板渲染须要的变量,它的主要做用有两点:express

  1. 便于与其余框架集成,起到一个适配器的做用,如MVC框架内部保存的变量每每在一个Map中,这样MVC框架就须要将这个Map适配到Velocity的context中。
  2. Velocity内部作数据隔离,数据进入Velocity的内部的不一样模块须要对数据作不一样的处理,封装不一样的数据接口有利于模块之间的解耦。

Context类是外部框架须要向Velocity传输数据必须实现的接口,具体实现时能够集成抽象类AbstractContext,例如,Spring MVC中直接继承了VelocityContext,调用构造函数建立Velocity须要的数据结构。apache

另一个接口InternetEventContext主要是为扩展Velocity事件处理准备的数据接口,当你扩展了事件处理、须要操做数据时能够实现这个接口,而且处理你须要的数据。编程

Runtime模块

整个Velocity的核心模块在runtime package下,这里会将加载的模板解析成JavaCC语法树,Velocity调用mergeTemplate方法时会渲染整棵树,并输出最终的渲染结果。数组

RuntimeInstance类

RuntimeInstance类为整个Velocity渲染提供了一个单例模式,它也是Velocity的一个门面,封装了渲染模板须要的全部接口拿到了这个实例就能够完成渲染过程了。它与VelocityEngine不一样,VelocityEngine表明了整个Velocity引擎,它不只包括模板渲染,还包括参数设置及数据的封装规则,RuntimeInstance仅仅表明一个模板的渲染状态。

JJTree渲染过程解析

下面是一段Velocity的模板代码vm和这段代码解析成的语法树:

Velocity渲染这段代码将从根节点ASTproces开始,按照深度优先遍历算法开始遍历整棵树,遍历的代码以下所示:

如代码所示,依次执行当前节点的全部子节点的render方法每一个节点的渲染规则都在render方法中实现,对应到上面的vm代码,#foreach节点对应到ASTDirective。这种类型的节点是一个特殊的节点,它能够经过directiveName来表示不一样类型的节点,目前ASTDirective已经有多个,如#break、#parse、#include、#define等都是ASTDirective类型的节点。这种类型的节点一般都有一个特色,就是它们的定义相似于一个函数的定义,一个directiveName后面跟着一对括号,括号里含有参数和一些关键词,如#foreach,directiveName是foreach,括号中的$i是ASTReference类型,in是关键词ASTWord类型,[1 ..10]是一个数组类型ASTIntegerRange,在#foreach和#end之间的全部内容都由ASTBlock表示。

所谓的指令指的就是在页面上能用一些相似标签的东西。Velocity默认的指令文件位置org/apache/velocity/runtime/defaults/directive.properties

在这个文件中定义了一些默认的指令,例如:

directive.1=org.apache.velocity.runtime.directive.Foreach

directive.2=org.apache.velocity.runtime.directive.Include

directive.3=org.apache.velocity.runtime.directive.Parse

directive.4=org.apache.velocity.runtime.directive.Macro

directive.5=org.apache.velocity.runtime.directive.Literal

directive.6=org.apache.velocity.runtime.directive.Evaluate

directive.7=org.apache.velocity.runtime.directive.Break

directive.8=org.apache.velocity.runtime.directive.Define

咱们在vm文件中能够直接使用foreach等指令来让咱们的页面更加的灵活。

Velocity的语法相对简单,因此它的语法节点并非不少,总共有50几个,它们能够划分为以下几种类型。

  1. 块节点类型:主要用来表示一个代码块,它们自己并不表示某个具体的语法节点,也不会有什么渲染规则。这种类型的节点主要由ASTReference、ASTBlock和ASTExpression等组成。
  2. 扩展节点类型:这些节点能够被扩展,能够本身去实现,如咱们上面提到的#foreach,它就是一个扩展类型的ASTDirective节点,咱们一样能够本身再扩展一个ASTDirective类型的节点。
  3. 中间节点类型:位于树的中间,它的下面有子节点,它的渲染依赖于子节点才能完成,如ASTIfStatement和ASTSetDirective等。
  4. 叶子节点:它位于树的叶子上,没有子节点,这种类型的节点要么直接输出值,要么写到writer中,如ASTText和ASTTrue等。

Velocity读取vm模板根据JavaCC语法分析器将不一样类型的节点按照上面的几个类型解析成一个完整的语法树。

在调用render方法以前,Velocity会调用整个节点树上全部节点的init方法来对节点作一些预处理,如变量解析配置信息获取等。这很是相似于Servlet实例化时调用init方法。Velocity在加载一个模板时也只会调用init方法一次,每次渲染时调用render方法就如同调用Servlet的service方法同样。

#set语法

#set语法能够建立一个Velocity的变量,#set语法对应的Velocity语法树是ASTSetDirective类,翻开这个类的代码,能够发现它有两个子节点:分别是RightHandSide和LeftHandSide,分别表明“=”两边的表达式值。与Java语言的赋值操做有点不同的是,左边的LeftHandSide多是一个变量标识符,也多是一个set方法调用。变量标识符很好理解,如前面的#set($var=“偶数”),另外是一个set方法调用,如#set($person.name=”junshan”),这实际上至关于Java中person.setName(“junshan”)方法的调用。

#set语法如何区分左边是变量标识符仍是set方法调用?看一下ASTSetDirective类的render方法:

从代码中能够看到,先取得右边表达式的值,而后根据左边是否有子节点判断是变量标识符仍是调用set方法。经过#set语法建立的变量是否有有效范围,从代码中能够看到会将这个变量直接放入context中,因此这个变量在这个vm模板中是一直有效的它的有效范围和context也是一致的。因此在vm模板中无论在什么地方经过#set建立的变量都是同样的,它对整个模板都是可见的。

Velocity的方法调用

Velocity的方法调用方式有多种,它和咱们熟悉的Java的方法调用仍是有一些区别之处的,若是你不熟悉,可能会产生一些误解,下面举例介绍一下。

Velocity经过ASTReference类来表示一个变量和变量的方法调用,ASTReference类若是有子节点,就表示这个变量有方法调用,方法调用一样是经过“.”来区分的,每个点后面会对应一个方法调用。ASTReference有两种类型的子节点,分别是ASTIdentifierASTMethod。它们分别表明两种类型的方法调用,其中ASTIdentifier主要表示隐式的“get”和“set”类型的方法调用。而ASTMethod表示全部其余类型的方法调用,如全部带括号的方法调用都会被解析成ASTMethod类型的节点。

所谓隐式方法调用在Velocity中一般有以下几种。

1.Set类型,如#set($person.name=”junshan”),以下:

  • person.setName(“junshan”)
  • person.setname(“junshan”)
  • person.put(“name”,”junshan”)

2.Get类型,如#set($name=$person.name)中的$person.name,以下:

  • person.getName()
  • person.getname()
  • person.get(“name”)
  • person.isname()
  • person.isName()

Get&Set反射调用

Set 继承SetExecutor:当Velocity在解析#set($person.name=”junshan”)时,它会找到$person对应的对象,而后建立一个SetPropertyExecutor对象并查找这个对象是否有setname(String)方法,若是没有,再查找setName(String)方法,若是再没有,那么再建立MapSetExecutor对象,看看$person对应的对象是否是一个Map。若是是Map,就调用Map的put方法,若是不是Map,再建立一个PutExecutor对象,检查一下$person对应的对象有没有put(String)方法,若是存在就调用对象的put方法。

Get:除去Set类型的方法调用,其余的方法调用都继承了AbstractExecutor类如#set($name=$person.name)中解析$person.name时,建立PropertyExecutor对象封装可能存在的getname(String)或getName(String)方法。不然建立MapGetExecutor检查$person变量是不是一个Map对象。若是不是,建立GetExecutor对象检查$person变量对应的对象是否有get(“Name”)方法。若是尚未,建立BooleanPropertyExecutor对象并检查$person变量对应的对象是否有isname()或者isName()方法。找到对应的方法后,将相应的java.lang.reflect.Method对象封装在对应的封装对象中。

以上这些查找顺序中,某个方法找到后就直接返回某种类型的Executor对象包装的Method,而后经过反射调用Method的invoke方法。Velocity的反射调用是经过Introspector类来完成的,它定义了类对象的方法查找规则。

显式调用:除去以上对两种隐式的方法调用的封装外,Velocity还有一种简单的方法调用方式,就是带有括号的方法调用,如$person.setName(“junshan”),这种精确的方法调用会直接查找变量$person对应的对象有没有setName(String)方法,若是有,会直接返回一个VelMethod对象,这个对象是对通用的方法调用的封装,它能够处理$person对应的对象数组类型或静态类时的状况。数组的状况如string=newString[]{“a”,”b”,”c”},要取的第二个值在Java中能够经过string[1]来取,但在Velocity中能够经过$string.get(1)取得数组的第二个值。为什么能这样作呢?能够看一下Velocity中相应的代码:

 

从上面的代码中咱们能够发现,精确查找方法的规则是查找$person对应的对象是否有指定的方法,而后检查该对象是不是数组,若是是数组,把它封装成List,而后按照ArrayListWrapper类去代理访问数组的相应值。若是$person对应的对象是静态类,能够调用其静态方法。

#if、#elseif和#else语法

#if和#else节点是Velocity中的逻辑判断节点,它的语法规则几乎和Java是同样的,主要的不一样点在条件判断上,如Velocity中判断#if($express)为true的状况是只要$express变量的值不为null和false就行,而Java中显然不能这样判断。

除单个变量的值判断以外,Velocity还支持Java的各类表达式判断,如“>”、“<”、“==”和逻辑判断“&&”、“||”等。每个判断条件都会对应一个节点类,如“==”对应的类为ASTEQNode,判断两个值是否相等的条件为:先取得等号两边的值,若是是数字,比较两个数字的大小是否相等,再判断两边的值是否都是null,都为null则相等,不然其中一个为null,确定不等;再次就是取这两个值的toString(),比较这两个值的字符值是否相等。值得注意的是,Velocity中并不能像Java中那样判断两个变量是不是同一个变量,也就是object1==object2与object1. equals(object2)在Velocity中是同样的效果。

特别要注意的是,不少人在写Velocity代码时有相似这样的写法,如#if("$example.user"== "null")和#if("$example.flag" == "true"),这些写法都是不正确的,正确的写法是#if($example.user)和#if($example.flag)。

若要使用 #ifnull() 或 #ifnotnull(), 要使用#ifnull ($foo)这个特性必须在velocity.properties文件中加入:

userdirective = org.apache.velocity.tools.generic.directive.Ifnull
userdirective = org.apache.velocity.tools.generic.directive.Ifnotnull

若是有多个#elseif节点,Velocity会依次判断每一个子节点,从#if节点的render方法代码中咱们能够看出,第一个子节点就是#if中的表达式判断,这个表达式的值为true则执行第二个子节点,第二个子节点就是#if下面的代码块。若是#if中表达式判断为false,则继续执行后面的子节点,若是存在其余子节点确定就是#elseif或者#else节点了,其中任何一个为true将会执行这个节点的render方法而且会直接返回。 

#foreach语法

Velocity中的循环语法只有这一种,它与Java中的for循环的语法糖形式十分相似,如#foreach($child in $person.children) $person.children表示的是一个集合,它多是一个List集合或者一个数组,而$child表示的是每一个从集合中取出的值。从render方法代码中能够看出,Velocity首先是取得$person.children的值,而后将这个值封装成Iterator集合,而后依次取出这个集合中的每个值,将这个值以$child为变量标识符放入context中。除此之外须要特别注意的是,Velocity在循环时还在context中放入了另外两个变量,分别是counterName和hasNextName,这两个变量的名称分别在配置文件配置项directive.foreach.counter.name和directive.foreach.iterator.name中定义,它们表示当前的循环计数和是否还有下一个值。前者至关于for(int i=1;i<10;i++)中的i值,后者至关于while(it.hasNext())中的it.hasNext()的值,这两个值在#foreach的循环体中都有可能用到。因为elementKey、counterName和hasNextName是在#foreach中临时建立的,若是当前的context中已经存在这几个变量,要把原始的变量值保存起来,以便在这个#foreach执行结束后恢复。若是context中没有这几个变量,那么#foreach执行结束后要删除它们,这就是代码最后部分作的事情,这与咱们前面介绍的#set语法没有范围限制不一样,#foreach中临时产生的变量只在#foreach中有效。

#parse语法

#parse语法也是Velocity中十分经常使用的语法,它的做用是可让咱们对Velocity模板进行模块化,能够将一些重复的模块抽取出来单独放在一个模板中,而后在其余模板中引入这个重用的模板,这样能够增长模板的可维护性。而#parse语法就提供了引入一个模板的功能,如#parse(‘head.vm’)引入一个公共页头。固然head.vm能够由一个变量来表示。#parse和#foreach同样都是经过扩展节点ASTDirective来解析的,因此#parse和#foreach同样都共享当前模板执行环境的上下文。虽然#parse是单独一个模板,可是这个模板中变量的值都在#parse所在的模板中取得Velocity中的#parse咱们能够仅理解为只是将一段vm代码放在一个单独的模板中,其余没有任何变化。 从代码中能够看出执行分为三部分,首先取得#parse(‘head.vm’)中的head.vm的模板名,而后调用getTemplate获取head.vm对应的模板对象,再调用该模板对应的整个语法树的render方法执行渲染。#parse语法的执行和其余的模板的渲染没有什么区别,只不过模板渲染时共用了父模板的context和writer对象而已。

事件处理机制

Velocity的事件处理机制所涉及的类在org.apache.velocity.app.event下面, EventHandler是全部类的父接口,EventHandler类有5个子类,分别表明5种不一样的事件处理类型。

  1. ReferenceInsertionEventHandler:表示针对Velocity中变量的事件处理,当Velocity在渲染输出某个“$”表示的变量时能够对这个变量作修改,如对这个变量的值作安全过滤以防止恶意JS代码出如今页面中等。
  2. NullSetEventHandler:顾名思义是对#set语法赋值为null时的事件作处理。
  3. MethodExceptionEventHandler:这个事件是对Velocity在反射执行某个方法调用时出错后,有机会作一些处理,如捕获异常、控制返回一些特殊值等。
  4. InvalidReferenceEventHandler:表示Velocity在解析“$”变量出现没有找到对应的对象时作如何处理。
  5. IncludeEventHandler:在处理#include和#parse时提供了处理和修改加载外部资源的机会。

Velocity提供的这些事件处理机制也为咱们扩展Velocity提供了机会,若是你想扩展Velocity,必须对它的事件处理机制有很好的理解。

如何调用到扩展的EventHandler?Velocity提供了两种方式,Velocity在渲染时遇到符合的事件都会检查如下的EventCartridge:

  1. 把你新建立的EventHandler直接加到org.apache.velocity.runtime.RuntimeInstance类的eventCartridge属性中,直接将自定义的EventHandler经过配置项eventCartridge.classes来设置,Velocity在初始化RuntimeInstance时会解析配置项,而后会实例化EventHandler。
  2. 把自定义的EventHandler加到本身建立的EventCartridge对象中,而后在渲染时把这个EventCartridge对象经过调用attachToContext方法加到context中,可是这个context必需要继承InternalEventContext接口,由于只有这个接口才提供了attachToContext方法和取得EventCartridge的getEventCartridge方法。动态地设置EventHandler,只要将EventHandler加到渲染时的context中,Velocity在渲染时就能调用它。

EventCartridge中保存了全部的EventHandler,而且EventCartridge把它们分别保存在5个不一样的属性集合中,分别是referenceHandlers、nullSetHandlers、methodExceptionHandlers、includeHandlers和invalidReferenceHandlers。如何找到EventHandle?Velocity在渲染时分别在两个地方检查可能存在的EventHandler,那就是RuntimeInstance对象和渲染时的context对象,这两个对象在Velocity渲染时随时都能访问到。什么时候被触发?有一个类EventHandlerUtil它就负责在合适的事件触发时调用事件处理接口来处理事件。如变量在输出到页面以前会调用value = EventHandlerUtil.referenceInsert(rsvc, context, literal(), value)来检查是否有referenceHandlers须要调用。其余事件也是相似处理方式。

ps:

扩展Velocity的事件处理会涉及对Context的处理,Velocity增长了一个ContextAware接口若是你实现的EventHandler须要访问Context,那么能够继承这个接口。Velocity在调用EventHandler以前会把渲染时的context设置到你的EventHandler中,这样你就能够在EventHandler中取到context了。若是要访问RuntimeServices对象,一样能够继承RuntimeServicesAware接口。

Velocity还支持另一种扩展方式,就是在渲染某个变量的时候判断这个变量是否是Renderable类的实例,若是是,将会调用这个实例的render( InternalContextAdapter context, Writer writer)方法,这种调用是隐式调用,也就是不须要在模板中显式调用render()方法。

优化的理论基础

程序的语言层次结构和这个语言的执行效率造成一对倒立的三角形结构。从图中能够看出,越是上层的高级语言,它的执行效率每每越低。这很好理解,由于最底层的程序语言只有计算机能明白,与人的思惟很不接近,为何咱们开发出这么多上层语言,很重要的目的就是对底层的程序作封装,使得咱们开发更方便,很显然这些通过重重封装的语言的执行效率确定比没有通过封装的底层程序语言的效率要差不少,不然和硬件相关的驱动程序也不会用C语言或汇编语言来实现了。

 

数据结构减小抽象化

程序的本质是数据结构加上算法,算法是过程,而数据结构是载体。程序语言也是一样的道理,越是高级的程序语言必然数据结构越抽象化,这里的抽象化是指它们的数据结构与人的思惟越接近。有些语言(如Python)的语法规则很是像咱们的人语言,即便没有学过编程的人也很容易理解它。这里所说的数据结构去抽象化是指把须要调用底层的接口的程序改由咱们本身去实现,减小这个程序的封装程度,从而达到提高性能的目的,因此并非改变程序语法。

简单的程序复杂化

先举一个例子,咱们想从数据库中去掉一行数据,目前的环境中已经有人提升了一个调数据库查询的接口,这个接口的实现使用了iBatis做为数据层调用数据库查询数据,实际上它封装了对象与数据字段的关系映射及管理数据库链接池等。使用起来很方便,可是它的执行效率是否是比咱们直接写一个简单的JDBC链接、提交一个SQL语句的效率高呢?很显然,后面的执行效率更高,抛去其余因素,显然没有通过封装的复杂程序要比简单的调用上层接口效率要高不少。因此咱们要作的就是适当地让咱们的程序复杂一点,而不要偷懒,也许这样咱们的程序效率会增长很多。

减小翻译的代价

咱们知道与不一样国家的人交流是要经过翻译的,可是这个翻译实在是耗时间。程序设计一样存在翻译的问题,如咱们的编码问题,美国人的全部字符一个字节就能所有表示,因此他们的全部字符就是一个字节,也就是一个ASSCII码,因此对他们来讲不存在字符编码问题,可是对其余国家的程序员来讲,不得不面临一个让人头疼的字符编码问题,须要将字节与字符之间来回翻译,并且还很容易出现错误。咱们要尽可能减小这种翻译,至少在真正与人交流时把一些常常用的词汇提早就翻译好,从而在面对面交流时减小须要翻译的词汇的数量,从而提高交流效率。

变的转化为不变

如今的网页基本上都是动态网页,可是所谓的动态网页中仍然有不少静态的东西,如模板中仍然有不少是HTML代码,它们和一些变量共同拼接成一个完整的页面,可是这些内容从程序员写出来到最终在浏览器里渲染,都是一成不变的。既然是不变的,那么就能够对它们作一些预处理,如提早将它们编码或者将它们放到CDN上。另外,尽可能把一些变化的内容转化成不变的内容,如咱们可能将一个URL做为一个变量传给模板去渲染,可是这个URL中真正变化的仅仅是其中的一个参数,整个主体确定是不会变化的,因此咱们仍然能够从变化的内容中分离出一部分做为不变的来处理。这些都是细节,可是当这些细节组合在一块儿时每每就会带来让你意想不到的好的结果。

经常使用优化技巧

Velocity渲染模板是先把模板解析成一棵语法树,而后去遍历这棵树分别渲染每一个节点,知道了它的工做原理,咱们就能够根据它的工做机制来优化渲染的速度。既然是遍历这棵树来渲染节点的,并且是顺序遍历的,那么很容易想到有两种办法来优化渲染:

  1. 减小树的总节点数量。
  2. 减小渲染耗时的节点数量。
  3. 改变Velocity的解释执行,变为编译执行。
  4. 方法调用的无反射优化
  5. 字符输出改为字节输出
  6. 去掉页面输出中多余的非中文空格。咱们知道,页面的HTML输出中多余的空格是不会在HTML的展现时有做用的,多个连续的空格最终都只会显示一个空格的间距,除非你使用“ ”表示空格。虽然多余的空格并不能影响HTML的页面展现样式,可是服务端页面渲染和网络数据传输这些空格和其余字符没有区别,一样要作处理,这样的话,这些空格就会形成时间和空间维度上的浪费,因此彻底能够将多个连续的空格合并成一个,从而既减小了字符又不会影响页面展现。
  7. 压缩TAB和换行。一样的道理,还能够将TAB字符合并成一个,以及将多余的换行也合并一下,也能减小很多字符。
  8. 合并相同的数据。在模板中有不少相同数据在循环中重复输出,如类目、商品、菜单等,能够将相同的重复内容提取出来合并在CSS中或者用JS来输出。
  9. 异步渲染。将一些静态内容抽取出来改为异步渲染,只在用户确实须要时再向服务器去请求,也可以减小不少没必要要的数据传输。

减小树的总节点数量

既然一个模板输出的内容是肯定的,那么这个模板的vm代码应该是固定的,减小节点数量必然删去一部分vm代码才能作到?其实并非这样的,虽然最终渲染出来的页面是同样的,可是vm的写法却有很大不一样,笔者在检查vm代码时遇到不少不优美的写法,致使无谓增长了不少没必要要的语法节点。以下面一段代码:

这段代码实际上只是要计算一个值,可是因为不熟悉Velocity的一些语法,写得很麻烦,其实只要一个表达式就行了,以下:

 

这样能够减小不少语法节点。

减小渲染耗时的节点数量

Velocity的方法调用是经过反射执行的,显然反射执行方法是耗时的,那么又如何减小反射执行的方法呢?这个改进就如同Java中同样,能够增长一些中间变量来保存中间值,而减小反射方法的调用。如在一个模板中要屡次调用到$person.name,那么能够经过#set建立一个变量$name来保存$person.name这个反射方法的执行结果。如#set($name=$person.name),这样虽然增长了一个#set节点,可是若是能减小屡次反射调用仍然是很值得的。

另外,Velocity自己提供了一个#macro语法,它相似于定义一个方法,而后能够调用这个方法,但在没有必要时尽可能少用这种语法节点,这些语法节点比较耗时。还有一些大数计算等,最好定义在Java中,经过调用Java中的方法能够加快Velocity的执行效率。

解释执行转换成编译执行

也就是将vm模板先编译成Java类,再去执行这个Java对象,从而渲染出页面。Sketch模版引擎,主要分为两个部分:运行时环境和编译时环境。前者主要用来将模板渲染成HTML,后者主要是把模板编译成Java类。当请求渲染一个vm模板时,经过调用单例RuntimeServer获取一个模板编译后的Java对象,而后调用这个模板对应的Java对象的render方法渲染出结果。若是是第一次调用一个vm模板,Sketch框架将会加载该vm模板,并将这个vm模板编译成Java,而后实例化该Java类,实例化对象放入RuntimeContext集合中,并根据Context容器中的变量对应的对象值渲染该模板。一个模板将会被屡次编译,这是一个不断优化的过程。

咱们优化Velocity模板的一个目的就是将模板的解释执行变为编译执行,从前面的理论分析可知,vm中的语法最终被解释成一棵语法树,而后经过执行这棵语法树来渲染出结果。咱们要将它变成编译执行的目的就是要将简单的程序复杂化,如一个#if语法在Velocity中会被解释成一个节点,显然执行这个#if语法要比真正执行Java中的if语句要复杂不少。虽然表面上只需调用一个树的render方法,可是若是要将这个树变成真正的Java中的if去执行,这个过程要复杂不少。因此咱们要将Velocity的语法翻译成Java语法,而后生成Java类再去执行这个Java类。理论上Velocity是动态解释语言而Java是编译性语言,显然Java的执行效率更高。

如何将Velocity的语法节点变成Java中对应的语法?实现思路大致以下。

仍然沿用Velocity中将一个vm模板解释成一棵AST语法树,可是从新修改这棵树的渲染规则,咱们将从新定义每一个语法节点生成对应的Java语法,而不是渲染出结果。在SimpleNode类中从新定义一个generate方法,这个方法将会执行全部子类的generater方法,它会将每一个Velocity的语法节点转化成Java中对应的语法形式。除这个方法外还有value方法和setValue方法,它们分别是获取这个语法节点的值和设置这个节点的值,而不是输出。

总之,要将全部的Velocity的语法都翻译成对应的Java语法,这样才能将整个vm模板变成一个Java类。那么整个vm又是如何组织成一个Java类的呢?

example_vm是模板example.vm编译成的Java类,它继承了AbstractTemplateInstance类,这个类是编译后模板的父类,也是遵守设计模板中的模板模式来设计的。这个类定义了模板的初始化和销毁的方法,同时定义了一个render方法供外部调用模板渲染,而TemplateInstance类很显然是全部模板的接口类,它定义了全部模板对外提供的方法

TemplateConfig类很是重要,它含有一些模板渲染时须要调用的辅助方法,如记录方法调用的实际对象类型及方法参数的类型,还有一些出错处理措施等。_TRACE方法在执行编译后的模板类时须要记录下vm模板中被执行的方法的执行参数,_COLLE方法当模板中的变量输出时能够触发各类注册的触发事件,如变量为空判断、安全字符转义等。咱们能够发现有个内部类I,这个类只保存一些变量属性,用于缓存每次模板执行时经过Context容器传过来的变量的值。

上面vm例子中的#foreach语法被编译成了一个单独的方法,这是为何呢?由于咱们的模板若是很是大,将全部的代码都放在一个方法中(如render),这个方法可能会超过64KB,咱们知道Java编译器的方法的最大大小限制是64KB,这个问题在JSP中也会存在,全部JSP中引入了标签,每一个标签都被编译成一个方法,也是为了不方法生成的Java类过长而不能编译。

ps:上面代码中还有两个地方要注意:一个地方是$exampleDO.getItemList()代码被解析成_I.exampleDO).getItemList()方法调用(第一次编译时是经过反射调用,屡次编译后经过方法调用),也就是将Velocity的动态反射调用变成了Java的原生方法调用;另一个地方是将静态字符串解析成byte数组,页面的渲染输出改为了字节流输出

方法调用的无反射优化

一个地方是$exampleDO.getItemList()代码被解析成_I.exampleDO).getItemList()方法调用(第一次编译时是经过反射调用,屡次编译后经过方法调用)。

只有当模板真正执行时才会知道$exampleDO变量实际对应的Java对象,才知道这个对象对应的Java类。而要能肯定一个方法,不只要知道这个方法的方法名,还要知道这个方法对应的参数类型。因此在这种状况下要屡次执行才能肯定每一个方法对应的Java对象及方法的参数类型。

第一次编译时不知道变量的类型,因此全部的方法调用都以反射方式执行,$exampleDO.getItemList()的调用变成了_TRACE方法调用,这个方法有点特殊,它会记录下这个$exampleDO.getItemList()此次调用传过来的对象context.get("exampleDO")及方法参数new Object[]{},并以这个方法的hash值做为key保存下来。当第二次编译时遇到$exampleDO.getItemList()语法节点时将会将这个语法节点解析成(Mode) _I.exampleDO).getItemList()。因为一个模板中一次执行并不能执行到全部的方法,因此一次执行并不能将全部的方法调用转变成反射方式。这种状况下就会屡次生成模板对应的Java类及屡次编译。

字符输出改为字节输出

另一个地方是将静态字符串解析成byte数组,页面的渲染输出改为了字节流输出。

静态字符串直接是out.write(_S0),这里的_S0是一个字节数组,而vm模板中是字符串,将字符串转成字节数组是在这个模板类初始化时完成的。字符的编码是很是耗时的,若是咱们将静态字符串提早编码好,那么在最终写Socket流时就会省去这个编码时间,从而提升执行效率。从实际的测试来看,这对提高性能颇有帮助。另外,从代码中还能够发现,若是是变量输出,调用的是out.write(_EVTCK(context,"$str", context.get("str"))),而_EVTCK方法在输出变量以前检查是否有事件须要调用,如XSS安全检查、为空检查等。

与JSP比较

JSP渲染机制

在实际应用中一般用两种方式调用JSP页面,一种方式是直接经过org.apache.jasper. servlet.JspServlet来调用请求的JSP页面,另外一种方式是经过以下方式调用:

两种方式均可以渲染JSP,前一种方式更加方便,只要中配置的路径符合JspServlet就能够直接渲染,后一种方式更加灵活,不须要特别的配置就行。虽然两种调用方式有所区别,可是最终的JSP渲染原理都是同样的。下面以一个最简单的JSP页面为例看它是如何渲染的:

如上面这个index.jsp页面,把它放在Tomcat的webapps/examples/jsp目录下,咱们经过第二种方式来调用,访问一个Servlet,而后在这个Servlet中经过RequestDispatcher来渲染这个JSP页面。调用代码以下:

从图中能够看出,ServletContext根据path来找到对应的Servlet,这个映射是在Mapper.map方法中完成的,Mapper的映射有7种规则,此次映射是经过扩展名“.jsp”来找到JspServlet对应的Wrapper的。而后根据这个JspServlet建立ApplicationDispatcher对象。接下来就和调用其余Servlet同样调用JspServlet的service方法,因为JspServlet专门处理渲染JSP页面,因此这个Servlet会根据请求的JSP文件名将这个JSP包装成JspServletWrapper对象。JSP在执行渲染时会被编译成一个Java类,而这个Java类实际上也是一个Servlet,那么JSP文件又是如何被编译成Servlet的呢?这个Servlet究竟是什么样子的?每个Servlet在Tomcat中都被包装成一个最底层的Wrapper容器,那么每个JSP页面最终都会被编译成一个对应的Servlet,这个Servlet在Tomcat容器中就是对应的JspServletWrapper。

HttpJspBase类是全部JSP编译成Java的基类,这个类也继承了HttpServlet类、实现了HttpJspPage接口,HttpJspBase的service方法会调用子类的_jspService方法。被编译成的Java类的_jspService方法会生成多个变量:pageContext、application、config、session、out和传进来的request、response,显然这些变量咱们均可以直接引用,它们也被称为JSP的内置变量。对比一下JSP页面和生成的Java类能够发现,页面的全部内容都被放在_jspService方法中,其中页面直接输出的HTML代码被翻译成out.write输出,页面中的动态“<%%>”包裹的Java代码直接写到_jspService方法中的相应位置,而“<%=%>”被翻译成out.print输出。

咱们从JspServlet的service方法开始看一下index.jsp是怎么被翻译成index_jsp类的,首先建立一个JspServletWrapper对象,而后建立编译环境类JspCompilationContext,这个类保存了编译JSP文件须要的全部资源,包括动态编译Java文件的编译器。在建立JspServletWrapper对象以前会首先根据jspUri路径检查JspRuntimeContext这个JSP运行环境的集合中对应的JspServletWrapper对象是否已经存在。在JDTCompiler调用generateJava方法时会生产JSP对应的Java文件,将JSP文件翻译成Java类是经过ParserController类完成的,它将JSP文件按照JSP的语法规则解析成一个个节点,而后遍历这些节点来生成最终的Java文件。具体的解析规则能够查看这个类的注释。翻译成Java类后,JDTCompiler再将这个类编译成class文件,而后建立对象并初始化这个类,接下来就是调用这个类的service方法,完成最后的渲染。下图这个过程的时序图。 

 

Velocity与JSP

从上面的JSP渲染机制咱们能够看出JSP文件渲染其实和Velocity的渲染机制很不同,JSP文件实际上执行的是JSP对应的Java类,简单地说就是将JSP的HTML转化成out.write输出,而JSP中的Java代码直接复制到翻译后的Java类中。最终执行的是翻译后的Java类,而Velocity是按照语法规则解析成一棵语法树,而后执行这棵语法树来渲染出结果。因此它们有以下这些区别。

  1. 执行方式不同:JSP是编译执行,而Velocity是解释执行。若是JSP文件被修改了,那么对应的Java类也会被从新编译,而Velocity却不须要,只是会从新生成一棵语法树
  2. 执行效率不一样:从二者的执行方式不一样能够看出,它们的执行效率不同,从理论上来讲,编译执行的效率明显好于解释执行,一个很明显的例子在JSP中方法调用是直接执行的,而Velocity的方法调用是反射执行的,JSP的效率会明显好于Velocity。固然若是JSP中有语法JSTL,语法标签的执行要看该标签的实现复杂度。
  3. 须要的环境支持不同:JSP的执行必需要有Servlet的运行环境,也就是须要ServletContext、HttpServletRequest和HttpServletResponse类。而要渲染Velocity彻底不须要其余环境类的支持,直接给定Velocity模板就能够渲染出结果。因此Velocity不仅应用在Servlet环境中。
相关文章
相关标签/搜索