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環境中。
相關文章
相關標籤/搜索