在MVC開發(fā)模式下,View離不開模板引擎,在java語言中模板引擎使用得最多是jsp、Velocity和FreeMarker,在MVC編程開發(fā)模式中,必不可少的一個(gè)部分是V的部分。V負(fù)責(zé)前端的頁面展示,也就是負(fù)責(zé)生產(chǎn)最終的HTML,V部分通常會對應(yīng)一個(gè)編碼引擎,當(dāng)前眾多的MVC框架都已經(jīng)可以將V部分獨(dú)立開來,可以與眾多的模板引擎集成。
從代碼結(jié)構(gòu)上看,Velocity主要分為app、context、runtime和一些輔助util幾個(gè)部分。
其中app主要封裝了一些接口,暴露給使用者使用。主要有兩個(gè)類,分別是Velocity(單例)和VelocityEngine。
前者主要封裝了一些靜態(tài)接口,可以直接調(diào)用,幫助你渲染模板,只要傳給Velocity一個(gè)模板和模板中對應(yīng)的變量值就可以直接渲染。
VelocityEngine類主要是供一些框架開發(fā)者調(diào)用的,它提供了更加復(fù)雜的接口供調(diào)用者選擇,MVC框架中初始化一個(gè)VelocityEngine:
以上是SPRing MVC創(chuàng)建Velocity模板引擎的VelocityEngine實(shí)例的代碼段,先創(chuàng)建一個(gè)VelocityEngine實(shí)例,再將配置參數(shù)設(shè)置到VelocityEngine的Property中,最終調(diào)用init方法初始化。
Context模塊主要封裝了模板渲染需要的變量,它的主要作用有兩點(diǎn):
便于與其他框架集成,起到一個(gè)適配器的作用,如MVC框架內(nèi)部保存的變量往往在一個(gè)Map中,這樣MVC框架就需要將這個(gè)Map適配到Velocity的context中。Velocity內(nèi)部做數(shù)據(jù)隔離,數(shù)據(jù)進(jìn)入Velocity的內(nèi)部的不同模塊需要對數(shù)據(jù)做不同的處理,封裝不同的數(shù)據(jù)接口有利于模塊之間的解耦。Context類是外部框架需要向Velocity傳輸數(shù)據(jù)必須實(shí)現(xiàn)的接口,具體實(shí)現(xiàn)時(shí)可以集成抽象類AbstractContext,例如,Spring MVC中直接繼承了VelocityContext,調(diào)用構(gòu)造函數(shù)創(chuàng)建Velocity需要的數(shù)據(jù)結(jié)構(gòu)。
另外一個(gè)接口InternetEventContext主要是為擴(kuò)展Velocity事件處理準(zhǔn)備的數(shù)據(jù)接口,當(dāng)你擴(kuò)展了事件處理、需要操作數(shù)據(jù)時(shí)可以實(shí)現(xiàn)這個(gè)接口,并且處理你需要的數(shù)據(jù)。
整個(gè)Velocity的核心模塊在runtime package下,這里會將加載的模板解析成JavaCC語法樹,Velocity調(diào)用mergeTemplate方法時(shí)會渲染整棵樹,并輸出最終的渲染結(jié)果。
RuntimeInstance類為整個(gè)Velocity渲染提供了一個(gè)單例模式,它也是Velocity的一個(gè)門面,封裝了渲染模板需要的所有接口,拿到了這個(gè)實(shí)例就可以完成渲染過程了。它與VelocityEngine不同,VelocityEngine代表了整個(gè)Velocity引擎,它不僅包括模板渲染,還包括參數(shù)設(shè)置及數(shù)據(jù)的封裝規(guī)則,RuntimeInstance僅僅代表一個(gè)模板的渲染狀態(tài)。
下面是一段Velocity的模板代碼vm和這段代碼解析成的語法樹:
Velocity渲染這段代碼將從根節(jié)點(diǎn)ASTproces開始,按照深度優(yōu)先遍歷算法開始遍歷整棵樹,遍歷的代碼如下所示:
如代碼所示,依次執(zhí)行當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)的render方法,每個(gè)節(jié)點(diǎn)的渲染規(guī)則都在render方法中實(shí)現(xiàn),對應(yīng)到上面的vm代碼,#foreach節(jié)點(diǎn)對應(yīng)到ASTDirective。這種類型的節(jié)點(diǎn)是一個(gè)特殊的節(jié)點(diǎn),它可以通過directiveName來表示不同類型的節(jié)點(diǎn),目前ASTDirective已經(jīng)有多個(gè),如#break、#parse、#include、#define等都是ASTDirective類型的節(jié)點(diǎn)。這種類型的節(jié)點(diǎn)通常都有一個(gè)特點(diǎn),就是它們的定義類似于一個(gè)函數(shù)的定義,一個(gè)directiveName后面跟著一對括號,括號里含有參數(shù)和一些關(guān)鍵詞,如#foreach,directiveName是foreach,括號中的$i是ASTReference類型,in是關(guān)鍵詞ASTWord類型,[1 ..10]是一個(gè)數(shù)組類型ASTIntegerRange,在#foreach和#end之間的所有內(nèi)容都由ASTBlock表示。
所謂的指令指的就是在頁面上能用一些類似標(biāo)簽的東西。Velocity默認(rèn)的指令文件位置在org/apache/velocity/runtime/defaults/directive.properties。
在這個(gè)文件中定義了一些默認(rèn)的指令,例如:
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的語法相對簡單,所以它的語法節(jié)點(diǎn)并不是很多,總共有50幾個(gè),它們可以劃分為如下幾種類型。
塊節(jié)點(diǎn)類型:主要用來表示一個(gè)代碼塊,它們本身并不表示某個(gè)具體的語法節(jié)點(diǎn),也不會有什么渲染規(guī)則。這種類型的節(jié)點(diǎn)主要由ASTReference、ASTBlock和ASTExpression等組成。擴(kuò)展節(jié)點(diǎn)類型:這些節(jié)點(diǎn)可以被擴(kuò)展,可以自己去實(shí)現(xiàn),如我們上面提到的#foreach,它就是一個(gè)擴(kuò)展類型的ASTDirective節(jié)點(diǎn),我們同樣可以自己再擴(kuò)展一個(gè)ASTDirective類型的節(jié)點(diǎn)。中間節(jié)點(diǎn)類型:位于樹的中間,它的下面有子節(jié)點(diǎn),它的渲染依賴于子節(jié)點(diǎn)才能完成,如ASTIfStatement和ASTSetDirective等。葉子節(jié)點(diǎn):它位于樹的葉子上,沒有子節(jié)點(diǎn),這種類型的節(jié)點(diǎn)要么直接輸出值,要么寫到writer中,如ASTText和ASTTrue等。Velocity讀取vm模板根據(jù)JavaCC語法分析器將不同類型的節(jié)點(diǎn)按照上面的幾個(gè)類型解析成一個(gè)完整的語法樹。
在調(diào)用render方法之前,Velocity會調(diào)用整個(gè)節(jié)點(diǎn)樹上所有節(jié)點(diǎn)的init方法來對節(jié)點(diǎn)做一些預(yù)處理,如變量解析、配置信息獲取等。這非常類似于Servlet實(shí)例化時(shí)調(diào)用init方法。Velocity在加載一個(gè)模板時(shí)也只會調(diào)用init方法一次,每次渲染時(shí)調(diào)用render方法就如同調(diào)用Servlet的service方法一樣。
#set語法可以創(chuàng)建一個(gè)Velocity的變量,#set語法對應(yīng)的Velocity語法樹是ASTSetDirective類,翻開這個(gè)類的代碼,可以發(fā)現(xiàn)它有兩個(gè)子節(jié)點(diǎn):分別是RightHandSide和LeftHandSide,分別代表“=”兩邊的表達(dá)式值。與Java語言的賦值操作有點(diǎn)不一樣的是,左邊的LeftHandSide可能是一個(gè)變量標(biāo)識符,也可能是一個(gè)set方法調(diào)用。變量標(biāo)識符很好理解,如前面的#set($var=“偶數(shù)”),另外是一個(gè)set方法調(diào)用,如#set($person.name=”junshan”),這實(shí)際上相當(dāng)于Java中person.setName(“junshan”)方法的調(diào)用。
#set語法如何區(qū)分左邊是變量標(biāo)識符還是set方法調(diào)用?看一下ASTSetDirective類的render方法:
從代碼中可以看到,先取得右邊表達(dá)式的值,然后根據(jù)左邊是否有子節(jié)點(diǎn)判斷是變量標(biāo)識符還是調(diào)用set方法。通過#set語法創(chuàng)建的變量是否有有效范圍,從代碼中可以看到會將這個(gè)變量直接放入context中,所以這個(gè)變量在這個(gè)vm模板中是一直有效的,它的有效范圍和context也是一致的。所以在vm模板中不管在什么地方通過#set創(chuàng)建的變量都是一樣的,它對整個(gè)模板都是可見的。
Velocity的方法調(diào)用方式有多種,它和我們熟悉的Java的方法調(diào)用還是有一些區(qū)別之處的,如果你不熟悉,可能會產(chǎn)生一些誤解,下面舉例介紹一下。
Velocity通過ASTReference類來表示一個(gè)變量和變量的方法調(diào)用,ASTReference類如果有子節(jié)點(diǎn),就表示這個(gè)變量有方法調(diào)用,方法調(diào)用同樣是通過“.”來區(qū)分的,每一個(gè)點(diǎn)后面會對應(yīng)一個(gè)方法調(diào)用。ASTReference有兩種類型的子節(jié)點(diǎn),分別是ASTIdentifier和ASTMethod。它們分別代表兩種類型的方法調(diào)用,其中ASTIdentifier主要表示隱式的“get”和“set”類型的方法調(diào)用。而ASTMethod表示所有其他類型的方法調(diào)用,如所有帶括號的方法調(diào)用都會被解析成ASTMethod類型的節(jié)點(diǎn)。
所謂隱式方法調(diào)用在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()Set 繼承SetExecutor:當(dāng)Velocity在解析#set($person.name=”junshan”)時(shí),它會找到$person對應(yīng)的對象,然后創(chuàng)建一個(gè)SetPropertyExecutor對象并查找這個(gè)對象是否有setname(String)方法,如果沒有,再查找setName(String)方法,如果再沒有,那么再創(chuàng)建MapSetExecutor對象,看看$person對應(yīng)的對象是不是一個(gè)Map。如果是Map,就調(diào)用Map的put方法,如果不是Map,再創(chuàng)建一個(gè)PutExecutor對象,檢查一下$person對應(yīng)的對象有沒有put(String)方法,如果存在就調(diào)用對象的put方法。
Get:除去Set類型的方法調(diào)用,其他的方法調(diào)用都繼承了AbstractExecutor類,如#set($name=$person.name)中解析$person.name時(shí),創(chuàng)建PropertyExecutor對象封裝可能存在的getname(String)或getName(String)方法。否則創(chuàng)建MapGetExecutor檢查$person變量是否是一個(gè)Map對象。如果不是,創(chuàng)建GetExecutor對象檢查$person變量對應(yīng)的對象是否有g(shù)et(“Name”)方法。如果還沒有,創(chuàng)建BooleanPropertyExecutor對象并檢查$person變量對應(yīng)的對象是否有isname()或者isName()方法。找到對應(yīng)的方法后,將相應(yīng)的java.lang.reflect.Method對象封裝在對應(yīng)的封裝對象中。
以上這些查找順序中,某個(gè)方法找到后就直接返回某種類型的Executor對象包裝的Method,然后通過反射調(diào)用Method的invoke方法。Velocity的反射調(diào)用是通過Introspector類來完成的,它定義了類對象的方法查找規(guī)則。
顯式調(diào)用:除去以上對兩種隱式的方法調(diào)用的封裝外,Velocity還有一種簡單的方法調(diào)用方式,就是帶有括號的方法調(diào)用,如$person.setName(“junshan”),這種精確的方法調(diào)用會直接查找變量$person對應(yīng)的對象有沒有setName(String)方法,如果有,會直接返回一個(gè)VelMethod對象,這個(gè)對象是對通用的方法調(diào)用的封裝,它可以處理$person對應(yīng)的對象是數(shù)組類型或靜態(tài)類時(shí)的情況。數(shù)組的情況如string=newString[]{“a”,”b”,”c”},要取的第二個(gè)值在Java中可以通過string[1]來取,但在Velocity中可以通過$string.get(1)取得數(shù)組的第二個(gè)值。為何能這樣做呢?可以看一下Velocity中相應(yīng)的代碼:
從上面的代碼中我們可以發(fā)現(xiàn),精確查找方法的規(guī)則是查找$person對應(yīng)的對象是否有指定的方法,然后檢查該對象是否是數(shù)組,如果是數(shù)組,把它封裝成List,然后按照ArrayListWrapper類去代理訪問數(shù)組的相應(yīng)值。如果$person對應(yīng)的對象是靜態(tài)類,可以調(diào)用其靜態(tài)方法。
#if和#else節(jié)點(diǎn)是Velocity中的邏輯判斷節(jié)點(diǎn),它的語法規(guī)則幾乎和Java是一樣的,主要的不同點(diǎn)在條件判斷上,如Velocity中判斷#if($express)為true的情況是只要$express變量的值不為null和false就行,而Java中顯然不能這樣判斷。
除單個(gè)變量的值判斷之外,Velocity還支持Java的各種表達(dá)式判斷,如“>”、“<”、“==”和邏輯判斷“&&”、“||”等。每一個(gè)判斷條件都會對應(yīng)一個(gè)節(jié)點(diǎn)類,如“==”對應(yīng)的類為ASTEQNode,判斷兩個(gè)值是否相等的條件為:先取得等號兩邊的值,如果是數(shù)字,比較兩個(gè)數(shù)字的大小是否相等,再判斷兩邊的值是否都是null,都為null則相等,否則其中一個(gè)為null,肯定不等;再次就是取這兩個(gè)值的toString(),比較這兩個(gè)值的字符值是否相等。值得注意的是,Velocity中并不能像Java中那樣判斷兩個(gè)變量是否是同一個(gè)變量,也就是object1==object2與object1. equals(object2)在Velocity中是一樣的效果。
特別要注意的是,很多人在寫Velocity代碼時(shí)有類似這樣的寫法,如#if("$example.user"== "null")和#if("$example.flag" == "true"),這些寫法都是不正確的,正確的寫法是#if($example.user)和#if($example.flag)。
若要使用 #ifnull() 或 #ifnotnull(), 要使用#ifnull ($foo)這個(gè)特性必須在velocity.properties文件中加入:
userdirective = org.apache.velocity.tools.generic.directive.Ifnulluserdirective = org.apache.velocity.tools.generic.directive.Ifnotnull
如果有多個(gè)#elseif節(jié)點(diǎn),Velocity會依次判斷每個(gè)子節(jié)點(diǎn),從#if節(jié)點(diǎn)的render方法代碼中我們可以看出,第一個(gè)子節(jié)點(diǎn)就是#if中的表達(dá)式判斷,這個(gè)表達(dá)式的值為true則執(zhí)行第二個(gè)子節(jié)點(diǎn),第二個(gè)子節(jié)點(diǎn)就是#if下面的代碼塊。如果#if中表達(dá)式判斷為false,則繼續(xù)執(zhí)行后面的子節(jié)點(diǎn),如果存在其他子節(jié)點(diǎn)肯定就是#elseif或者#else節(jié)點(diǎn)了,其中任何一個(gè)為true將會執(zhí)行這個(gè)節(jié)點(diǎn)的render方法并且會直接返回。
Velocity中的循環(huán)語法只有這一種,它與Java中的for循環(huán)的語法糖形式十分類似,如#foreach($child in $person.children) $person.children表示的是一個(gè)集合,它可能是一個(gè)List集合或者一個(gè)數(shù)組,而$child表示的是每個(gè)從集合中取出的值。從render方法代碼中可以看出,Velocity首先是取得$person.children的值,然后將這個(gè)值封裝成Iterator集合,然后依次取出這個(gè)集合中的每一個(gè)值,將這個(gè)值以$child為變量標(biāo)識符放入context中。除此以外需要特別注意的是,Velocity在循環(huán)時(shí)還在context中放入了另外兩個(gè)變量,分別是counterName和hasNextName,這兩個(gè)變量的名稱分別在配置文件配置項(xiàng)directive.foreach.counter.name和directive.foreach.iterator.name中定義,它們表示當(dāng)前的循環(huán)計(jì)數(shù)和是否還有下一個(gè)值。前者相當(dāng)于for(int i=1;i<10;i++)中的i值,后者相當(dāng)于while(it.hasNext())中的it.hasNext()的值,這兩個(gè)值在#foreach的循環(huán)體中都有可能用到。由于elementKey、counterName和hasNextName是在#foreach中臨時(shí)創(chuàng)建的,如果當(dāng)前的context中已經(jīng)存在這幾個(gè)變量,要把原始的變量值保存起來,以便在這個(gè)#foreach執(zhí)行結(jié)束后恢復(fù)。如果context中沒有這幾個(gè)變量,那么#foreach執(zhí)行結(jié)束后要刪除它們,這就是代碼最后部分做的事情,這與我們前面介紹的#set語法沒有范圍限制不同,#foreach中臨時(shí)產(chǎn)生的變量只在#foreach中有效。
#parse語法也是Velocity中十分常用的語法,它的作用是可以讓我們對Velocity模板進(jìn)行模塊化,可以將一些重復(fù)的模塊抽取出來單獨(dú)放在一個(gè)模板中,然后在其他模板中引入這個(gè)重用的模板,這樣可以增加模板的可維護(hù)性。而#parse語法就提供了引入一個(gè)模板的功能,如#parse(‘head.vm’)引入一個(gè)公共頁頭。當(dāng)然head.vm可以由一個(gè)變量來表示。#parse和#foreach一樣都是通過擴(kuò)展節(jié)點(diǎn)ASTDirective來解析的,所以#parse和#foreach一樣都共享當(dāng)前模板執(zhí)行環(huán)境的上下文。雖然#parse是單獨(dú)一個(gè)模板,但是這個(gè)模板中變量的值都在#parse所在的模板中取得。Velocity中的#parse我們可以僅理解為只是將一段vm代碼放在一個(gè)單獨(dú)的模板中,其他沒有任何變化。 從代碼中可以看出執(zhí)行分為三部分,首先取得#parse(‘head.vm’)中的head.vm的模板名,然后調(diào)用getTemplate獲取head.vm對應(yīng)的模板對象,再調(diào)用該模板對應(yīng)的整個(gè)語法樹的render方法執(zhí)行渲染。#parse語法的執(zhí)行和其他的模板的渲染沒有什么區(qū)別,只不過模板渲染時(shí)共用了父模板的context和writer對象而已。
Velocity的事件處理機(jī)制所涉及的類在org.apache.velocity.app.event下面, EventHandler是所有類的父接口,EventHandler類有5個(gè)子類,分別代表5種不同的事件處理類型。
ReferenceInsertionEventHandler:表示針對Velocity中變量的事件處理,當(dāng)Velocity在渲染輸出某個(gè)“$”表示的變量時(shí)可以對這個(gè)變量做修改,如對這個(gè)變量的值做安全過濾以防止惡意JS代碼出現(xiàn)在頁面中等。NullSetEventHandler:顧名思義是對#set語法賦值為null時(shí)的事件做處理。MethodExceptionEventHandler:這個(gè)事件是對Velocity在反射執(zhí)行某個(gè)方法調(diào)用時(shí)出錯(cuò)后,有機(jī)會做一些處理,如捕獲異常、控制返回一些特殊值等。InvalidReferenceEventHandler:表示Velocity在解析“$”變量出現(xiàn)沒有找到對應(yīng)的對象時(shí)做如何處理。IncludeEventHandler:在處理#include和#parse時(shí)提供了處理和修改加載外部資源的機(jī)會。Velocity提供的這些事件處理機(jī)制也為我們擴(kuò)展Velocity提供了機(jī)會,如果你想擴(kuò)展Velocity,必須對它的事件處理機(jī)制有很好的理解。
如何調(diào)用到擴(kuò)展的EventHandler?Velocity提供了兩種方式,Velocity在渲染時(shí)遇到符合的事件都會檢查以下的EventCartridge:
把你新創(chuàng)建的EventHandler直接加到org.apache.velocity.runtime.RuntimeInstance類的eventCartridge屬性中,直接將自定義的EventHandler通過配置項(xiàng)eventCartridge.classes來設(shè)置,Velocity在初始化RuntimeInstance時(shí)會解析配置項(xiàng),然后會實(shí)例化EventHandler。把自定義的EventHandler加到自己創(chuàng)建的EventCartridge對象中,然后在渲染時(shí)把這個(gè)EventCartridge對象通過調(diào)用attachToContext方法加到context中,但是這個(gè)context必須要繼承InternalEventContext接口,因?yàn)橹挥羞@個(gè)接口才提供了attachToContext方法和取得EventCartridge的getEventCartridge方法。動態(tài)地設(shè)置EventHandler,只要將EventHandler加到渲染時(shí)的context中,Velocity在渲染時(shí)就能調(diào)用它。EventCartridge中保存了所有的EventHandler,并且EventCartridge把它們分別保存在5個(gè)不同的屬性集合中,分別是referenceHandlers、nullSetHandlers、methodExceptionHandlers、includeHandlers和invalidReferenceHandlers。如何找到EventHandle?Velocity在渲染時(shí)分別在兩個(gè)地方檢查可能存在的EventHandler,那就是RuntimeInstance對象和渲染時(shí)的context對象,這兩個(gè)對象在Velocity渲染時(shí)隨時(shí)都能訪問到。何時(shí)被觸發(fā)?有一個(gè)類EventHandlerUtil,它就負(fù)責(zé)在合適的事件觸發(fā)時(shí)調(diào)用事件處理接口來處理事件。如變量在輸出到頁面之前會調(diào)用value = EventHandlerUtil.referenceInsert(rsvc, context, literal(), value)來檢查是否有referenceHandlers需要調(diào)用。其他事件也是類似處理方式。
ps:
擴(kuò)展Velocity的事件處理會涉及對Context的處理,Velocity增加了一個(gè)ContextAware接口,如果你實(shí)現(xiàn)的EventHandler需要訪問Context,那么可以繼承這個(gè)接口。Velocity在調(diào)用EventHandler之前會把渲染時(shí)的context設(shè)置到你的EventHandler中,這樣你就可以在EventHandler中取到context了。如果要訪問RuntimeServices對象,同樣可以繼承RuntimeServicesAware接口。
Velocity還支持另外一種擴(kuò)展方式,就是在渲染某個(gè)變量的時(shí)候判斷這個(gè)變量是不是Renderable類的實(shí)例,如果是,將會調(diào)用這個(gè)實(shí)例的render( InternalContextAdapter context, Writer writer)方法,這種調(diào)用是隱式調(diào)用,也就是不需要在模板中顯式調(diào)用render()方法。
程序的語言層次結(jié)構(gòu)和這個(gè)語言的執(zhí)行效率形成一對倒立的三角形結(jié)構(gòu)。從圖中可以看出,越是上層的高級語言,它的執(zhí)行效率往往越低。這很好理解,因?yàn)樽畹讓拥某绦蛘Z言只有計(jì)算機(jī)能明白,與人的思維很不接近,為什么我們開發(fā)出這么多上層語言,很重要的目的就是對底層的程序做封裝,使得我們開發(fā)更方便,很顯然這些經(jīng)過重重封裝的語言的執(zhí)行效率肯定比沒有經(jīng)過封裝的底層程序語言的效率要差很多,否則和硬件相關(guān)的驅(qū)動程序也不會用C語言或匯編語言來實(shí)現(xiàn)了。
程序的本質(zhì)是數(shù)據(jù)結(jié)構(gòu)加上算法,算法是過程,而數(shù)據(jù)結(jié)構(gòu)是載體。程序語言也是同樣的道理,越是高級的程序語言必然數(shù)據(jù)結(jié)構(gòu)越抽象化,這里的抽象化是指它們的數(shù)據(jù)結(jié)構(gòu)與人的思維越接近。有些語言(如Python)的語法規(guī)則非常像我們的人語言,即使沒有學(xué)過編程的人也很容易理解它。這里所說的數(shù)據(jù)結(jié)構(gòu)去抽象化是指把需要調(diào)用底層的接口的程序改由我們自己去實(shí)現(xiàn),減少這個(gè)程序的封裝程度,從而達(dá)到提升性能的目的,所以并不是改變程序語法。
先舉一個(gè)例子,我們想從數(shù)據(jù)庫中去掉一行數(shù)據(jù),目前的環(huán)境中已經(jīng)有人提高了一個(gè)調(diào)數(shù)據(jù)庫查詢的接口,這個(gè)接口的實(shí)現(xiàn)使用了iBatis作為數(shù)據(jù)層調(diào)用數(shù)據(jù)庫查詢數(shù)據(jù),實(shí)際上它封裝了對象與數(shù)據(jù)字段的關(guān)系映射及管理數(shù)據(jù)庫連接池等。使用起來很方便,但是它的執(zhí)行效率是不是比我們直接寫一個(gè)簡單的JDBC連接、提交一個(gè)SQL語句的效率高呢?很顯然,后面的執(zhí)行效率更高,拋去其他因素,顯然沒有經(jīng)過封裝的復(fù)雜程序要比簡單的調(diào)用上層接口效率要高很多。所以我們要做的就是適當(dāng)?shù)刈屛覀兊某绦驈?fù)雜一點(diǎn),而不要偷懶,也許這樣我們的程序效率會增加不少。
我們知道與不同國家的人交流是要通過翻譯的,但是這個(gè)翻譯實(shí)在是耗時(shí)間。程序設(shè)計(jì)同樣存在翻譯的問題,如我們的編碼問題,美國人的所有字符一個(gè)字節(jié)就能全部表示,所以他們的所有字符就是一個(gè)字節(jié),也就是一個(gè)ASSCII碼,所以對他們來說不存在字符編碼問題,但是對其他國家的程序員來說,不得不面臨一個(gè)讓人頭疼的字符編碼問題,需要將字節(jié)與字符之間來回翻譯,而且還很容易出現(xiàn)錯(cuò)誤。我們要盡量減少這種翻譯,至少在真正與人交流時(shí)把一些經(jīng)常用的詞匯提前就翻譯好,從而在面對面交流時(shí)減少需要翻譯的詞匯的數(shù)量,從而提升交流效率。
現(xiàn)在的網(wǎng)頁基本上都是動態(tài)網(wǎng)頁,但是所謂的動態(tài)網(wǎng)頁中仍然有很多靜態(tài)的東西,如模板中仍然有很多是HTML代碼,它們和一些變量共同拼接成一個(gè)完整的頁面,但是這些內(nèi)容從程序員寫出來到最終在瀏覽器里渲染,都是一成不變的。既然是不變的,那么就可以對它們做一些預(yù)處理,如提前將它們編碼或者將它們放到CDN上。另外,盡量把一些變化的內(nèi)容轉(zhuǎn)化成不變的內(nèi)容,如我們可能將一個(gè)URL作為一個(gè)變量傳給模板去渲染,但是這個(gè)URL中真正變化的僅僅是其中的一個(gè)參數(shù),整個(gè)主體肯定是不會變化的,所以我們?nèi)匀豢梢詮淖兓膬?nèi)容中分離出一部分作為不變的來處理。這些都是細(xì)節(jié),但是當(dāng)這些細(xì)節(jié)組合在一起時(shí)往往就會帶來讓你意想不到的好的結(jié)果。
Velocity渲染模板是先把模板解析成一棵語法樹,然后去遍歷這棵樹分別渲染每個(gè)節(jié)點(diǎn),知道了它的工作原理,我們就可以根據(jù)它的工作機(jī)制來優(yōu)化渲染的速度。既然是遍歷這棵樹來渲染節(jié)點(diǎn)的,而且是順序遍歷的,那么很容易想到有兩種辦法來優(yōu)化渲染:
減少樹的總節(jié)點(diǎn)數(shù)量。減少渲染耗時(shí)的節(jié)點(diǎn)數(shù)量。改變Velocity的解釋執(zhí)行,變?yōu)榫幾g執(zhí)行。方法調(diào)用的無反射優(yōu)化字符輸出改成字節(jié)輸出去掉頁面輸出中多余的非中文空格。我們知道,頁面的HTML輸出中多余的空格是不會在HTML的展示時(shí)有作用的,多個(gè)連續(xù)的空格最終都只會顯示一個(gè)空格的間距,除非你使用“ ”表示空格。雖然多余的空格并不能影響HTML的頁面展示樣式,但是服務(wù)端頁面渲染和網(wǎng)絡(luò)數(shù)據(jù)傳輸這些空格和其他字符沒有區(qū)別,同樣要做處理,這樣的話,這些空格就會造成時(shí)間和空間維度上的浪費(fèi),所以完全可以將多個(gè)連續(xù)的空格合并成一個(gè),從而既減少了字符又不會影響頁面展示。壓縮TAB和換行。同樣的道理,還可以將TAB字符合并成一個(gè),以及將多余的換行也合并一下,也能減少不少字符。合并相同的數(shù)據(jù)。在模板中有很多相同數(shù)據(jù)在循環(huán)中重復(fù)輸出,如類目、商品、菜單等,可以將相同的重復(fù)內(nèi)容提取出來合并在CSS中或者用JS來輸出。異步渲染。將一些靜態(tài)內(nèi)容抽取出來改成異步渲染,只在用戶確實(shí)需要時(shí)再向服務(wù)器去請求,也能夠減少很多不必要的數(shù)據(jù)傳輸。既然一個(gè)模板輸出的內(nèi)容是確定的,那么這個(gè)模板的vm代碼應(yīng)該是固定的,減少節(jié)點(diǎn)數(shù)量必然刪去一部分vm代碼才能做到?其實(shí)并不是這樣的,雖然最終渲染出來的頁面是一樣的,但是vm的寫法卻有很大不同,筆者在檢查vm代碼時(shí)遇到很多不優(yōu)美的寫法,導(dǎo)致無謂增加了很多不必要的語法節(jié)點(diǎn)。如下面一段代碼:
這段代碼實(shí)際上只是要計(jì)算一個(gè)值,但是由于不熟悉Velocity的一些語法,寫得很麻煩,其實(shí)只要一個(gè)表達(dá)式就好了,如下:
這樣可以減少很多語法節(jié)點(diǎn)。
Velocity的方法調(diào)用是通過反射執(zhí)行的,顯然反射執(zhí)行方法是耗時(shí)的,那么又如何減少反射執(zhí)行的方法呢?這個(gè)改進(jìn)就如同Java中一樣,可以增加一些中間變量來保存中間值,而減少反射方法的調(diào)用。如在一個(gè)模板中要多次調(diào)用到$person.name,那么可以通過#set創(chuàng)建一個(gè)變量$name來保存$person.name這個(gè)反射方法的執(zhí)行結(jié)果。如#set($name=$person.name),這樣雖然增加了一個(gè)#set節(jié)點(diǎn),但是如果能減少多次反射調(diào)用仍然是很值得的。
另外,Velocity本身提供了一個(gè)#macro語法,它類似于定義一個(gè)方法,然后可以調(diào)用這個(gè)方法,但在沒有必要時(shí)盡量少用這種語法節(jié)點(diǎn),這些語法節(jié)點(diǎn)比較耗時(shí)。還有一些大數(shù)計(jì)算等,最好定義在Java中,通過調(diào)用Java中的方法可以加快Velocity的執(zhí)行效率。
也就是將vm模板先編譯成Java類,再去執(zhí)行這個(gè)Java對象,從而渲染出頁面。Sketch模版引擎,主要分為兩個(gè)部分:運(yùn)行時(shí)環(huán)境和編譯時(shí)環(huán)境。前者主要用來將模板渲染成HTML,后者主要是把模板編譯成Java類。當(dāng)請求渲染一個(gè)vm模板時(shí),通過調(diào)用單例RuntimeServer獲取一個(gè)模板編譯后的Java對象,然后調(diào)用這個(gè)模板對應(yīng)的Java對象的render方法渲染出結(jié)果。如果是第一次調(diào)用一個(gè)vm模板,Sketch框架將會加載該vm模板,并將這個(gè)vm模板編譯成Java,然后實(shí)例化該Java類,實(shí)例化對象放入RuntimeContext集合中,并根據(jù)Context容器中的變量對應(yīng)的對象值渲染該模板。一個(gè)模板將會被多次編譯,這是一個(gè)不斷優(yōu)化的過程。
我們優(yōu)化Velocity模板的一個(gè)目的就是將模板的解釋執(zhí)行變?yōu)榫幾g執(zhí)行,從前面的理論分析可知,vm中的語法最終被解釋成一棵語法樹,然后通過執(zhí)行這棵語法樹來渲染出結(jié)果。我們要將它變成編譯執(zhí)行的目的就是要將簡單的程序復(fù)雜化,如一個(gè)#if語法在Velocity中會被解釋成一個(gè)節(jié)點(diǎn),顯然執(zhí)行這個(gè)#if語法要比真正執(zhí)行Java中的if語句要復(fù)雜很多。雖然表面上只需調(diào)用一個(gè)樹的render方法,但是如果要將這個(gè)樹變成真正的Java中的if去執(zhí)行,這個(gè)過程要復(fù)雜很多。所以我們要將Velocity的語法翻譯成Java語法,然后生成Java類再去執(zhí)行這個(gè)Java類。理論上Velocity是動態(tài)解釋語言而Java是編譯性語言,顯然Java的執(zhí)行效率更高。
如何將Velocity的語法節(jié)點(diǎn)變成Java中對應(yīng)的語法?實(shí)現(xiàn)思路大體如下。
仍然沿用Velocity中將一個(gè)vm模板解釋成一棵AST語法樹,但是重新修改這棵樹的渲染規(guī)則,我們將重新定義每個(gè)語法節(jié)點(diǎn)生成對應(yīng)的Java語法,而不是渲染出結(jié)果。在SimpleNode類中重新定義一個(gè)generate方法,這個(gè)方法將會執(zhí)行所有子類的generater方法,它會將每個(gè)Velocity的語法節(jié)點(diǎn)轉(zhuǎn)化成Java中對應(yīng)的語法形式。除這個(gè)方法外還有value方法和setValue方法,它們分別是獲取這個(gè)語法節(jié)點(diǎn)的值和設(shè)置這個(gè)節(jié)點(diǎn)的值,而不是輸出。
總之,要將所有的Velocity的語法都翻譯成對應(yīng)的Java語法,這樣才能將整個(gè)vm模板變成一個(gè)Java類。那么整個(gè)vm又是如何組織成一個(gè)Java類的呢?
example_vm是模板e(cuò)xample.vm編譯成的Java類,它繼承了AbstractTemplateInstance類,這個(gè)類是編譯后模板的父類,也是遵照設(shè)計(jì)模板中的模板模式來設(shè)計(jì)的。這個(gè)類定義了模板的初始化和銷毀的方法,同時(shí)定義了一個(gè)render方法供外部調(diào)用模板渲染,而TemplateInstance類很顯然是所有模板的接口類,它定義了所有模板對外提供的方法
TemplateConfig類非常重要,它含有一些模板渲染時(shí)需要調(diào)用的輔助方法,如記錄方法調(diào)用的實(shí)際對象類型及方法參數(shù)的類型,還有一些出錯(cuò)處理措施等。_TRACE方法在執(zhí)行編譯后的模板類時(shí)需要記錄下vm模板中被執(zhí)行的方法的執(zhí)行參數(shù),_COLLE方法當(dāng)模板中的變量輸出時(shí)可以觸發(fā)各種注冊的觸發(fā)事件,如變量為空判斷、安全字符轉(zhuǎn)義等。我們可以發(fā)現(xiàn)有個(gè)內(nèi)部類I,這個(gè)類只保存一些變量屬性,用于緩存每次模板執(zhí)行時(shí)通過Context容器傳過來的變量的值。
上面vm例子中的#foreach語法被編譯成了一個(gè)單獨(dú)的方法,這是為什么呢?因?yàn)槲覀兊哪0迦绻浅4螅瑢⑺械拇a都放在一個(gè)方法中(如render),這個(gè)方法可能會超過64KB,我們知道Java編譯器的方法的最大大小限制是64KB,這個(gè)問題在JSP中也會存在,所有JSP中引入了標(biāo)簽,每個(gè)標(biāo)簽都被編譯成一個(gè)方法,也是為了避免方法生成的Java類過長而不能編譯。
ps:上面代碼中還有兩個(gè)地方要注意:一個(gè)地方是$exampleDO.getItemList()代碼被解析成_I.exampleDO).getItemList()方法調(diào)用(第一次編譯時(shí)是通過反射調(diào)用,多次編譯后通過方法調(diào)用),也就是將Velocity的動態(tài)反射調(diào)用變成了Java的原生方法調(diào)用;另外一個(gè)地方是將靜態(tài)字符串解析成byte數(shù)組,頁面的渲染輸出改成了字節(jié)流輸出。
一個(gè)地方是$exampleDO.getItemList()代碼被解析成_I.exampleDO).getItemList()方法調(diào)用(第一次編譯時(shí)是通過反射調(diào)用,多次編譯后通過方法調(diào)用)。
只有當(dāng)模板真正執(zhí)行時(shí)才會知道$exampleDO變量實(shí)際對應(yīng)的Java對象,才知道這個(gè)對象對應(yīng)的Java類。而要能確定一個(gè)方法,不僅要知道這個(gè)方法的方法名,還要知道這個(gè)方法對應(yīng)的參數(shù)類型。所以在這種情況下要多次執(zhí)行才能確定每個(gè)方法對應(yīng)的Java對象及方法的參數(shù)類型。
第一次編譯時(shí)不知道變量的類型,所以所有的方法調(diào)用都以反射方式執(zhí)行,$exampleDO.getItemList()的調(diào)用變成了_TRACE方法調(diào)用,這個(gè)方法有點(diǎn)特殊,它會記錄下這個(gè)$exampleDO.getItemList()這次調(diào)用傳過來的對象context.get("exampleDO")及方法參數(shù)new Object[]{},并以這個(gè)方法的hash值作為key保存下來。當(dāng)?shù)诙尉幾g時(shí)遇到$exampleDO.getItemList()語法節(jié)點(diǎn)時(shí)將會將這個(gè)語法節(jié)點(diǎn)解析成(Mode) _I.exampleDO).getItemList()。由于一個(gè)模板中一次執(zhí)行并不能執(zhí)行到所有的方法,所以一次執(zhí)行并不能將所有的方法調(diào)用轉(zhuǎn)變成反射方式。這種情況下就會多次生成模板對應(yīng)的Java類及多次編譯。
另外一個(gè)地方是將靜態(tài)字符串解析成byte數(shù)組,頁面的渲染輸出改成了字節(jié)流輸出。
靜態(tài)字符串直接是out.write(_S0),這里的_S0是一個(gè)字節(jié)數(shù)組,而vm模板中是字符串,將字符串轉(zhuǎn)成字節(jié)數(shù)組是在這個(gè)模板類初始化時(shí)完成的。字符的編碼是非常耗時(shí)的,如果我們將靜態(tài)字符串提前編碼好,那么在最終寫Socket流時(shí)就會省去這個(gè)編碼時(shí)間,從而提高執(zhí)行效率。從實(shí)際的測試來看,這對提升性能很有幫助。另外,從代碼中還可以發(fā)現(xiàn),如果是變量輸出,調(diào)用的是out.write(_EVTCK(context,"$str", context.get("str"))),而_EVTCK方法在輸出變量之前檢查是否有事件需要調(diào)用,如XSS安全檢查、為空檢查等。
在實(shí)際應(yīng)用中通常用兩種方式調(diào)用JSP頁面,一種方式是直接通過org.apache.jasper. servlet.JspServlet來調(diào)用請求的JSP頁面,另一種方式是通過如下方式調(diào)用:
兩種方式都可以渲染JSP,前一種方式更加方便,只要中配置的路徑符合JspServlet就可以直接渲染,后一種方式更加靈活,不需要特別的配置就行。雖然兩種調(diào)用方式有所區(qū)別,但是最終的JSP渲染原理都是一樣的。下面以一個(gè)最簡單的JSP頁面為例看它是如何渲染的:
如上面這個(gè)index.jsp頁面,把它放在Tomcat的webapps/examples/jsp目錄下,我們通過第二種方式來調(diào)用,訪問一個(gè)Servlet,然后在這個(gè)Servlet中通過RequestDispatcher來渲染這個(gè)JSP頁面。調(diào)用代碼如下:
從圖中可以看出,ServletContext根據(jù)path來找到對應(yīng)的Servlet,這個(gè)映射是在Mapper.map方法中完成的,Mapper的映射有7種規(guī)則,這次映射是通過擴(kuò)展名“.jsp”來找到JspServlet對應(yīng)的Wrapper的。然后根據(jù)這個(gè)JspServlet創(chuàng)建applicationDispatcher對象。接下來就和調(diào)用其他Servlet一樣調(diào)用JspServlet的service方法,由于JspServlet專門處理渲染JSP頁面,所以這個(gè)Servlet會根據(jù)請求的JSP文件名將這個(gè)JSP包裝成JspServletWrapper對象。JSP在執(zhí)行渲染時(shí)會被編譯成一個(gè)Java類,而這個(gè)Java類實(shí)際上也是一個(gè)Servlet,那么JSP文件又是如何被編譯成Servlet的呢?這個(gè)Servlet到底是什么樣子的?每一個(gè)Servlet在Tomcat中都被包裝成一個(gè)最底層的Wrapper容器,那么每一個(gè)JSP頁面最終都會被編譯成一個(gè)對應(yīng)的Servlet,這個(gè)Servlet在Tomcat容器中就是對應(yīng)的JspServletWrapper。
HttpJspBase類是所有JSP編譯成Java的基類,這個(gè)類也繼承了HttpServlet類、實(shí)現(xiàn)了HttpJspPage接口,HttpJspBase的service方法會調(diào)用子類的_jspService方法。被編譯成的Java類的_jspService方法會生成多個(gè)變量:pageContext、application、config、session、out和傳進(jìn)來的request、response,顯然這些變量我們都可以直接引用,它們也被稱為JSP的內(nèi)置變量。對比一下JSP頁面和生成的Java類可以發(fā)現(xiàn),頁面的所有內(nèi)容都被放在_jspService方法中,其中頁面直接輸出的HTML代碼被翻譯成out.write輸出,頁面中的動態(tài)“<%%>”包裹的Java代碼直接寫到_jspService方法中的相應(yīng)位置,而“<%=%>”被翻譯成out.print輸出。
我們從JspServlet的service方法開始看一下index.jsp是怎么被翻譯成index_jsp類的,首先創(chuàng)建一個(gè)JspServletWrapper對象,然后創(chuàng)建編譯環(huán)境類JspCompilationContext,這個(gè)類保存了編譯JSP文件需要的所有資源,包括動態(tài)編譯Java文件的編譯器。在創(chuàng)建JspServletWrapper對象之前會首先根據(jù)jspUri路徑檢查JspRuntimeContext這個(gè)JSP運(yùn)行環(huán)境的集合中對應(yīng)的JspServletWrapper對象是否已經(jīng)存在。在JDTCompiler調(diào)用generateJava方法時(shí)會生產(chǎn)JSP對應(yīng)的Java文件,將JSP文件翻譯成Java類是通過ParserController類完成的,它將JSP文件按照J(rèn)SP的語法規(guī)則解析成一個(gè)個(gè)節(jié)點(diǎn),然后遍歷這些節(jié)點(diǎn)來生成最終的Java文件。具體的解析規(guī)則可以查看這個(gè)類的注釋。翻譯成Java類后,JDTCompiler再將這個(gè)類編譯成class文件,然后創(chuàng)建對象并初始化這個(gè)類,接下來就是調(diào)用這個(gè)類的service方法,完成最后的渲染。下圖這個(gè)過程的時(shí)序圖。
從上面的JSP渲染機(jī)制我們可以看出JSP文件渲染其實(shí)和Velocity的渲染機(jī)制很不一樣,JSP文件實(shí)際上執(zhí)行的是JSP對應(yīng)的Java類,簡單地說就是將JSP的HTML轉(zhuǎn)化成out.write輸出,而JSP中的Java代碼直接復(fù)制到翻譯后的Java類中。最終執(zhí)行的是翻譯后的Java類,而Velocity是按照語法規(guī)則解析成一棵語法樹,然后執(zhí)行這棵語法樹來渲染出結(jié)果。所以它們有如下這些區(qū)別。
執(zhí)行方式不一樣:JSP是編譯執(zhí)行,而Velocity是解釋執(zhí)行。如果JSP文件被修改了,那么對應(yīng)的Java類也會被重新編譯,而Velocity卻不需要,只是會重新生成一棵語法樹。執(zhí)行效率不同:從兩者的執(zhí)行方式不同可以看出,它們的執(zhí)行效率不一樣,從理論上來說,編譯執(zhí)行的效率明顯好于解釋執(zhí)行,一個(gè)很明顯的例子在JSP中方法調(diào)用是直接執(zhí)行的,而Velocity的方法調(diào)用是反射執(zhí)行的,JSP的效率會明顯好于Velocity。當(dāng)然如果JSP中有語法JSTL,語法標(biāo)簽的執(zhí)行要看該標(biāo)簽的實(shí)現(xiàn)復(fù)雜度。需要的環(huán)境支持不一樣:JSP的執(zhí)行必須要有Servlet的運(yùn)行環(huán)境,也就是需要ServletContext、HttpServletRequest和HttpServletResponse類。而要渲染Velocity完全不需要其他環(huán)境類的支持,直接給定Velocity模板就可以渲染出結(jié)果。所以Velocity不只應(yīng)用在Servlet環(huán)境中。新聞熱點(diǎn)
疑難解答