互联网上,这方面的资料实在太少了,故把自己研究的一些结果公布出来。
首先,问大家几个问题,看大家能不能回答出来,或者在网上能不能找到答案:
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文件的预编译、编译和缓存机制 说白了,它就是一个“文本文件”,有一些“指定的语法”,与HTML比,它被称为“动态页面”,但是如果每次访问,都要从磁盘去读取该文件,显然性能不好。所以说,关键点就是怎么减少动态编译。
第一:缓存技术, 第二:支持预编译,即在服务器启动完毕之前,就先把指定的JSP文件编译好。
关于JSP解析、编译的研究,暂时就告一段落,有兴趣的可以和我交流。