解讀JSP的解析過程
網際網路上,這方面的資料實在太少了,故把自己研究的一些結果公佈出來。
首先,問大家幾個問題,看大家能不能回答出來,或者在網上能不能找到答案:
1、page、include、taglib這三個編譯指令,執行的順序是什麼?
2、JSP檔案中的Java程式碼、編譯指令、動作指令、EL標籤、第三方標籤、靜態文字等,被編譯的順序是什麼?
3、常用的、與JSP解析/編譯相關的類有哪些?換句話說,JSP解析、編譯技術是建立在哪些介面和工具之上的?
4、JSP技術所有的編譯指令和動作指令有哪些?
5、JSP技術是由誰發起的,現在有哪幾個標準?都有哪些伺服器或者專案支援JSP的解析和編譯?
先回答簡單、基礎性的問題:
回答問題5:
JSP的發起者為——Sun Microsystems, Inc.
JSP是由Java Servlets發展而來。
JSP和Servlet一樣,都是在Java Community Process(Java社群組織)等眾多人蔘與下,共同開發出來的,並且最終制定了一些規範和標準。Java體系中的規範和技術,會被制定成JSR規範。JSP和Servlet也屬於這個規範之中,例如JSR-53規定了JSP 1.2和Servlet 2.4的規範,JSR-152規定了JSP 2.0的規範。(詳見http://zh.wikipedia.org/wiki/JSR)
JSP常用的版本有1.2、2.0和2.1(最新版本),其中1.2版本最大的進步在於優化了JSTL(JavaServer Pages Standard Tag Library),已經過時了(因為它不支援EL表示式),2.0在1.2的基礎上有了巨大的進步,因為它引入了Expression Language(EL表示式)、簡化了tag標籤的擴充套件、增強了XML的語法,以及其他一些重要改進,我是翻譯過來的,官方原文如下:
The JSP 2.0 specification (JSR-152) substantially extended the technology by integrating a simple yet powerful expression language, simplifying the tag extension API, and enhancing the pure XML syntax,
among other important enhancements. These enhancements greatly reduced the learning curve of the technology, warranting a major version number upgrade.
而JSP 2.1是在JSP 2.0的基礎上,為JSF而生的,簡而言之,JSP 2.1加強了EL,更能夠適應J2EE、J2SE、Servlet API的發展。
JSP 2.1 to enhance the expression language to meet the needs of JSF technology. Many of these enhancements are likely to be useful in other contexts as well.
Enhancements to be considered for the JSP 2.1 expression language include, but are not limited to, the following:
- moving the expression language chapter into its own specification document, to be delivered under this JSR with consultation from the JavaServer Faces expert group
- ability to redefine the behavior of the "." operator through a Property Resolver API
- ability to plug in Variable Resolvers on a per-application and per-page basis
- ability to plug in Property Resolvers on a per-application and per-page basis
- ability to express references to bean methods using the expression language and invoking those methods via a Method Binding API
- ability to express references to bean properties using the expression language and getting/setting those attributes via a Property Binding API
- ability to defer expression evaluation until a time of a tag handler's choosing
目前支援JSP的引擎有很多,NB的人自己都可以寫一套出來,最常見的WebLogic、Tomcat、WebSphere、Resin(據說效率比Tomcat等要高,網易等大網站都在用)。
回答問題3:
第一個想到的就是Java Servlet API,即servlet-api.jar ,然後JSP的解析有一個工具包,即jasper-compiler.jar,因為JSP是一個標準,那麼在解析工具包之上,應該有一個規範的API包,即jsp-api.jar,JSTL是JSP規範的一部分,故還有jstl-api.jar,另外,還有expression language擴充套件包,el-api.jar。還注意到一點,JSP是基於XML的,故也涉及到XML的解析(用的是SAX)。
補充一點,可以手動呼叫程式來解析(也叫預編譯)JSP,在Tomcat或WebSphere下,是呼叫JspC這個工具類:
Tomcat下,jsp是通過org.apache.jasper.JspC編譯工具將JSP 頁面的預編譯,在WAS下,是通過 com.ibm.websphere.ant.tasks.JspC進行預編譯。
回答問題4和1:
JSP技術所有的編譯指令和動作指令,可以參看官方API文件,我這裡找到的如下:
ATTRIBUTE_ACTION"attribute"
ATTRIBUTE_DIRECTIVE_ACTION"directive.attribute"
BODY_ACTION"body"
DECLARATION_ACTION"declaration"
DIRECTIVE_ACTION"directive."
DOBODY_ACTION"doBody"
ELEMENT_ACTION"element"
EXPRESSION_ACTION"expression"
FALLBACK_ACTION"fallback"
FORWARD_ACTION"forward"
GET_PROPERTY_ACTION"getProperty"
INCLUDE_ACTION"include"
INCLUDE_DIRECTIVE_ACTION"directive.include"
INVOKE_ACTION"invoke"
JSP_ATTRIBUTE_ACTION"jsp:attribute"
JSP_ATTRIBUTE_DIRECTIVE_ACTION"jsp:directive.attribute"
JSP_BODY_ACTION"jsp:body"
JSP_DECLARATION_ACTION"jsp:declaration"
JSP_DOBODY_ACTION"jsp:doBody"
JSP_ELEMENT_ACTION"jsp:element"
JSP_EXPRESSION_ACTION"jsp:expression"
JSP_FALLBACK_ACTION"jsp:fallback"
JSP_FORWARD_ACTION"jsp:forward"
JSP_GET_PROPERTY_ACTION"jsp:getProperty"
JSP_INCLUDE_ACTION"jsp:include"
JSP_INCLUDE_DIRECTIVE_ACTION"jsp:directive.include"
JSP_INVOKE_ACTION"jsp:invoke"
JSP_OUTPUT_ACTION"jsp:output"
JSP_PAGE_DIRECTIVE_ACTION"jsp:directive.page"
JSP_PARAM_ACTION"jsp:param"
JSP_PARAMS_ACTION"jsp:params"
JSP_PLUGIN_ACTION"jsp:plugin"
JSP_ROOT_ACTION"jsp:root"
JSP_SCRIPTLET_ACTION"jsp:scriptlet"
JSP_SET_PROPERTY_ACTION"jsp:setProperty"
JSP_TAG_DIRECTIVE_ACTION"jsp:directive.tag"
JSP_TAGLIB_DIRECTIVE_ACTION"jsp:taglib"
JSP_TEXT_ACTION"jsp:text"
JSP_USE_BEAN_ACTION"jsp:useBean"
JSP_VARIABLE_DIRECTIVE_ACTION"jsp:directive.variable"
OUTPUT_ACTION"output"
PAGE_DIRECTIVE_ACTION"directive.page"
PARAM_ACTION"param"
PARAMS_ACTION"params"
PLUGIN_ACTION"plugin"
ROOT_ACTION"root"
SCRIPTLET_ACTION"scriptlet"
SET_PROPERTY_ACTION"setProperty"
TAG_DIRECTIVE_ACTION"directive.tag"
TAGLIB_DIRECTIVE_ACTION"taglib"
TEXT_ACTION"text"
USE_BEAN_ACTION"useBean"
VARIABLE_DIRECTIVE_ACTION"directive.variable"
其中directive就是編譯指令,總結起來,有:page、include、taglib、tag、attribute、variable,關於他們的執行順序,看解析程式是最有說服力的,程式如下:
[ 摘自org.apache.jasper.compiler.Parser ]
private void parseDirective(Node parent)
throws JasperException
{
this.reader.skipSpaces();
String directive = null;
if (this.reader.matches("page")) {
directive = "<%@ page";
if (this.isTagFile) {
this.err.jspError(this.reader.mark(), "jsp.error.directive.istagfile", directive);
}
parsePageDirective(parent);
} else if (this.reader.matches("include")) {
directive = "<%@ include";
parseIncludeDirective(parent);
} else if (this.reader.matches("taglib")) {
if (this.directivesOnly)
{
return;
}
directive = "<%@ taglib";
parseTaglibDirective(parent);
} else if (this.reader.matches("tag")) {
directive = "<%@ tag";
if (!(this.isTagFile)) {
this.err.jspError(this.reader.mark(), "jsp.error.directive.isnottagfile", directive);
}
parseTagDirective(parent);
} else if (this.reader.matches("attribute")) {
directive = "<%@ attribute";
if (!(this.isTagFile)) {
this.err.jspError(this.reader.mark(), "jsp.error.directive.isnottagfile", directive);
}
parseAttributeDirective(parent);
} else if (this.reader.matches("variable")) {
directive = "<%@ variable";
if (!(this.isTagFile)) {
this.err.jspError(this.reader.mark(), "jsp.error.directive.isnottagfile", directive);
}
parseVariableDirective(parent);
} else {
this.err.jspError(this.reader.mark(), "jsp.error.invalid.directive");
}
this.reader.skipSpaces();
if (!(this.reader.matches("%>")))
this.err.jspError(this.start, "jsp.error.unterminated", directive);
}
再來回答問題2:
根據原始碼,我看到JSP編譯的順序是這樣的:
1-- getJspConfigPageEncoding
2-- determineSyntaxAndEncoding
3-- 解析成 Node.Nodes parsedPage 物件,即取出所有節點
4-- 解析每個節點
第1步,是從web.xml等配置檔案中去讀取配置(裡面有個<jsp-config>配置),如果配置時設定了統一編碼,則使用這種型別的編碼,第2步,是根據檔案來獲取編碼,注意看如下一段程式碼:
if ((jspReader.matches("tag ")) || (jspReader.matches("page")))
{
jspReader.skipSpaces();
Attributes attrs = Parser.parseAttributes(this, jspReader);
encoding = getPageEncodingFromDirective(attrs, "pageEncoding");
if (encoding != null) {
break;
}
encoding = getPageEncodingFromDirective(attrs, "contentType");
if (encoding != null) {
saveEncoding = encoding;
}
}
}
程式首先判斷有無編譯指令tag或者page,如果有,則檢查編譯指令是否指定了pageEncoding屬性或者contentType屬性。根據這種邏輯,可知如下這種寫法:
<%@ page contentType="text/html;charset=utf-8" pageEncoding="UTF-8"%>
其實是重複指定了編碼,解析時會以pageEncoding為準。
第4步,解析每個節點:
while (reader.hasMoreInput()) {
parser.parseElements(root);
}
這裡又分為幾個步驟,先看程式:
private void parseElements(Node parent)
throws JasperException
{
this.start = this.reader.mark();
if (this.reader.matches("<%--")) {
parseComment(parent);
} else if (this.reader.matches("<%@")) {
parseDirective(parent);
} else if (this.reader.matches("<jsp:directive.")) {
parseXMLDirective(parent);
} else if (this.reader.matches("<%!")) {
parseDeclaration(parent);
} else if (this.reader.matches("<jsp:declaration")) {
parseXMLDeclaration(parent);
} else if (this.reader.matches("<%=")) {
parseExpression(parent);
} else if (this.reader.matches("<jsp:expression")) {
parseXMLExpression(parent);
} else if (this.reader.matches("<%")) {
parseScriptlet(parent);
} else if (this.reader.matches("<jsp:scriptlet")) {
parseXMLScriptlet(parent);
} else if (this.reader.matches("<jsp:text")) {
parseXMLTemplateText(parent);
} else if ((!(this.pageInfo.isELIgnored())) && (this.reader.matches("${"))) {
parseELExpression(parent, '$');
} else if ((!(this.pageInfo.isELIgnored())) && (this.reader.matches("#{")))
{
parseELExpression(parent, '#');
} else if (this.reader.matches("<jsp:")) {
parseStandardAction(parent);
} else if (!(parseCustomTag(parent))) {
checkUnbalancedEndTag();
parseTemplateText(parent);
}
}
處理的順序如下:
1-- “<%-- --%>”型別的註釋
2-- “<%@ %>”編譯指令
3-- “<jsp:directive. %>”編譯指令
4-- “<%! %>”宣告指令
5-- “<%= %>”表示式指令
6-- “<% %>”嵌入指令碼
7-- “<jsp:text >”嵌入文字
8-- “${ }”EL表示式
9-- “#{ }”EL表示式
10-- “<jsp: >”其他jsp動作指令
11-- 自定義的tag標籤
然後,再看看jsp中的java程式碼(ScriptingElement)是怎麼執行的:
第一步:
new一個Node節點,然後把java的字串完整地賦值給Node的text屬性,然後把node新增到Parent Node 佇列(List)裡面。
第二步:讀取這些Nodes,將其轉換成java原始碼,然後在呼叫java編譯器將原始碼編譯成class檔案。(注意:這個功能相當於是把字串,轉換成了java位元組碼)
這個過程,呼叫了SmapUtil將上面那些nodes轉換成Java原始檔,然後呼叫JDTCompiler工具類,將Java原始檔編譯成.class檔案,我看Tomcat呼叫的是org.eclipse.jdt.internal.compiler.*包下面的編譯工具,實際上JDK也為我們提供了自己手動編譯Java檔案的方法,JDK 1.6可以用javax.tools.JavaCompiler。
借鑑這個過程,我自己也實現了“字串——生成Java檔案——編譯成class檔案”的全自動化過程。這個過程很有用,可以允許我們自己在網站上 上傳Java檔案並編譯執行!
關於JSP解析、編譯的研究,暫時就告一段落,有興趣的可以和我交流。