程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> JSP編程 >> 關於JSP >> jsp編譯過程

jsp編譯過程

編輯:關於JSP

j2ee規范中對jsp的編譯有個規范:第一步,先編譯出來一個xml文件, 第二部再從這個xml文件編譯為一個java文件
例如: test.jsp
    <%!
        int a = 1;
        private String sayHello(){return "hello";}
    %>
    <%
        int a = 1;
    %>
    <h1>Hello World</h1>
第一步,先編譯為一個xml文件,結果如下
    <jsp:declare>
    int a = 1;
    private String sayHello(){return "hello";}
    </jsp:declare>
    <jsp:scriptlet>
    int a = 1;
    </jsp:scriptlet>
    &lt;h1&gt;Hello World&lt;/h1&gt;
第三步,再編譯為一個java文件, 大致結果如下
    public class _xxx_test{
        int a = 1;
        private String sayHello(){return "hello";}

        public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException{

            JspWriter out = xxxx.getWriter();

            int a = 1;
            out.write("<h1>Hello World</h1>");
        }
    }

從中可以看出編譯過程, 編譯器依次讀入文本, 遇到<%@就認為這是個jsp指令, 指令是對編譯和執行這個jsp生效的.
當遇到<%!它的時候就認為這是個聲明, 其中的內容會直接生成為類的類屬性或者類方法, 這個看裡面是怎麼寫的,
例如: int a = 1; 就認為這是個類屬性.

當遇到<%它的時候就認為這是個腳本, 會被放置到默認的方法裡面的.

以上是jsp的編譯過程, 還沒有說對標簽怎麼編譯, 後面再說.

有個問題, 當編譯器遇到<%的時候,會依次讀入後續內容直到遇到%>, 如果裡面的java代碼裡面包含了個字符串,這個字符串的內容是%>,怎麼辦?
我知道的是像tomcat是不會處理這種情況的,也就是說jsp的編譯器並不做語法檢查, 只解析字符串, 上面的這種情況編譯出來的結果就是錯的了,下一步再編譯為class
文件的時候就會報未結束的字符常量. 例如:
<%
    String s = "test%>"
%>

編譯出來的結果大致如下:
    public class _xxx_test{
        public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException{
            JspWriter out = xxxx.getWriter();
            String s = "test
            out.write("\r\n");
        }
    }

j2ee規范還定義了jsp可以使用xml語法編寫, 因為jsp是先編譯為xml, 其實<%也是先編譯成了<jsp:scriptlet>因此下面的兩個文件是等效的:
文件1:
<%
    int a = 1;
%>
文件2:
<jsp:scriptlet>int a = 1;</jsp:scriptlet>

不過對於規范,不同的容器在實現的時候並不一定會按照規范來做,我知道的是tomcat是按照這個來做的,並且我記得在tomcat的早期版本中還能在work目錄中找到對應的xml文件.
但是websphere是不支持的,不知道現在的版本支不支持, resin好像也不支持, 也就是說在websphere中, <%必須寫成<%, 不能用<jsp:script>
websphere並沒有先編譯為xml, 再編譯為java

以上的編譯過程對於編碼來說是很簡單的,如果不編譯為xml文件,它簡單到只用正則就能搞定.

EL表達式
對於el表達式的支持也很簡單, 遇到${, 就開始讀入, 直到遇到}, 將其中的內容生成為一個表達式對象, 直接調用該表達式的write方法即可, 例如:
abc${user.name}123

編譯結果大致如下:
    public class _xxx_test{
        public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException{
            JspWriter out = xxxx.getWriter();
            ExprEnv exprEnv = xxx.create();

            out.write("abc");
            org.xxx.xxx.Expr _expr_xxx = xxx.createExpr("${user.name}");
            _expr_xxx.write(out, exprEnv);
            out.write("123\r\n");
        }
    }

不同的容器在實現的時候有所不同, 例如resin, 會將所有的表達式編譯為類的靜態變量, 以提升性能. 因為一個jsp頁面一旦寫好, 表達式的數目和內容是確定的,
因此是可以編譯為靜態變量的.

為什麼要編譯為調用表達式的write方法, 而不是編譯為out.write(_expr_xxx.getValue()), 我認為其中一個原因是為了表達式做null處理,\
任何一個表達式如果返回會空, 那麼寫到頁面上都應該是"", 而不應該是"null"
out.write默認會將null對象轉為"null"字符串寫入, 如果編譯為out.write(_expr_xxx.getValue()),
就得 out.write((_expr_xxx.getValue() != null ? _expr_xxx.getValue() : ""));
很顯然這樣是影響性能的, 因為如果返回結果不為null的話對表達式可能會計算兩次.
如果不這樣做,就需要重新定義變量, 為了變量不沖突,每個地方編譯器都要生成一個新的變量名, 導致最終生成的文件較大.

tag編譯
對tag的編譯略微麻煩,但也不復雜,這需要對源文件做html解析,但是跟一個完整的html解析器比起來,對tag的解析相對來說簡單多了
只需要在遇到'<'字符的時候讀出來節點名,然後在當前應用支持的標簽庫中去查找對應的標簽類, 如果沒查到,就按照上面的繼續編譯為out.write("<");
否則, 讀入所有的屬性, 創建一個標簽實例, 然後根據定義的屬性和標簽中定義的屬性,依次調用對應的setter方法, 例如:
<c:if test="${user.name == 'tom'}"><h1>a</h1></c:if>
編譯結果大致為:
    Expr expr_0 = xxx.createExpr("${user.name == 'tom'}");
    Tag _tag_0 = new xxx.xxx.IfTag();

    _tag_0.setter(...);

    int _tag_flag_0 = _tag_0.doStartTag();

    if(_tag_flag_0 != SKIP_BODY)
    {
        while(true)
        {
            // doInitBody, doBody等
            _tag_flag_0 = _tag_0.doEndTag();

            // doAfterBody等

            if(_tag_flag_0 != EVAL_BODY_AGAIN)
            {
                break;
            }
        }
    }
上面是一個標簽運行的標准流程, 事實上對於不同的容器,編譯結果區別很大,例如resin, 實際編譯結果大致如下:

Expr expr_0 = xxx.createExpr("${user.name == 'tom'}");

if(expr_0.getBoolean())
{
}

很簡單的編譯結果, 對於j2ee核心標簽庫的支持除了forEach編譯為了循環之外,其他的一律編譯成了很簡單的代碼,都沒有使用循環.
這一點可能是為了減小編譯結果,並且提升性能。
因為對於大部分標簽來說實在沒有必要按照標准的tag執行流程來編譯, 對於核心標簽庫中定義的標簽因為行為很明確,所以可以簡化編譯結果.
tomcat對於標簽的編譯, 采用的是每個標簽都編譯為一個方法, 並且采用的是do...while結構. resin則都編譯在_jspService方法內.

標簽的結束, 在編譯標簽的過程中,如何知道標簽結束了呢?一個很簡單的想法是,如果遇到開始標簽,就一直讀入,直到遇到結束標簽,很顯然這樣是行不通的。
因為標簽有嵌套,如果遇到嵌套標簽怎麼辦?按照上面的流程接著讀啊,讀到子標簽結束, 再然後呢? 稍微懂點數據結構的話,就很容易了,用棧。
同樣的問題,大致的解決思路都是一樣的, 比如計算器, 比如html,xml解析器, 都可以這麼做, 對於html解析器,我將會寫另外一篇文章專門說明.
先建立一個棧, 當遇到一個標簽的時候,就先把它壓入棧, 元素內容根據需要自己定義, 我們暫時假定結構如下:
class TagInfo{
    String nodeName;                // 節點的名稱
    Map<String, String> attributes; // 節點屬性 例如: test: ${user.name == 'tom'}
    Map<String, String> variables;  // 當前標簽可能需要用到的變量列表, 例如 flagName: _flag_0, exprName: expr_0等
}
注意是把TagInfo壓入棧

當遇到一個結束標簽的時候, 取得結束標簽的nodeName, 然後從棧彈出一個元素, 如果tagInfo.nodeName == nodeName, 那麼生成該標簽結束的代碼
對於標簽的標准流程來說,只需要生成如下的代碼就可以了:

            // out.write("<h1>1</h1>");
            // 這之前的代碼可能都是out.write之類的
            // _tag_flag_0之類的變量都從tagInfo獲取
            _tag_flag_0 = _tag_0.doEndTag();

            // doAfterBody等

            if(_tag_flag_0 != EVAL_BODY_AGAIN)
            {
                break;
            }
        }
    }
如果當前nodeName != tagInfo.nodeName那麼就繼續彈, 直到找到一個對應的標簽, 其實這種情況只是容錯處理,
實際上頁面最後運行出來的結果跟jsp編寫者的預期是不一致的.
如果一直到棧底都沒找到,那就拋異常吧。

對於棧來說,很多時候不需要pop, 只需要查看一下棧頂是否符合要求,符合的時候才pop, 否則先pop, 不符合還得push, 很麻煩
所以棧最好提供一個peek函數, 傳入一個int, 默認是棧頂, 根據參數決定返回當前棧的那個元素, 這樣比較方便

最後, 在jsp中,規范規定, 所有以_jsp開頭的變量都不能使用, 這是留給API或者容器用的.

上面是對jsp編譯過程的一個分析,對於j2ee規范定義的部分,我沒有看過原文,是從一些java書上看的一些零散的東西, 更多的是
看一些容器編譯出來的java源文件分析和猜測的,可能很多地方的想法跟j2ee規范定義的不一致,有興趣的可以在java官網找一下
規范原文看看。

06年的時候,我曾經用java實現過一套類似於tomcat的容器,當然功能弱多了, 只支持一些基本的功能,能跑jsp和servlet, 不支持el和tag.
更要命的是當時剛工作,對於一個代碼量較大的項目的控制能力很差,寫到最後覺得架構上很力不從心,勉勉強強能把jsp和servlet跑起來之後就沒有再繼續了。
當時還不了解socket的nio, socket的io用的是阻塞io, 線程也沒有用線程池,每次都是new一個新線程,性能很差。
有興趣的可以參考我的另外幾篇文章,用java實現反向代理, 其中的代碼是當年代碼中的一部分.

說一說js版的jstl吧
js版的jstl基本上是按照我上面分析的來實現的, 支持腳本, 支持el, 支持tag, 支持自定義tag.
為了性能的考慮,對tag的編譯借鑒了resin的思路, 對於標准標簽不按照標准流程編譯, 而是精簡編譯.
還是出於性能的考慮,編譯過程省略了中間一步,也就是不先編譯為xml, 而是直接編譯為js源文件.
因為如果編譯過程產生xml, 對於大文件來說就要在內存中再產生一份xml的內容, 然後再次編譯為js文件
中間需要兩次編譯,耗內存還耗資源.

對el的支持,采用了一個偷懶的方法. 例如abc{user.name}123這樣的代碼, 在jstl的實現中,需要寫成: abc{this.user.name}123
只要是pageContext中的屬性都需要加上this;
這跟實現有關, 對el如何計算是很麻煩的, 需要寫一個解釋器, 否則簡單的解析對於復雜的表達式就無能為力了.
例如${user.name}很容易計算出來結果, 但是對於${myfun1(user.name) + myfun2('test') + myfun3('test')}這樣的表達式
或者是%{user.age > 100 * 2}就比較麻煩了, 沒有一個解釋器基本搞不定.
我一開始考慮用eval, 但是eval在某些環境中性能較差, 而且編譯出來的結果裡面如果有很多el就會調用很多次
更重要的是用eval也無法實現, 例如, eval("user.name + '123'"); 在全局中根本沒有user這個對象
但是如果都加上this, 那麼eval就可以了

但是絕對不能用eval, eval的開銷太大.
寫個解釋器不現實,也沒必要,為了支持表達式,用一個解釋型的語言再寫個解釋器,不太劃算。
最後采用了一個折中的辦法,就是pageContext中的對象, 在el中都加this, 也就是說el中的所有的this都指向pageContext
對於每一個表達式都生成一個表達式對象,這點和j2ee中的定義保持一致. 另外會生成一個函數, 例如:
abc${this.user.name}123

最後的編譯結果大致如下:
new (function(){
this.handle = function(pageContext){
    var out = this.getWriter();
    out.write("abc");
    this._expr_0.write(out, pageContext);
    out.write("123");
})();
// 這裡是編譯過程產生的所有的表達式對象
this._expr_0 = new Expression("_expr_0", "this.user.name");

// 這裡記錄了編譯過程產生的所有的表達式對象的引用
this.exprPool = [
    this._expr_0
];
// 這個地方對關鍵,是所有的表達式函數, 目前為null, 在第一次運行的時候才會被編譯
this.exprList = null;

第一次運行的時候,會檢查this.exprList是否為空, 如果為空,編譯所有的表達式, 編譯結果如下:
this.exprList = new (function(){
    this._expr_0 = function(){
        // 最終this._expr_0函數會被放到pageContext中, 這就是為什麼要用this的原因
        return ( this.user.name );
    }
})();

this.exprList指向的是一個新的對象, 這裡必須是個對象才行。
下一步,運行期:
scriptlet.execute(context);
context由調用者傳入, 可以是一個純粹的json對象. scriptlet.execute方法如下:
// scriptlet指向了第一次編譯返回的對象
// scriptlet在new的時候創建了execute方法
scriptlet.execute = function(context){
    var pageContext = PageContextFactory.create(this, context, this.exprList);
    this.handle(pageContext);
};

在PageContextFactory.create方法裡面會對context包裝, 創建一個新的對象,並把context的所有屬性賦給新的pageContext
然後再把exprList包含的所有的函數賦值給新的pageContext, 這樣pageContext就擁有了context的所有屬性和scriptlet運行所
需要的所有的表達式函數, 表達式中的this指向的是pageContext, 這就是el中為什麼要用this的原因.


 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved