經(jīng)常有朋友問起,JSP和Servlet之間有什么區(qū)別,,兩者之間又有什么聯(lián)系,?其實(shí)Servlet技術(shù)的出現(xiàn)時間很早,是當(dāng)時為了Java的服務(wù)器端應(yīng)用而開發(fā)的,。大家都知道Applet是應(yīng)用小程序,,Servlet就是服務(wù)器端小程序了。但在Microsoft公司的ASP技術(shù)出現(xiàn)后,,使用Servlet進(jìn)行響應(yīng)輸出時一行行的輸出語句就顯得非常笨拙,,對于復(fù)雜布局或者顯示頁面更是如此。JSP就是為了滿足這種需求在Servlet技術(shù)之上開發(fā)的,??梢姡琂SP和Servlet之間有著內(nèi)在的血緣關(guān)系,,在學(xué)習(xí)JSP時,,如果能夠抓住這種聯(lián)系,就能更深刻地理解JSP的運(yùn)行機(jī)理,,達(dá)到事半功倍的效果,。
本文將通過對一個JSP運(yùn)行過程的剖析,深入JSP運(yùn)行的內(nèi)幕,,并從全新的視角闡述一些JSP中的技術(shù)要點(diǎn),。
HelloWorld.jsp
我們以Tomcat 4.1.17服務(wù)器為例,來看看最簡單的HelloWorld.jsp是怎么運(yùn)行的,。
代碼清單1:HelloWorld.jsp
HelloWorld.jsp <% String message = "Hello World!"; %> <%=message%>
這個文件非常簡單,,僅僅定義了一個String的變量,并且輸出。把這個文件放到Tomcat的webapps\ROOT\目錄下,,啟動Tomcat,,在瀏覽器中訪問http://localhost:8080/HelloWorld.jsp,瀏覽器中的輸出為“HelloWorld!”
讓我們來看看Tomcat都做了什么,。轉(zhuǎn)到Tomcat的\work\Standalone\localhost\_目錄下,,可以找到如下的HelloWorld_jsp.java,這個文件就是Tomcat解析HelloWorld.jsp時生成的源文件:
代碼清單2:HelloWorld_jsp.java
package org.apache.jsp;
import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; import org.apache.jasper.runtime.*;
public class HelloWorld_jsp extends HttpJspBase { ...... public void _jspService(HttpServletRequest request, HttpServletResponse response)throws java.io.IOException, ServletException { JspFactory _jspxFactory = null; javax.servlet.jsp.PageContext pageContext = null; HttpSession session = null; ServletContext application = null; ServletConfig config = null; JspWriter out = null; Object page = this; JspWriter _jspx_out = null;
try { _jspxFactory = JspFactory.getDefaultFactory(); response.setContentType("text/html;charset=ISO-8859-1"); pageContext = _jspxFactory.getPageContext(this, request, response,null, true, 8192, true); application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out;
String message = "Hello World!"; out.print(message); } catch (Throwable t) { out = _jspx_out; if (out != null && out.getBufferSize() != 0) out.clearBuffer(); if (pageContext != null) pageContext.handlePageException(t); } finally { if (_jspxFactory != null) _jspxFactory.releasePageContext(pageContext); } } }
從上面可以看出,,HelloWorld.jsp在運(yùn)行時首先解析成一個Java類HelloWorld_jsp.java,,該類繼承于org.apache.jasper.runtime.HttpJspBase基類,HttpJspBase實(shí)現(xiàn)了HttpServlet接口,??梢姡琂SP在運(yùn)行前首先將編譯為一個Servlet,,這就是理解JSP技術(shù)的關(guān)鍵,。
我們還知道JSP頁面中內(nèi)置了幾個對象,如pageContext,、application,、config、page,、session,、out等,你可能會奇怪,,為什么在JSP中的代碼片斷中可以直接使用這些內(nèi)置對象,。觀察_jspService()方法,實(shí)際上這幾個內(nèi)置對象就是在這里定義的,。在對JSP文件中的代碼片斷進(jìn)行解析之前,,先對這幾個內(nèi)置對象進(jìn)行初始化。
首先,,調(diào)用JspFactory的getDefaultFactory()方法獲取容器實(shí)現(xiàn)(本文中指Tomcat 4.1.17)的一個JspFactory對象的引用,。JspFactory是javax.servlet.jsp包中定義的一個抽象類,其中定義了兩個靜態(tài)方法set/getDefaultFactory(),。set方法由JSP容器(Tomcat)實(shí)例化該頁面Servlet(即HelloWorld_jsp類)的時候置入,,所以可以直接調(diào)用JspFactory.getDefaultFactory()方法得到這個JSP工廠的實(shí)現(xiàn)類。Tomcat是調(diào)用org.apache.jasper.runtime.JspFactoryImpl類,。
然后,,調(diào)用這個JspFactoryImpl的getPageContext()方法,填充一個PageContext返回,,并賦給內(nèi)置變量pageConext,。其它內(nèi)置對象都經(jīng)由該pageContext得到,。具體過程見上面的代碼,這里不再贅述,。該頁面Servlet的環(huán)境設(shè)置完畢,開始對頁面進(jìn)行解析,。HelloWorld.jsp頁面僅僅定義了一個String變量,,然后直接輸出。解析后的代碼如下:
代碼清單3:JSP頁面解析后的代碼片斷
String message = "Hello World!"; out.print(message);
定制標(biāo)簽的解析過程
在一個中大型的Web應(yīng)用中,,通常使用JSP定制標(biāo)簽來封裝頁面顯示邏輯,。剖析容器對定制標(biāo)簽的解析過程,對我們深入理解定制標(biāo)簽的運(yùn)行機(jī)理非常有幫助,。下面我們以Struts1.1b中附帶的struts-example應(yīng)用的主頁運(yùn)行為例加以說明,。
包含定制標(biāo)簽的index.jsp
Struts1.1b的下載地址是http://jakarta./struts/index.html。將下載的包解壓,,在webapps目錄下可以找到struts-example.war,。將該War包拷貝到Tomcat的webapps目錄下,Tomcat會自動安裝此應(yīng)用包,。在瀏覽器中通過http://localhost:8080/struts-example訪問struts-example應(yīng)用,,將顯示應(yīng)用的首頁(見圖1)。
圖一 應(yīng)用的首頁
代碼清單4:index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %> <%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %> <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %> <html:html locale="true"> <head> <title><bean:message key="index.title"/></title> <html:base/> </head> <body bgcolor="white"> …… </body> </html:html>
我們僅以index.jsp中的<bean:message/>標(biāo)簽的解析為例進(jìn)行分析,,看容器是怎樣把這個自定義標(biāo)簽解析成HTML輸出的,。上面代碼省略了頁面的其它顯示部分。首先,,查看上面瀏覽器中頁面的源文件:
<html lang="zh"> <head> <title>MailReader Demonstration Application (Struts 1.0)</title> </head> <body bgcolor="white"> …… </body> </html>
可見,,容器已經(jīng)把<bean:message key="index.title"/>替換成一個字串,顯示為頁面的標(biāo)題,。
解析過程
那么,,JSP容器是怎樣完成解析的呢?查看在工作目錄jakarta-tomcat-4.1.17\work\Standalone\localhost\struts-example下解析后的index_jsp.java文件:
代碼清單5:index_jsp.java
package org.apache.jsp;
import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; import org.apache.jasper.runtime.*; public class index_jsp extends HttpJspBase { //為所有的定制標(biāo)簽定義處理器池類的引用 private org.apache.jasper.runtime.TagHandlerPool ,; _jspx_tagPool_bean_message_key; …… //頁面類構(gòu)造方法 public index_jsp() { _jspx_tagPool_bean_message_key = new org.apache.jasper.runtime.TagHandlerPool(); …… }
public void _jspService(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, ServletException { …… _jspxFactory = JspFactory.getDefaultFactory(); response.setContentType("text/html;charset=UTF-8"); pageContext = _jspxFactory.getPageContext(this, request, response,null, true, 8192, true); application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; …… if (_jspx_meth_html_html_0(pageContext)) return; …… } //頁面在處理退出時釋放所有定制標(biāo)簽的屬性 public void _jspDestroy() { _jspx_tagPool_bean_message_key.release(); …… } }
生成的index_jsp.java繼承于org.apache. jasper.runtime.HttpJspBase,。研究這個文件為我們了解定制標(biāo)簽的運(yùn)行機(jī)理提供了途徑。
從上面可以看出,,Tomcat在解析一個JSP頁面時,,首先為每一個定制標(biāo)簽定義并實(shí)例化了一個TagHandlerPool對象。頁面的處理方法覆蓋父類的_ jspService()方法,,_jspService方法首先初始化環(huán)境,,為內(nèi)置對象賦值。由于index.jsp頁面整體由一個<html:html/>標(biāo)簽包裹,,Tomcat對每一個標(biāo)簽都產(chǎn)生一個私有方法加以實(shí)現(xiàn),。<html:html/>標(biāo)簽的處理方法是_jspx_meth_html_html_0()。這個方法的命名規(guī)范大家也可以從這里看出,就是“_jspx_meth + 標(biāo)簽的前綴 + 標(biāo)簽名 + 該標(biāo)簽在JSP頁面同類標(biāo)簽中出現(xiàn)的序號”,。其它標(biāo)簽都被包含在該標(biāo)簽中,,所以其它標(biāo)簽在_jspx_meth_html_html_0()方法中進(jìn)行解析。具體的代碼實(shí)現(xiàn)請參見賽迪網(wǎng)http://linux.期刊瀏覽2003年第6期,。
在_jspx_meth_html_html_0()方法中,,首先從_jspx_tagPool_html_html_locale池中得到一個org.apache.struts.taglib.html.HtmlTag的實(shí)例,然后設(shè)置這個tag實(shí)例的頁面上下文及上級標(biāo)簽,,由于html:html標(biāo)簽是頁面的最頂層標(biāo)簽,,所以它的parent是null。然后對該標(biāo)簽的內(nèi)容進(jìn)行解析,。HTML代碼直接輸出,,下面主要看看<html:html></html:html>標(biāo)簽之間包含的<bean:message key="index.title"/>標(biāo)簽的解析。對bean:message標(biāo)簽的解析類似于html:html,,Tomcat也將其放入一個單獨(dú)的方法_jspx_meth_bean_message_0()中進(jìn)行,。
bean:message標(biāo)簽的解析
代碼清單7:_jspx_meth_bean_message_0()方法片斷
//對message定制標(biāo)簽的處理方法 private boolean _jspx_meth_bean_message_0( javax.servlet.jsp.tagext.Tag _jspx_th_html_html_0, javax.servlet.jsp.PageContext pageContext) throws Throwable { JspWriter out = pageContext.getOut(); /* ---- bean:message ---- */ org.apache.struts.taglib.bean.MessageTag _jspx_th_bean_message_0 = (org.apache.struts.taglib.bean.MessageTag) _jspx_tagPool_bean_message_key.get( org.apache.struts.taglib.bean.MessageTag.class); _jspx_th_bean_message_0.setPageContext(pageContext); _jspx_th_bean_message_0.setParent(_jspx_th_html_html_0); _jspx_th_bean_message_0.setKey("index.title"); int _jspx_eval_bean_message_0 = _jspx_th_bean_message_0.doStartTag(); if (_jspx_th_bean_message_0.doEndTag()== javax.servlet.jsp.tagext.Tag.SKIP_PAGE) return true; _jspx_tagPool_bean_message_key.reuse(_jspx_th_bean_message_0); return false; }
同樣,對html:bean也需要從池中得到一個標(biāo)簽類的實(shí)例,,然后設(shè)置環(huán)境,。這里不再贅述。我們只專注對MessageTag定制標(biāo)簽類特殊的處理部分,。定制標(biāo)簽類的開發(fā)不在本文討論范圍之內(nèi),。在index.jsp中定義了一個bean:message標(biāo)簽,并設(shè)置了一個屬性:<bean:message key="index.title"/>,。Tomcat在解析時,,調(diào)用MessageTag對象的key屬性設(shè)置方法setKey(),將該屬性置入,。然后調(diào)用MessageTag的doStartTag()和doEndTag()方法,,完成解析。如果doEndTag()方法的返回值為javax.servlet.jsp.tagext.Tag. SKIP_PAGE,,表明已經(jīng)完成解析,,返回true,Tomcat將立即停止剩余頁面代碼的執(zhí)行,,并返回,。否則把該MessageTag的實(shí)例放回池中。
標(biāo)簽類對象實(shí)例的池化
為了提高運(yùn)行效率,,Tomcat對所有的定制標(biāo)簽類進(jìn)行了池化,,池化工作由org.apache.jasper. runtime.TagHandlerPool類完成。TagHandlerPool類主要有兩個方法,,代碼如下:
代碼清單8:TagHandlerPool.java
public class TagHandlerPool { private static final int MAX_POOL_SIZE = 5; private Tag[] handlers; public synchronized Tag get(Class handlerClass) throws JspException {……} public synchronized void reuse(Tag handler) {……} }
TagHandlerPool簡單地實(shí)現(xiàn)了對標(biāo)簽類的池化,,其中MAX_POOL_SIZE是池的初始大小,handlers是一個Tag的數(shù)組,,存儲標(biāo)簽類的實(shí)例,。get(Class handlerClass)得到一個指定標(biāo)簽類的實(shí)例,如果池中沒有可用實(shí)例,,則新實(shí)例化一個,。reuse(Tag handler)把handler對象放回池中。
至此,,我們對JSP在容器中的運(yùn)行過程已經(jīng)了然于胸了。雖然每種JSP容器的解析結(jié)果會有差異,,但其中的原理都雷同,。對于編寫JSP應(yīng)用,我們并不需要干涉容器中的運(yùn)行過程,,但如果你對整個底層的運(yùn)行機(jī)制比較熟悉,,就能對JSP/Servlet技術(shù)有更深的認(rèn)識。
|