Jasper模块是Tomcat的JSP核心引擎,我们知道JSP本质是Servlet, 那么从一个JSP编写完后到真正的Tomcat运行, 它将经历从JSP转变成Servlet,再由Servlet转变成Class的过程 , 在Tomcat中正是使用Jasper对JSP请求时行解析,生成Servlet并生成 Class字节码,另外,在运行时,Jasper还会检测JSP文件是否修改, 如果可以修改,则会重新编译JSP文件,现在就来讨论Jasper引擎的内部原理实现, JSP 从语法上来讲可以分为标准的JSP 和基于XML的JSP,但是实现思路基本相同,只是规定的不同的语法,本质上是对规定的语法进行解析编译 。
标准的JSP大家都很熟悉,从开始学习Java接触的就是它了,可以说, JSP 是Servlet的扩展,它主要是为了解耦动静态内容,解决动态内容和静态内容一起混合在Servlet中的问题, 但是JSP 本质上也是一个Servlet,只不过它定义了一些语法糖让开发人员可以在HTML 中动态处理,而Servlet其实就是一个Java 类,所以这里面其实就涉及一个从JSP编译为Java 类的过程 , 对于 Java 类来说, 真正运行在JVM上的又是Class字节码,所以这里还涉及另外一个从Java到Class字节码编译的过程,编译的具体实现由不同的厂家实现, 这里讨论的Tomcat如何编译标准的JSP 。
在探讨如何编译JSP之前我们应该先看看标准的JSP语法,只有在了解JSP语法之后才能根据其语法进行编译,下面列举了一些常见的语法,但并不包含所有的语法,旨在说明一个大致的编码过程 。
- 对于代码脚本,格式为<% Java 代码片段 %>
- 对于变量声明,格式为<%! int i = 0 ;%>
- 对于表达式,格式为<%= 表达式 %>
- 对于注释,格式为<%-- JSP注释 --%>
- 对于指令,格式为<%@page…%> , <%@include…%> , <%@tablib …%>
- 对于动作,格式为<jsp:include/> , <jsp:useBean%>, <jsp:setProperty/> , <jsp:getProperty/> , <jsp:forward/> , <jsp:plugine/> ,<jsp:element/> , <jsp:attribute/> ,<jsp:body/> , <jsp:text/> 。
- 对于内置对象,脚本中内置了 request, response , out , session , application , config , pageContext , page , Exception 等对象 。
从JSP 到Servlet 。
语法树的生成 , 语法解析 。
一般来说,语句是按一定规则进行推导后形成一个语法树,这种树状结构有利于对语句结构层次的描述,它是对代码语句进行语法分析后得到的产物,编译器利用它可以方便的进行编译,同样,Jasper对JSP语法解析后也会生成一树树,这棵对中的各个节点包含了不同的信息,但是对于JSP来说, 解析后的语法树比较简单,它只有一个父亲节点和N 个子节点,如图16.1所示,node1 ,表示形如<% --字符串–> 的注释节点,节点里面包含了一个表示注释的字符串属性,而node2则可能表示形如<%=a + b %> 的表达式节点,节点里面包含了一个表示表达式的属性, 同样的,其他节点可能表示JSP的其他语法 , 有了这棵树,我们就可以很方便的生成对应的Servlet。
那么具体怎样解析生成这查对的呢? 下面给出简单的代码实现。
- 首先定义树的数据结构,其中,parent 指向父亲节点,nodes 是此节点的子节点,且nodes应该是有序列表,这样就能保证与解析的顺序一致,另外,由于每个节点的属性不同,Node类只提供公共部分的属性, 对于不同的节点,其他属性需要继承Node额外的实现。
abstract class Node implements TagConstants { private static final VariableInfo[] ZERO_VARIABLE_INFO = {}; protected Attributes attrs; protected Attributes taglibAttrs; protected Attributes nonTaglibXmlnsAttrs; protected Nodes body; protected String text; protected Mark startMark; protected int beginJavaLine; protected int endJavaLine; protected Node parent; protected Nodes namedAttributeNodes; // cached for performance protected String qName; protected String localName; protected String innerClassName; private boolean isDummy; public static class Root extends Node { } public static class JspRoot extends Node { } public static class PageDirective extends Node { } public static class IncludeDirective extends Node { } public static class TaglibDirective extends Node { } public static class TagDirective extends Node { } public static class AttributeDirective extends Node { } ... }
- 其实需要一个读取JSP文件工具类,此工具类主要提供了JSP文件字符操作,其中,有个cursor变量用于表示目前解析的位置,主要的方法则包含判断是否达到文件末尾的hasMoreInput方法,获取下一个字符的nextChar方法,获取某个范围内字符组成的字符串的getText方法,匹配是否包含某个字符串的matches方法,跳过空格符的skipSpaces方法, 以及跳转到某个字符串的skipUtil方法,有有这些辅助操作, 就可以开始读取解析语法了。
class JspReader { private final Log log = LogFactory.getLog(JspReader.class); // must not be static private Mark current; private String master; private List<String> sourceFiles; private int currFileId; private int size; private JspCompilationContext context; private ErrorDispatcher err; private boolean singleFile; public JspReader(){ } String getFile(final int fileid) { return sourceFiles.get(fileid); } boolean hasMoreInput() throws JasperException { } int nextChar() throws JasperException { } private Boolean indexOf(char c, Mark mark) throws JasperException { } void pushChar() { } String getText(Mark start, Mark stop) throws JasperException { } int peekChar() { return peekChar(0); } Mark mark() { return new Mark(current); } private boolean markEquals(Mark another) { return another.equals(current); } void reset(Mark mark) { current = new Mark(mark); } private void setCurrent(Mark mark) { current = mark; } boolean matches(String string) throws JasperException { } boolean matchesETag(String tagName) throws JasperException { } boolean matchesETagWithoutLessThan(String tagName) throws JasperException{ } boolean matchesOptionalSpacesFollowedBy( String s ) throws JasperException{ } int skipSpaces() throws JasperException { } Mark skipUntil(String limit) throws JasperException { } Mark skipUntilIgnoreEsc(String limit, boolean ignoreEL) throws JasperException { } Mark skipUntilETag(String tag) throws JasperException { } Mark skipELExpression() throws JasperException { } final boolean isSpace() throws JasperException { } String parseToken(boolean quoted) throws JasperException { } void setSingleFile(boolean val) { } private boolean isDelimiter() throws JasperException { } private int registerSourceFile(final String file) { } private int unregisterSourceFile(final String file) { } private void pushFile(String file, String encoding, InputStreamReader reader) throws JasperException { } private boolean popFile() throws JasperException { } }
- 需要一个JSP语法解析器对JSP进行解析,为了简单说明,这里只解析<%-- … --> 注释语法,<@page …%> 页面指令, <%include … /%> 包含指令,<%@taglib…%> 标签指令,假设这里对index.jsp 进行语法解析,如果匹配到<%-- ,则表示注释语法,获取其中的注释文字并创建commentNode 节点作为根节点的子节点,如果匹配到 <%@ ,则有三种可能,所以需要进一步的解析,即对于页面指令,包含指令和标签指令的解析,最后解析出来的就是一棵语法树。
class Parser implements TagConstants { private ParserController parserController; private JspCompilationContext ctxt; private JspReader reader; private Mark start; private ErrorDispatcher err; private int scriptlessCount; private boolean isTagFile; private boolean directivesOnly; private JarResource jarResource; private PageInfo pageInfo; private static final boolean STRICT_WHITESPACE = Boolean.parseBoolean( System.getProperty( "org.apache.jasper.compiler.Parser.STRICT_WHITESPACE", "true")); public static Node.Nodes parse(ParserController pc, JspReader reader, Node parent, boolean isTagFile, boolean directivesOnly, JarResource jarResource, String pageEnc, String jspConfigPageEnc, boolean isDefaultPageEncoding, boolean isBomPresent) throws JasperException { Parser parser = new Parser(pc, reader, isTagFile, directivesOnly, jarResource); Node.Root root = new Node.Root(reader.mark(), parent, false); root.setPageEncoding(pageEnc); root.setJspConfigPageEncoding(jspConfigPageEnc); root.setIsDefaultPageEncoding(isDefaultPageEncoding); root.setIsBomPresent(isBomPresent); // For the Top level page, add include-prelude and include-coda PageInfo pageInfo = pc.getCompiler().getPageInfo(); if (parent == null && !isTagFile) { parser.addInclude(root, pageInfo.getIncludePrelude()); } if (directivesOnly) { parser.parseFileDirectives(root); } else { while (reader.hasMoreInput()) { parser.parseElements(root); } } if (parent == null && !isTagFile) { parser.addInclude(root, pageInfo.getIncludeCoda()); } Node.Nodes page = new Node.Nodes(root); return page; } /* * AllBody ::= ( '<%--' JSPCommentBody ) | ( '<%@' DirectiveBody ) | ( '<jsp:directive.' * XMLDirectiveBody ) | ( '<%!' DeclarationBody ) | ( '<jsp:declaration' * XMLDeclarationBody ) | ( '<%=' ExpressionBody ) | ( '<jsp:expression' * XMLExpressionBody ) | ( '${' ELExpressionBody ) | ( '<%' ScriptletBody ) | ( '<jsp:scriptlet' * XMLScriptletBody ) | ( '<jsp:text' XMLTemplateText ) | ( '<jsp:' * StandardAction ) | ( '<' CustomAction CustomActionBody ) | TemplateText */ private void parseElements(Node parent) throws JasperException { if (scriptlessCount > 0) { // vc: ScriptlessBody // We must follow the ScriptlessBody production if one of // our parents is ScriptlessBody. parseElementsScriptless(parent); return; } start = reader.mark(); if (reader.matches("<%--")) { parseComment(parent); } else if (reader.matches("<%@")) { parseDirective(parent); } else if (reader.matches("<jsp:directive.")) { parseXMLDirective(parent); } else if (reader.matches("<%!")) { parseDeclaration(parent); } else if (reader.matches("<jsp:declaration")) { parseXMLDeclaration(parent); } else if (reader.matches("<%=")) { parseExpression(parent); } else if (reader.matches("<jsp:expression")) { parseXMLExpression(parent); } else if (reader.matches("<%")) { parseScriptlet(parent); } else if (reader.matches("<jsp:scriptlet")) { parseXMLScriptlet(parent); } else if (reader.matches("<jsp:text")) { parseXMLTemplateText(parent); } else if (!pageInfo.isELIgnored() && reader.matches("${")) { parseELExpression(parent, '$'); } else if (!pageInfo.isELIgnored() && !pageInfo.isDeferredSyntaxAllowedAsLiteral() && reader.matches("#{")) { parseELExpression(parent, '#'); } else if (reader.matches("<jsp:")) { parseStandardAction(parent); } else if (!parseCustomTag(parent)) { checkUnbalancedEndTag(); parseTemplateText(parent); } } }
语法树的遍历,访问者模式 。
语法树可以理解也一种数据结构,假如某些语句已经被解析成一棵语法树,那么接下来就是要对此语法树进行处理,但是考虑到为了不把处理操作与数据结构混合在一块,我们需要一种方法将其分离,提供很好的解耦作用,让我们可以在生成语法树的过程中只须关注如何构建相关的数据结构,而在对语法树的处理只须要关注处理的逻辑,这是一种非常巧的设计模式,接下来,通过一个简单的示例代码看看如何实现一个访问者模式,具体操作如下 。
定义访问者操作方法接口,声明所有访问者操作方法 。
访问者模式将数据结构和处理逻辑很好的解耦出来,这种模式经常用在语法树的解析处理上, 熟悉此模式有助于编译过程的理解,JSP 对语法的解析也是如此 。
16.1.3 JSP 编译后的Servlet
JSP 编译后的Servlet类会是怎样子的呢? 它们之间有什么样的映射关系,在探讨JSP与Servlet关系时,先看一个简单的index.jsp 编译成index_jsp.java 后会是怎样子。
index.jsp文件如下所示 。
<%-- Created by IntelliJ IDEA. User: quyixiao Date: 2022/5/22 Time: 11:02 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.sql.*,javax.sql.*,javax.naming.*" %> <%@ page import="com.example.servelettest.Person" %> <%-- 我在测试 --%> <% Person person =new Person(); person.setName("帅哥"); person.setHeight(167); person.setAge(20); request.setAttribute("person", person); %> 名字 : ${person.name} <br> 人身高 : ${person.height}
生成的index_jsp.java文件如下
/* * Generated by the Jasper component of Apache Tomcat * Version: Apache Tomcat/@VERSION@ * Generated at: 2022-08-12 07:36:03 UTC * Note: The last modified time of this file was set to * the last modified time of the source file after * generation to assist with modification tracking. */ package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; import java.sql.*; import javax.sql.*; import javax.naming.*; import com.example.servelettest.Person; public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent { private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory.getDefaultFactory(); private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants; private volatile javax.el.ExpressionFactory _el_expressionfactory; private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map<java.lang.String,java.lang.Long> getDependants() { return _jspx_dependants; } public javax.el.ExpressionFactory _jsp_getExpressionFactory() { if (_el_expressionfactory == null) { synchronized (this) { if (_el_expressionfactory == null) { _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory(); } } } return _el_expressionfactory; } public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() { if (_jsp_instancemanager == null) { synchronized (this) { if (_jsp_instancemanager == null) { _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig()); } } } return _jsp_instancemanager; } public void _jspInit() { } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write('\n'); Person person =new Person(); person.setName("帅哥"); person.setHeight(167); person.setAge(20); request.setAttribute("person", person); out.write("\n"); out.write("名字 : "); out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${person.name}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null, false)); out.write(" <br>\n"); out.write("人身高 : "); out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${person.height}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null, false)); } catch (java.lang.Throwable t) { if (!(t instanceof javax.servlet.jsp.SkipPageException)){ out = _jspx_out; if (out != null && out.getBufferSize() != 0) try { if (response.isCommitted()) { out.flush(); } else { out.clearBuffer(); } } catch (java.io.IOException e) {} if (_jspx_page_context != null) _jspx_page_context.handlePageException(t); else throw new ServletException(t); } } finally { _jspxFactory.releasePageContext(_jspx_page_context); } } }
经过前面的介绍的语法解析及使用访问者模式把index.jsp 文件编译成相应的index_jsp.java文件,可以看到,Servlet类名由JSP 文件名加_jsp拼成,下面看index_jsp.java 文件的详细内容,类包名默认为org.apache.jsp默认有三个导入import javax.servlet.* ,import javax.servlet.http.* , import javax.servlet.jsp.* 。
接下来是真正的类主体,JSP 生成的Java 类都必须继承org.apache.jasper.runtime.HttpJspBase ,这个类的结构图如图16.2所示,它继承HttpServlet 是为了将HttpServlet的所有功能都继承下来,除此之外,还有_jspInit 和 _jspDestory,它们用于在JSP 初始化和销毁时执行,这些方法其实都由Servlet 和service ,init , destory 方法间接调用,所以JSP 生成Servlet主要就是实现这三个方法 。
除了继承HttpJspBase 外,还须实现org.apache.jasper.runtime.JspSourceDependent 接口。这个接口只有一个返回Map<String,Long> 类型的getDependants() 方法,Map 的键值分别的资源名和最后修改时间,这个实现主要是为了记录某些依赖资源是否过时 , 依赖资源可能是Page 指令导入的,也可能是标签文件引用等, 在生成的Servlet时,如果JSP页面中存在上述依赖,则会在Servlet 类中添加一个Static块,Static块会把资源及最后修改时间添加到Map 中。
在JSP 类型的Servlet处理过程中会依赖很多资源,比如,如果操作会话,就需要此次访问的HttpSession对象,如果要操作Context 容器级别的对象,就要ServletContext对象 。最后,还需要一个输出对象用于在处理过程中将内容输出,这些对象在核心方法_jspService中使用, 作为Servlet类,要获取这些对象其实非常简单,因为这些对象本身就属于Servlet属性, 它们相关的方法可供直接获取,但这里因为JSP有自己的标准 , 所以必须按照它的标准,所以必须按照它的标准去实现。
具体JSP标准是怎样的呢?首先为了方便 JSP 的实现, 提供了一个统一的工厂类JSP的标准是怎样的,首先,为了方便JSP 的实现, 提供了一个统一的工厂类JspFactory用于获取不同的资源 ,其次由于按照标准规定不能直接使用Servlet上下文,因此需要定义一个PageContext类封装Servlet 上下文,最后,同样按照标准需要定义一个输出类JspWriter封装Servlet的输出,所以可的看到 ,PageContext 对象通过JspFactory获取,其他的ServletContext 对象,ServletConfig对象,HttpSession对象及JspWriter则通过PageContext对象获取,通过这些对象,再加上前面的语法解析得到的语法树对象,再利用访问者模式对语法树的遍历就可以生成核心处理方法 _jspService了 。
接下来,来看jsp页面如何编译。关于如何找到JspServlet的源码,只能等到后面的博客再来分析,从jsp编译开始分析 。
JspServlet
public void service(HttpServletRequest request, HttpServletResponse response, boolean precompile) throws ServletException, IOException, FileNotFoundException { Servlet servlet; try { if (ctxt.isRemoved()) { throw new FileNotFoundException(jspUri); } if ((available > 0L) && (available < Long.MAX_VALUE)) { if (available > System.currentTimeMillis()) { response.setDateHeader("Retry-After", available); response.sendError (HttpServletResponse.SC_SERVICE_UNAVAILABLE, Localizer.getMessage("jsp.error.unavailable")); return; } // Wait period has expired. Reset. available = 0; } /* * (1) Compile * 将jsp文件编译成servlet */ if (options.getDevelopment() || mustCompile) { synchronized (this) { if (options.getDevelopment() || mustCompile) { // The following sets reload to true, if necessary ctxt.compile(); mustCompile = false; } } } else { if (compileException != null) { // Throw cached compilation exception throw compileException; } } /* * (2) (Re)load servlet class file * 生成Servlet文件对应的对象实例 */ servlet = getServlet(); // If a page is to be precompiled only, return. // 一个页面仅仅只需要编译 if (precompile) { return; } } catch (ServletException ex) { if (options.getDevelopment()) { throw handleJspException(ex); } throw ex; } catch (FileNotFoundException fnfe) { // File has been removed. Let caller handle this. throw fnfe; } catch (IOException ex) { if (options.getDevelopment()) { throw handleJspException(ex); } throw ex; } catch (IllegalStateException ex) { if (options.getDevelopment()) { throw handleJspException(ex); } throw ex; } catch (Exception ex) { if (options.getDevelopment()) { throw handleJspException(ex); } throw new JasperException(ex); } try { /* * (3) Handle limitation of number of loaded Jsps */ if (unloadAllowed) { // 是否要卸载jsp-servlet synchronized(this) { if (unloadByCount) { // 如果配置了限制的数量,则表示ctxt.getRuntimeContext()中只能容纳固定的jsw // 那么如果超过了限制则将队尾jsw移除掉 // 当然,就算没有配置限制的数量,background线程会定时执行,将超过jspIdleTimeout时间的移除掉 if (unloadHandle == null) { unloadHandle = ctxt.getRuntimeContext().push(this); } else if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) { // lastUsageTime表示当前jsw上次使用时间 // ctxt.getRuntimeContext().getLastJspQueueUpdate()这个时间会由background线程定时更新一次 // 如果lastUsageTime 大于 ctxt.getRuntimeContext().getLastJspQueueUpdate()不需要做什么操作 // 第一种情况 // 1. jsw被访问 // 2. background线程执行 // 3. jsw再次被访问 // 4. 符合当前条件,jsw被移动至队首 // 第二种情况 // 1. background线程执行 // 2. jsw第一次被访问 // 3. 不符合条件,而此时应该符合unloadHandle == null // 将最近访问的jsw移动至队首 ctxt.getRuntimeContext().makeYoungest(unloadHandle); // 将unloadHandle移到队首 lastUsageTime = System.currentTimeMillis(); } } else { // 更新最近使用的时间 if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) { lastUsageTime = System.currentTimeMillis(); } } } } /* * (4) Service request */ if (servlet instanceof SingleThreadModel) { // sync on the wrapper so that the freshness // of the page is determined right before servicing synchronized (this) { servlet.service(request, response); } } else { servlet.service(request, response); } } catch (UnavailableException ex) { String includeRequestUri = (String) request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI); if (includeRequestUri != null) { // This file was included. Throw an exception as // a response.sendError() will be ignored by the // servlet engine. throw ex; } int unavailableSeconds = ex.getUnavailableSeconds(); if (unavailableSeconds <= 0) { unavailableSeconds = 60; // Arbitrary default } available = System.currentTimeMillis() + (unavailableSeconds * 1000L); response.sendError (HttpServletResponse.SC_SERVICE_UNAVAILABLE, ex.getMessage()); } catch (ServletException ex) { if(options.getDevelopment()) { throw handleJspException(ex); } throw ex; } catch (IOException ex) { if (options.getDevelopment()) { throw new IOException(handleJspException(ex).getMessage(), ex); } throw ex; } catch (IllegalStateException ex) { if(options.getDevelopment()) { throw handleJspException(ex); } throw ex; } catch (Exception ex) { if(options.getDevelopment()) { throw handleJspException(ex); } throw new JasperException(ex); } }
上面有两个初始化变量,如加粗代码所示单词所示 ,options和ctxt,这两个变量是何时初始化的呢?先来看options初始化。
从图中可以知道,在Tomcat start时,先调用StandardContext的addChild方法将StandardWrapper加入到StandardContext的子节点,再调用子节点的load()方法,在load方法中调用其initServlet()->init()->new EmbeddedServletOptions()。接下来先看一下EmbeddedServletOptions结构。
public final class EmbeddedServletOptions implements Options { /** * 是否让 Jasper 用于开发模式?如果是,检查 JSPs 修改的频率,将通过设置 modificationTestInterval 参数来完成。 */ private boolean development = true; /** * 是否让 Ant 派生出 JSP 页面多个编译,它们将运行在一个独立于 Tomcat 的 JVM 上。true 或者 false, 缺省为 true. */ public boolean fork = true; /** * 是否保存每个页面生成的 java 源代码,而不删除。true 或 false,缺省为 true。 */ private boolean keepGenerated = true; /** * 是否去掉模板文本中行为和指令之间的空格。缺省为 false。 */ private boolean trimSpaces = false; /** * 是否对每个输入行都用一条 print 语句来生成静态内容,以方便调试。true 或 false,缺省为 true。 */ private boolean mappedFile = true; /** * Do we want to include debugging information in the class file? * 类文件在编译时是否显示调试(debugging)信息? true 或 false,缺省为 true。 */ private boolean classDebugInfo = true; /** * 如果“development”属性为 false 且“checkInterval”大于 0,则使用后台编译。“checkInterval”是查看 JSP 页面(包括其附属文件) */ private int checkInterval = 0; // 在一个 useBean action 中,当类属性的值不是一个合法的 bean class 时,Jasper 是否抛出异常?true private boolean errorOnUseBeanInvalidClassAttribute = true; /** * 当编译 JSP 页面时使用的 scratch 目录。缺省为当前 WEB 应用的工作目录。 */ private File scratchDir; /** * 编译 servlet 时要使用的类路径,当 ServletContext 属性 org.apache.jasper.Constants.SERVLET_CLASSPATH 未设置的情况下, */ private String classpath = null; /** * – Ant 将要使用的 JSP 页面编译器,请查阅 Ant 文档获取更多信息。如果该参数未设置,那么默认的 Eclipse JDT Java 编译器将被用 */ private String compiler = null; ... }
这些参数又是如何配置的呢? 还得进入EmbeddedServletOptions的构造函数。
public EmbeddedServletOptions(ServletConfig config, ServletContext context) { ... // 是否去掉模板文本中行为和指令间的空格,缺省为false String trimsp = config.getInitParameter("trimSpaces"); if (trimsp != null) { if (trimsp.equalsIgnoreCase("true")) { trimSpaces = true; } else if (trimsp.equalsIgnoreCase("false")) { trimSpaces = false; } else { if (log.isWarnEnabled()) { log.warn(Localizer.getMessage("jsp.warning.trimspaces")); } } } this.isPoolingEnabled = true; // 确定是否共享标签处理器,true 或 false,缺省为 true。 String poolingEnabledParam = config.getInitParameter("enablePooling"); if (poolingEnabledParam != null && !poolingEnabledParam.equalsIgnoreCase("true")) { if (poolingEnabledParam.equalsIgnoreCase("false")) { this.isPoolingEnabled = false; } else { if (log.isWarnEnabled()) { log.warn(Localizer.getMessage("jsp.warning.enablePooling")); } } } ... }
trimSpaces和enablePooling是两个参数是通过config的getInitParameter方法获取值, 之前分析过config就是StandardWrapperFade,进入其getInitParameter方法。
public String getInitParameter(String name) { return config.getInitParameter(name); } public String getInitParameter(String name) { // 方法 findInitParameter 的参数为参数名,并调用 parameters HashMap 的 get 方法 return (findInitParameter(name)); } public String findInitParameter(String name) { try { parametersLock.readLock().lock(); return parameters.get(name); } finally { parametersLock.readLock().unlock(); } }
tomcat 为了使得StandardWrapper不被外部直接调用其内部方法,因此使用了门面类StandardWrapperFade来包装StandardWrapper,因此getInitParameter()方法实际上调用的是StandardWrapper的parameters属性来获取变量值。
fade又是如何构建的呢?请看下图。
parameters属性又是如何封装的呢?我们继续在代码中寻寻觅觅。
通过截图可知wrapper来源于map.values(),而map.values()来源于children[]数组,因此我们只需要弄清楚children从何而来即可。 在代码中寻寻觅觅,最终得出,StandardContext的childrens是通过findChildren()方法得出。
StandardContext
protected synchronized void startInternal() throws LifecycleException { if (ok) { if (!loadOnStartup(findChildren())){ log.error(sm.getString("standardContext.servletFail")); ok = false; } } } public Container[] findChildren() { synchronized (children) { Container results[] = new Container[children.size()]; return children.values().toArray(results); } }
因此只需要弄明白children是如何添加进去即可。 在代码中寻寻觅觅,发现StandardContext有addChild()方法,在其中打断点,重启Tomcat,看效果 。
StandardWrapper[jsp]来源于ServletDef的定义。而在Tomcat 源码解析一初识中也分析过关于Servlet的定义,Web 应用开发人员一般对这个Servlet 比较陌生,因为他们不会直接与它打交道,既然是Servlet ,那么肯定要声明后才会被部署应用,它被部署到Tomcat
安装目录下,conf目录下的web.xml 文件中,这里的web.xml 文件是Tomcat 全局Web 描述文件,当然也可以配置在工程目录下的WEB-INF的web.xml文件中
JspServlet 的配置如下 。
<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>fork</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>xpowereBy</param-name> <param-value>false</param-value> </init-param> <load-on-startup>3</load-on-startup> <servlet-mapping> <servlet-name>jsp</servlet-name> <url-pattern>*.jsp</url-pattern> <url-pattern>*.jspx</url-pattern> </servlet-mapping> </servlet>
从上面分析得知,jsp的配置来源于conf/web.xml或项目目录下的WEB-INF/web.xml文件,我们来看一个例子。
在config/web.xml配置文件中添加初始化参数trimSpaces,设置其值为false。在EmbeddedServletOptions中打断点 。
通过分析,显然我们的结论正确。 EmbeddedServletOptions的参数是通过config/web.xml或WEB-INF/web.xml中配置,当然有小伙伴肯定会想,如果conf/web.xml和WEB-INF/web.xml中都配置了jsp的servlet,结果会怎样呢?
在conf/web.xml中配置jsp的trimSpaces为false,而在工程目录下WEB-INF/web.xml中配置trimSpaces为true,执行结果。
因此得出结论,如果tomcat全局配置文件conf/web.xml和工程中WEB-INF/web.xml都配置了相同的变量,则工程中WEB-INF/web.xml配置会覆盖掉全局配置文件中的配置,为什么呢?后面的博客再来分析,tomcat对配置文件中的内容会有一个合并的过程,并且工程中的配置文件中的内容会覆盖掉全局配置文件中的内容 。
接下来再看service()方法中另一个变量ctxt变量。
而JspCompilationContext(jspUri, options, config.getServletContext(), this, rctxt); 的rctxt参数又是何时初始化呢?
这些变量只能通过截图的方式告诉你怎样来的,知识点零散,具体的细节部分还是自己去打断点查看,这些变量及配置对后续分析jsp的解析,编译有着重要作用。
其他代码,后面再来分析,先着重分析加粗代码ctxt.compile(); 接下来,进入compile()方法 。
public void compile() throws JasperException, FileNotFoundException { // 创建编译器,默认编译器为 JDTCompiler createCompiler(); if (jspCompiler.isOutDated()) { if (isRemoved()) { throw new FileNotFoundException(jspUri); } try { // 删除work目录下的当前jsp对应的class文件 // 删除work目录下的当前jsp对应的java文件 jspCompiler.removeGeneratedFiles(); jspLoader = null; // 进入代码编译 jspCompiler.compile(); jsw.setReload(true); jsw.setCompilationException(null); } catch (JasperException ex) { // Cache compilation exception jsw.setCompilationException(ex); if (options.getDevelopment() && options.getRecompileOnFail()) { // Force a recompilation attempt on next access jsw.setLastModificationTest(-1); } throw ex; } catch (FileNotFoundException fnfe) { // Re-throw to let caller handle this - will result in a 404 throw fnfe; } catch (Exception ex) { JasperException je = new JasperException( Localizer.getMessage("jsp.error.unable.compile"), ex); // Cache compilation exception jsw.setCompilationException(je); throw je; } } }
先来看编译类的创建 。
public Compiler createCompiler() { if (jspCompiler != null ) { return jspCompiler; } jspCompiler = null; // 如果web.xml中配置了compilerClassName变量 ,则 if (options.getCompilerClassName() != null) { jspCompiler = createCompiler(options.getCompilerClassName()); } else { if (options.getCompiler() == null) { jspCompiler = createCompiler("org.apache.jasper.compiler.JDTCompiler"); if (jspCompiler == null) { jspCompiler = createCompiler("org.apache.jasper.compiler.AntCompiler"); } } else { jspCompiler = createCompiler("org.apache.jasper.compiler.AntCompiler"); if (jspCompiler == null) { jspCompiler = createCompiler("org.apache.jasper.compiler.JDTCompiler"); } } } if (jspCompiler == null) { throw new IllegalStateException(Localizer.getMessage("jsp.error.compiler.config", options.getCompilerClassName(), options.getCompiler())); } jspCompiler.init(this, jsw); return jspCompiler; } protected Compiler createCompiler(String className) { Compiler compiler = null; try { // 通过反射创建编译类 compiler = (Compiler) Class.forName(className).newInstance(); } catch (InstantiationException e) { log.warn(Localizer.getMessage("jsp.error.compiler"), e); } catch (IllegalAccessException e) { log.warn(Localizer.getMessage("jsp.error.compiler"), e); } catch (NoClassDefFoundError e) { if (log.isDebugEnabled()) { log.debug(Localizer.getMessage("jsp.error.compiler"), e); } } catch (ClassNotFoundException e) { if (log.isDebugEnabled()) { log.debug(Localizer.getMessage("jsp.error.compiler"), e); } } return compiler; }
如果没有配置compilerClassName类,tomcat默认使用org.apache.jasper.compiler.JDTCompiler作为jsp编译类。 接下来看如何删除
public void removeGeneratedFiles() { removeGeneratedClassFiles(); // 删除work目录下的当前jsp对应的class文件 try { File javaFile = new File(ctxt.getServletJavaFileName()); // 删除work目录下的当前jsp对应的java文件 if (log.isDebugEnabled()) log.debug("Deleting " + javaFile); if (javaFile.exists()) { // 如果java文件存在,则删除它 if (!javaFile.delete()) { log.warn(Localizer.getMessage( "jsp.warning.compiler.javafile.delete.fail", javaFile.getAbsolutePath())); } } } catch (Exception e) { // Remove as much as possible, log possible exceptions log.warn(Localizer.getMessage("jsp.warning.compiler.classfile.delete.fail.unknown"), e); } } public void removeGeneratedClassFiles() { try { File classFile = new File(ctxt.getClassFileName()); if (log.isDebugEnabled()) log.debug("Deleting " + classFile); if (classFile.exists()) { // 如果class文件存在,则删除它 if (!classFile.delete()) { log.warn(Localizer.getMessage( "jsp.warning.compiler.classfile.delete.fail", classFile.getAbsolutePath())); } } } catch (Exception e) { // Remove as much as possible, log possible exceptions log.warn(Localizer.getMessage("jsp.warning.compiler.classfile.delete.fail.unknown"), e); } }
删除Java文件和Class文件的逻辑还是很简单的,但有一点值得注意,如何找到对应的java文件路径和类文件路径, 以Java文件为例。进入getServletJavaFileName()方法 。
public String getServletJavaFileName() { if (servletJavaFileName == null) { servletJavaFileName = getOutputDir() + getServletClassName() + ".java"; } return servletJavaFileName; }
发现servletJavaFileName来源于两个方法,一个是getOutputDir()方法,另外一个是getServletClassName()方法,因此先研究getOutputDir()方法,是如何获取文件路径的。
public String getOutputDir() { if (outputDir == null) { // 如果outputDir不存在,则创建它 createOutputDir(); } return outputDir; }
依然进入createOutputDir()方法。
protected void createOutputDir() { String path = null; // 1. 是否使用了tag-file if (isTagFile()) { String tagName = tagInfo.getTagClassName(); path = tagName.replace('.', File.separatorChar); path = path.substring(0, path.lastIndexOf(File.separatorChar)); } else { // 获取servlet包名称 ,并将包名中的.替换成/ path = getServletPackageName().replace('.',File.separatorChar); } try { // 2. 获取config/web.xml或工程目录下的WEB-INF/web.xml中的scratchdir配置 // 如果没有,则从ApplicationContext中取javax.servlet.context.tempdir配置 File base = options.getScratchDir(); baseUrl = base.toURI().toURL(); // outputDir = base的绝对路径 + / + path + / outputDir = base.getAbsolutePath() + File.separator + path + File.separator; // 如果目录不存在,则创建目录 if (!makeOutputDir()) { throw new IllegalStateException(Localizer.getMessage("jsp.error.outputfolder")); } } catch (MalformedURLException e) { throw new IllegalStateException(Localizer.getMessage("jsp.error.outputfolder"), e); } } public File getScratchDir() { return scratchDir; } protected boolean makeOutputDir() { synchronized(outputDirLock) { File outDirFile = new File(outputDir); return (outDirFile.mkdirs() || outDirFile.isDirectory()); } }
在createOutputDir()方法中有两个难点,一个是tag-file 的path获取,另外一个是getScratchDir()方法的内部逻辑。先来看一个tag-file的例子。
- 创建 iterator.tag
<%@ tag pageEncoding="GBK" import="java.util.List" %> <!-- 定义了四个标签属性 --> <%@ attribute name="bgColor" %> <%@ attribute name="cellColor" %> <%@ attribute name="title" %> <%@ attribute name="bean" %> <table border="1" bgcolor="${bgColor}"> <tr> <td><b>${title}</b></td> </tr> <% List<String> list = (List<String>) request.getAttribute("a"); // 遍历输出list集合的元素 for (Object ele : list) { %> <tr> <td bgcolor="${cellColor}"> <%=ele%> </td> </tr> <%}%> </table>
- 创建show.jsp 来测试 。
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %> <%@ page import="java.util.*" %> <%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>迭代器tag file</title> <meta name="website" content="http://www.linjw.org" /> </head> <body> <h2>迭代器tag file</h2> <% // 创建集合对象,用于测试Tag File所定义的标签 List<String> a = new ArrayList<String>(); a.add("疯狂Java讲义"); a.add("轻量级Java EE企业应用实战"); a.add("疯狂Ajax讲义"); // 将集合对象放入页面范围 request.setAttribute("a" , a); %> <h3>使用自定义标签</h3> <tags:iterator bgColor="#99dd99" cellColor="#9999cc" title="迭代器标签" bean="a" /> </body> </html>
- 在isTagFile()条件内部打断点
结果输出
这就是tag-file的使用,接下来,分析另外一个getScratchDir()方法,代码中寻寻觅觅,发现scratchDir的初始化在EmbeddedServletOptions的构造函数中。
public EmbeddedServletOptions(ServletConfig config, ServletContext context) { ... /* * scratchdir 当编译 JSP 页面时使用的 scratch 目录。缺省为当前 WEB 应用的工作目录。 */ String dir = config.getInitParameter("scratchdir"); if (dir != null && Constants.IS_SECURITY_ENABLED) { log.info(Localizer.getMessage("jsp.info.ignoreSetting", "scratchdir", dir)); dir = null; } if (dir != null) { scratchDir = new File(dir); } else { // First try the Servlet 2.2 javax.servlet.context.tempdir property scratchDir = (File) context.getAttribute("javax.servlet.context.tempdir"); if (scratchDir == null) { // Not running in a Servlet 2.2 container. // Try to get the JDK 1.2 java.io.tmpdir property dir = System.getProperty("java.io.tmpdir"); if (dir != null) scratchDir = new File(dir); } } if (this.scratchDir == null) { log.fatal(Localizer.getMessage("jsp.error.no.scratch.dir")); return; } if (!(scratchDir.exists() && scratchDir.canRead() && scratchDir.canWrite() && scratchDir.isDirectory())) log.fatal(Localizer.getMessage("jsp.error.bad.scratch.dir", scratchDir.getAbsolutePath())); } ... }
scratchDir目录的获取,一方面从web.xml配置文件中获取,但如果没有配置,则从context的javax.servlet.context.tempdir中获取。
那么,我又有疑问了,javax.servlet.context.tempdir 是何设置值的呢?在代码中寻寻觅觅 。终于在StandardContext的startInternal()方法调用了postWorkDirectory()创建工作目录。
从截图中可以得到,默认工作目录为 ${catalina.base} + “work” + engineName + hostName + contextName构成,因此经过一系列分析,得到工作目录的来源。而继续分析getServletPackageName()获取servlet包名。
public String getServletPackageName() { if (isTagFile()) { String className = tagInfo.getTagClassName(); int lastIndex = className.lastIndexOf('.'); String pkgName = ""; if (lastIndex != -1) { pkgName = className.substring(0, lastIndex); } return pkgName; } else { String dPackageName = getDerivedPackageName(); if (dPackageName.length() == 0) { return basePackageName; } return basePackageName + '.' + getDerivedPackageName(); } } protected String getDerivedPackageName() { if (derivedPackageName == null) { int iSep = jspUri.lastIndexOf('/'); // 如果jspUri 为 /test/user/show.jsp ,则derivedPackageName = test.user // 如果jspUri为 /index.jsp ,则 derivedPackageName 为空串("") derivedPackageName = (iSep > 0) ? JspUtil.makeJavaPackage(jspUri.substring(1,iSep)) : ""; } //derivedPackageName 的默认值为org.apache.jsp return derivedPackageName; }
此时万事具备,看目录获取结果。
接下来,剩下最后一步,看getServletClassName()的获取。
public String getServletClassName() { if (className != null) { return className; } if (isTagFile) { className = tagInfo.getTagClassName(); int lastIndex = className.lastIndexOf('.'); if (lastIndex != -1) { className = className.substring(lastIndex + 1); } } else { // 如果url为 /test/user/show.jsp ,则className= show_jsp // 如果url 为 index_jsp int iSep = jspUri.lastIndexOf('/') + 1; className = JspUtil.makeJavaIdentifier(jspUri.substring(iSep)); } return className; }
最终得到java文件名为/Users/quyixiao/gitlab/tomcat/work/Catalina/localhost/servelet-test-1.0/org/apache/jsp/index_jsp.java。 接下来,进入我们的主角compile()方法。
public void compile() throws FileNotFoundException, JasperException, Exception { compile(true); } public void compile(boolean compileClass) throws FileNotFoundException, JasperException, Exception { compile(compileClass, false); } public void compile(boolean compileClass, boolean jspcMode) throws FileNotFoundException, JasperException, Exception { if (errDispatcher == null) { this.errDispatcher = new ErrorDispatcher(jspcMode); } try { // 解析jsp文件,生成java文件,编译,并返回jsp和java文件行号之间的对应关系 String[] smap = generateJava(); File javaFile = new File(ctxt.getServletJavaFileName()); Long jspLastModified = ctxt.getLastModified(ctxt.getJspFile()); // 记录java文件最后修改时间 javaFile.setLastModified(jspLastModified.longValue()); if (compileClass) { // 2. 修改class文件附加内容,将jsp文件行号与java文件行号对应关系添加到class字节码中, // 并将类装载到StandardContext的类加载器中。 generateClass(smap); // Fix for bugzilla 41606 // Set JspServletWrapper.servletClassLastModifiedTime after successful compile File targetFile = new File(ctxt.getClassFileName()); if (targetFile.exists()) { // 如果class文件存在,记录他的最后修改时间 targetFile.setLastModified(jspLastModified.longValue()); if (jsw != null) { jsw.setServletClassLastModifiedTime( jspLastModified.longValue()); } } } } finally { if (tfp != null && ctxt.isPrototypeMode()) { tfp.removeProtoTypeFiles(null); } // Make sure these object which are only used during the // generation and compilation of the JSP page get // dereferenced so that they can be GC'd and reduce the // memory footprint. tfp = null; errDispatcher = null; pageInfo = null; // Only get rid of the pageNodes if in production. // In development mode, they are used for detailed // error messages. // http://bz.apache.org/bugzilla/show_bug.cgi?id=37062 if (!this.options.getDevelopment()) { pageNodes = null; } if (ctxt.getWriter() != null) { ctxt.getWriter().close(); ctxt.setWriter(null); } } }
compile方法做了3件事情 。
- 解析jsp文件,生成java 的servelt文件,编译,并返回jsp文件和java文件行号之间的对应关系 。
- 修改class文件附加内容,将jsp文件行号与java文件行号对应关系添加到class字节码中,并将类装载到StandardContext的类加载器中。
- 保存_jsp.java 和 _jsp.class文件的最后修改时间,定时器检测到文件最后修改时间修改时, 重新装载它。
先来看第一步,解析jsp文件,并生成servlet的java文件。并研究jsp文件和java文件行号之间的对应关系是如何建立的。
protected String[] generateJava() throws Exception { String[] smapStr = null; long t1, t2, t3, t4; t1 = t2 = t3 = t4 = 0; if (log.isDebugEnabled()) { t1 = System.currentTimeMillis(); } // Setup page info area pageInfo = new PageInfo(new BeanRepository(ctxt.getClassLoader(), errDispatcher), ctxt.getJspFile(), ctxt.isTagFile()); JspConfig jspConfig = options.getJspConfig(); JspConfig.JspProperty jspProperty = jspConfig.findJspProperty(ctxt .getJspFile()); /* * If the current uri is matched by a pattern specified in a * jsp-property-group in web.xml, initialize pageInfo with those * properties. * 若为true,表示不支持EL 语法; */ if (jspProperty.isELIgnored() != null) { pageInfo.setELIgnored(JspUtil.booleanValue(jspProperty .isELIgnored())); } // 若为true,表示不支持<% scripting %>语法; if (jspProperty.isScriptingInvalid() != null) { pageInfo.setScriptingInvalid(JspUtil.booleanValue(jspProperty .isScriptingInvalid())); } // 设置JSP 网页的抬头,扩展名为.jspf; if (jspProperty.getIncludePrelude() != null) { pageInfo.setIncludePrelude(jspProperty.getIncludePrelude()); } // 设置JSP 网页的结尾,扩展名为.jspf。 if (jspProperty.getIncludeCoda() != null) { pageInfo.setIncludeCoda(jspProperty.getIncludeCoda()); } if (jspProperty.isDeferedSyntaxAllowedAsLiteral() != null) { pageInfo.setDeferredSyntaxAllowedAsLiteral(JspUtil.booleanValue(jspProperty .isDeferedSyntaxAllowedAsLiteral())); } if (jspProperty.isTrimDirectiveWhitespaces() != null) { pageInfo.setTrimDirectiveWhitespaces(JspUtil.booleanValue(jspProperty .isTrimDirectiveWhitespaces())); } // Default ContentType processing is deferred until after the page has // been parsed if (jspProperty.getBuffer() != null) { pageInfo.setBufferValue(jspProperty.getBuffer(), null, errDispatcher); } if (jspProperty.isErrorOnUndeclaredNamespace() != null) { pageInfo.setErrorOnUndeclaredNamespace( JspUtil.booleanValue( jspProperty.isErrorOnUndeclaredNamespace())); } if (ctxt.isTagFile()) { try { double libraryVersion = Double.parseDouble(ctxt.getTagInfo() .getTagLibrary().getRequiredVersion()); if (libraryVersion < 2.0) { pageInfo.setIsELIgnored("true", null, errDispatcher, true); } if (libraryVersion < 2.1) { pageInfo.setDeferredSyntaxAllowedAsLiteral("true", null, errDispatcher, true); } } catch (NumberFormatException ex) { errDispatcher.jspError(ex); } } ctxt.checkOutputDir(); String javaFileName = ctxt.getServletJavaFileName(); ServletWriter writer = null; try { /* * The setting of isELIgnored changes the behaviour of the parser * in subtle ways. To add to the 'fun', isELIgnored can be set in * any file that forms part of the translation unit so setting it * in a file included towards the end of the translation unit can * change how the parser should have behaved when parsing content * up to the point where isELIgnored was set. Arghh! * Previous attempts to hack around this have only provided partial * solutions. We now use two passes to parse the translation unit. * The first just parses the directives and the second parses the * whole translation unit once we know how isELIgnored has been set. * TODO There are some possible optimisations of this process. */ // Parse the file ParserController parserCtl = new ParserController(ctxt, this); // Pass 1解析<jsp:directive. , <%@ 开头的行。 Node.Nodes directives = parserCtl.parseDirectives(ctxt.getJspFile()); Validator.validateDirectives(this, directives); // Pass 2 - the whole translation unit pageNodes = parserCtl.parse(ctxt.getJspFile()); // Leave this until now since it can only be set once - bug 49726 if (pageInfo.getContentType() == null && jspProperty.getDefaultContentType() != null) { pageInfo.setContentType(jspProperty.getDefaultContentType()); } if (ctxt.isPrototypeMode()) { // generate prototype .java file for the tag file writer = setupContextWriter(javaFileName); Generator.generate(writer, this, pageNodes); writer.close(); writer = null; return null; } // 验证所有节点,这里同样也用了访问者模式 // 有兴趣根据具体的节点去看其内部实现 Validator.validateExDirectives(this, pageNodes); if (log.isDebugEnabled()) { t2 = System.currentTimeMillis(); } // Collect page info , 用CollectVisitor 访问者模式 ,更新PageInfo信息 , 如修改pageInfo的scriptless字段 Collector.collect(this, pageNodes); // Compile (if necessary) and load the tag files referenced in // this compilation unit. tfp = new TagFileProcessor(); tfp.loadTagFiles(this, pageNodes); if (log.isDebugEnabled()) { t3 = System.currentTimeMillis(); } // Determine which custom tag needs to declare which scripting vars ScriptingVariabler.set(pageNodes, errDispatcher); // Optimizations by Tag Plugins TagPluginManager tagPluginManager = options.getTagPluginManager(); tagPluginManager.apply(pageNodes, errDispatcher, pageInfo); // Optimization: concatenate contiguous template texts. TextOptimizer.concatenate(this, pageNodes); // Generate static function mapper codes. ELFunctionMapper.map(pageNodes); // generate servlet .java file writer = setupContextWriter(javaFileName); Generator.generate(writer, this, pageNodes); writer.close(); writer = null; // The writer is only used during the compile, dereference // it in the JspCompilationContext when done to allow it // to be GC'd and save memory. ctxt.setWriter(null); if (log.isDebugEnabled()) { t4 = System.currentTimeMillis(); log.debug("Generated " + javaFileName + " total=" + (t4 - t1) + " generate=" + (t4 - t3) + " validate=" + (t2 - t1)); } } catch (Exception e) { if (writer != null) { try { writer.close(); writer = null; } catch (Exception e1) { // do nothing } } // Remove the generated .java file File file = new File(javaFileName); if (file.exists()) { if (!file.delete()) { log.warn(Localizer.getMessage( "jsp.warning.compiler.javafile.delete.fail", file.getAbsolutePath())); } } throw e; } finally { if (writer != null) { try { writer.close(); } catch (Exception e2) { // do nothing } } } // JSR45 Support if (!options.isSmapSuppressed()) { smapStr = SmapUtil.generateSmap(ctxt, pageNodes); } // If any proto type .java and .class files was generated, // the prototype .java may have been replaced by the current // compilation (if the tag file is self referencing), but the // .class file need to be removed, to make sure that javac would // generate .class again from the new .java file just generated. tfp.removeProtoTypeFiles(ctxt.getClassFileName()); return smapStr; }
上面代码首先创建了一个pageInfo对象,这个对象用于容纳解析的jsp页面元素信息,先来看对象的构建函数 。
PageInfo(BeanRepository beanRepository, String jspFile, boolean isTagFile) { this.isTagFile = isTagFile; this.jspFile = jspFile; this.beanRepository = beanRepository; this.varInfoNames = new HashSet<String>(); this.taglibsMap = new HashMap<String, TagLibraryInfo>(); this.jspPrefixMapper = new HashMap<String, String>(); this.xmlPrefixMapper = new HashMap<String, LinkedList<String>>(); this.nonCustomTagPrefixMap = new HashMap<String, Mark>(); this.imports = new Vector<String>(); this.dependants = new HashMap<String,Long>(); this.includePrelude = new Vector<String>(); this.includeCoda = new Vector<String>(); this.pluginDcls = new Vector<String>(); this.prefixes = new HashSet<String>(); // 将来生成的jsp对应的java文件默认导入的包是"javax.servlet.*", //"javax.servlet.http.*", "javax.servlet.jsp.*" imports.addAll(Arrays.asList("javax.servlet.*", "javax.servlet.http.*", "javax.servlet.jsp.*")); }
接下来,我们看jspConfig的由来 。 jspConfig对象的初始化来源于EmbeddedServletOptions对象的初始化 。
public EmbeddedServletOptions(ServletConfig config, ServletContext context) { ... tldLocationsCache = TldLocationsCache.getInstance(context); jspConfig = new JspConfig(context); tagPluginManager = new TagPluginManager(context); }
下面一系列的pageInfo初始化都来源于JspConfig.JspProperty对象,因此进入findJspProperty()方法,看其参数是如何得到的。
public JspProperty findJspProperty(String uri) throws JasperException { init(); // 如果以.tag 或 .tagx结尾,则使用默认的jsp属性 if (jspProperties == null || uri.endsWith(".tag") || uri.endsWith(".tagx")) { return defaultJspProperty; } String uriPath = null; int index = uri.lastIndexOf('/'); // 如果uri为/home/admin/ ,则uriPath 为 /home/admin/ // 如果uri 为 /home/admin,则uriPath为 /home/ if (index >=0 ) { uriPath = uri.substring(0, index+1); } // 如果uri 为/home/admin.jsp ,则uriExtension为jsp String uriExtension = null; index = uri.lastIndexOf('.'); if (index >=0) { uriExtension = uri.substring(index+1); } Vector<String> includePreludes = new Vector<String>(); Vector<String> includeCodas = new Vector<String>(); JspPropertyGroup isXmlMatch = null; JspPropertyGroup elIgnoredMatch = null; JspPropertyGroup scriptingInvalidMatch = null; JspPropertyGroup pageEncodingMatch = null; JspPropertyGroup deferedSyntaxAllowedAsLiteralMatch = null; JspPropertyGroup trimDirectiveWhitespacesMatch = null; JspPropertyGroup defaultContentTypeMatch = null; JspPropertyGroup bufferMatch = null; JspPropertyGroup errorOnUndeclaredNamespaceMatch = null; Iterator<JspPropertyGroup> iter = jspProperties.iterator(); while (iter.hasNext()) { JspPropertyGroup jpg = iter.next(); JspProperty jp = jpg.getJspProperty(); // (arrays will be the same length) String extension = jpg.getExtension(); String path = jpg.getPath(); if (extension == null) { // exact match pattern: /a/foo.jsp if (!uri.equals(path)) { // not matched; continue; } } else { // Matching patterns *.ext or /p/* if (path != null && uriPath != null && ! uriPath.startsWith(path)) { // not matched continue; } if (!extension.equals("*") && !extension.equals(uriExtension)) { // not matched continue; } } // We have a match // Add include-preludes and include-codas if (jp.getIncludePrelude() != null) { includePreludes.addAll(jp.getIncludePrelude()); } if (jp.getIncludeCoda() != null) { includeCodas.addAll(jp.getIncludeCoda()); } // If there is a previous match for the same property, remember // the one that is more restrictive. if (jp.isXml() != null) { isXmlMatch = selectProperty(isXmlMatch, jpg); } if (jp.isELIgnored() != null) { elIgnoredMatch = selectProperty(elIgnoredMatch, jpg); } if (jp.isScriptingInvalid() != null) { scriptingInvalidMatch = selectProperty(scriptingInvalidMatch, jpg); } if (jp.getPageEncoding() != null) { pageEncodingMatch = selectProperty(pageEncodingMatch, jpg); } if (jp.isDeferedSyntaxAllowedAsLiteral() != null) { deferedSyntaxAllowedAsLiteralMatch = selectProperty(deferedSyntaxAllowedAsLiteralMatch, jpg); } if (jp.isTrimDirectiveWhitespaces() != null) { trimDirectiveWhitespacesMatch = selectProperty(trimDirectiveWhitespacesMatch, jpg); } if (jp.getDefaultContentType() != null) { defaultContentTypeMatch = selectProperty(defaultContentTypeMatch, jpg); } if (jp.getBuffer() != null) { bufferMatch = selectProperty(bufferMatch, jpg); } if (jp.isErrorOnUndeclaredNamespace() != null) { errorOnUndeclaredNamespaceMatch = selectProperty(errorOnUndeclaredNamespaceMatch, jpg); } } String isXml = defaultIsXml; String isELIgnored = defaultIsELIgnored; String isScriptingInvalid = defaultIsScriptingInvalid; String pageEncoding = null; String isDeferedSyntaxAllowedAsLiteral = defaultDeferedSyntaxAllowedAsLiteral; String isTrimDirectiveWhitespaces = defaultTrimDirectiveWhitespaces; String defaultContentType = defaultDefaultContentType; String buffer = defaultBuffer; String errorOnUndeclaredNamespace = defaultErrorOnUndeclaredNamespace; if (isXmlMatch != null) { isXml = isXmlMatch.getJspProperty().isXml(); } if (elIgnoredMatch != null) { isELIgnored = elIgnoredMatch.getJspProperty().isELIgnored(); } if (scriptingInvalidMatch != null) { isScriptingInvalid = scriptingInvalidMatch.getJspProperty().isScriptingInvalid(); } if (pageEncodingMatch != null) { pageEncoding = pageEncodingMatch.getJspProperty().getPageEncoding(); } if (deferedSyntaxAllowedAsLiteralMatch != null) { isDeferedSyntaxAllowedAsLiteral = deferedSyntaxAllowedAsLiteralMatch.getJspProperty().isDeferedSyntaxAllowedAsLiteral(); } if (trimDirectiveWhitespacesMatch != null) { isTrimDirectiveWhitespaces = trimDirectiveWhitespacesMatch.getJspProperty().isTrimDirectiveWhitespaces(); } if (defaultContentTypeMatch != null) { defaultContentType = defaultContentTypeMatch.getJspProperty().getDefaultContentType(); } if (bufferMatch != null) { buffer = bufferMatch.getJspProperty().getBuffer(); } if (errorOnUndeclaredNamespaceMatch != null) { errorOnUndeclaredNamespace = errorOnUndeclaredNamespaceMatch.getJspProperty().isErrorOnUndeclaredNamespace(); } return new JspProperty(isXml, isELIgnored, isScriptingInvalid, pageEncoding, includePreludes, includeCodas, isDeferedSyntaxAllowedAsLiteral, isTrimDirectiveWhitespaces, defaultContentType, buffer, errorOnUndeclaredNamespace); }
isXml,isELIgnored,isScriptingInvalid,pageEncoding,includePreludes,includeCodas,isDeferedSyntaxAllowedAsLiteral,isTrimDirectiveWhitespaces,defaultContentType,buffer,errorOnUndeclaredNamespace这些属性都来源于jspProperties,而jspProperties又是从哪里来的呢? 进入其init()方法 。
private void init() throws JasperException { // 如果没有被初始化,则进行初始化,这里使用双重较验锁 if (!initialized) { synchronized (this) { if (!initialized) { processWebDotXml(); // 如果文件名以tag或tagx结尾时,则使用下面默认的属性 defaultJspProperty = new JspProperty(defaultIsXml, defaultIsELIgnored, defaultIsScriptingInvalid, null, null, null, defaultDeferedSyntaxAllowedAsLiteral, defaultTrimDirectiveWhitespaces, defaultDefaultContentType, defaultBuffer, defaultErrorOnUndeclaredNamespace); initialized = true; } } } }
在 init()方法中使用了双重较验锁,防止重复初始化,如果uri以tag或tagx结尾,则使用默认的defaultJspProperty属性作为jspProperty,否则调用processWebDotXml()初始化。同样进入processWebDotXml()方法 。
private void processWebDotXml() throws JasperException { WebXml webXml = null; try { webXml = new WebXml(ctxt); boolean validate = Boolean.parseBoolean( ctxt.getInitParameter("org.apache.jasper.XML_VALIDATE")); String blockExternalString = ctxt.getInitParameter("org.apache.jasper.XML_BLOCK_EXTERNAL"); boolean blockExternal; if (blockExternalString == null) { blockExternal = true; } else { blockExternal = Boolean.parseBoolean(blockExternalString); } TreeNode webApp = null; if (webXml.getInputSource() != null) { ParserUtils pu = new ParserUtils(validate, blockExternal); webApp = pu.parseXMLDocument(webXml.getSystemId(), webXml.getInputSource()); } if (webApp == null || getVersion(webApp) < 2.4) { defaultIsELIgnored = "true"; defaultDeferedSyntaxAllowedAsLiteral = "true"; return; } if (getVersion(webApp) < 2.5) { defaultDeferedSyntaxAllowedAsLiteral = "true"; } TreeNode jspConfig = webApp.findChild("jsp-config"); if (jspConfig == null) { return; } jspProperties = new Vector<JspPropertyGroup>(); Iterator<TreeNode> jspPropertyList = jspConfig.findChildren("jsp-property-group"); while (jspPropertyList.hasNext()) { TreeNode element = jspPropertyList.next(); Iterator<TreeNode> list = element.findChildren(); Vector<String> urlPatterns = new Vector<String>(); String pageEncoding = null; String scriptingInvalid = null; String elIgnored = null; String isXml = null; Vector<String> includePrelude = new Vector<String>(); Vector<String> includeCoda = new Vector<String>(); String deferredSyntaxAllowedAsLiteral = null; String trimDirectiveWhitespaces = null; String defaultContentType = null; String buffer = null; String errorOnUndeclaredNamespace = null; while (list.hasNext()) { element = list.next(); String tname = element.getName(); if ("url-pattern".equals(tname)) // 设定值所影响的范围,如:/CH2或/*.jsp; urlPatterns.addElement( element.getBody() ); else if ("page-encoding".equals(tname)) //设定JSP网页的编码; pageEncoding = element.getBody(); else if ("is-xml".equals(tname)) isXml = element.getBody(); else if ("el-ignored".equals(tname)) // 若为true,表示不支持EL语法; elIgnored = element.getBody(); else if ("scripting-invalid".equals(tname)) //若为true,表示不支持<%scripting%>语法; scriptingInvalid = element.getBody(); else if ("include-prelude".equals(tname)) //设置JSP网页的抬头; includePrelude.addElement(element.getBody()); else if ("include-coda".equals(tname)) //设置JSP网页的结尾。 includeCoda.addElement(element.getBody()); // 注意,该属性是在JSP 2.1规范中引入的,JSP 2.1规范对JSP 2.0和Java Server Faces 1.1中的表达式语言进行了统一。在JSP 2.1中,字符序列#{被保留给表达式语言使用,你不能在模板本中使用字符序列#{。 // 如果JSP页面运行在JSP 2.1之前版本的容器中,则没有这个限制。对于JSP 2.1的容器,如果在模板文本中需要出现字符序列#{,那么可以将该属性设置为true。 else if ("deferred-syntax-allowed-as-literal".equals(tname)) // 该属性指示在JSP页面的模板文本中是否允许出现字符序列#{。如果该属性的值为false(默认值),当模板文本中出现字符序列#{时,将引发页面转换错误。 deferredSyntaxAllowedAsLiteral = element.getBody(); else if ("trim-directive-whitespaces".equals(tname)) //删除页面多余的空白 trimDirectiveWhitespaces = element.getBody(); else if ("default-content-type".equals(tname)) defaultContentType = element.getBody(); else if ("buffer".equals(tname)) buffer = element.getBody(); else if ("error-on-undeclared-namespace".equals(tname)) errorOnUndeclaredNamespace = element.getBody(); } if (urlPatterns.size() == 0) { continue; } // Add one JspPropertyGroup for each URL Pattern. This makes // the matching logic easier. for( int p = 0; p < urlPatterns.size(); p++ ) { String urlPattern = urlPatterns.elementAt( p ); String path = null; String extension = null; if (urlPattern.indexOf('*') < 0) { // Exact match path = urlPattern; } else { int i = urlPattern.lastIndexOf('/'); String file; if (i >= 0) { path = urlPattern.substring(0,i+1); file = urlPattern.substring(i+1); } else { file = urlPattern; } // pattern must be "*", or of the form "*.jsp" if (file.equals("*")) { extension = "*"; } else if (file.startsWith("*.")) { extension = file.substring(file.indexOf('.')+1); } // The url patterns are reconstructed as the following: // path != null, extension == null: / or /foo/bar.ext // path == null, extension != null: *.ext // path != null, extension == "*": /foo/* boolean isStar = "*".equals(extension); if ((path == null && (extension == null || isStar)) || (path != null && !isStar)) { if (log.isWarnEnabled()) { log.warn(Localizer.getMessage( "jsp.warning.bad.urlpattern.propertygroup", urlPattern)); } continue; } } JspProperty property = new JspProperty(isXml, elIgnored, scriptingInvalid, pageEncoding, includePrelude, includeCoda, deferredSyntaxAllowedAsLiteral, trimDirectiveWhitespaces, defaultContentType, buffer, errorOnUndeclaredNamespace); JspPropertyGroup propertyGroup = new JspPropertyGroup(path, extension, property); jspProperties.addElement(propertyGroup); } } } catch (Exception ex) { throw new JasperException(ex); } finally { if (webXml != null) { webXml.close(); } } }
上面这些代码,其实就是从xml中读取下面标签的内容 。
- <jsp-config> 包括<taglib> 和<jsp-property-group> 两个子元素。
- <taglib>元素在JSP 1.2 时就已经存在;
- <jsp-property-group>是JSP 2.0 新增的元素:
- <jsp-property-group>元素主要有八个子元素,它们分别为:
- <description>:设定的说明;
- <display-name>:设定名称;
- <url-pattern>:设定值所影响的范围,如: /*.jsp;
- <el-ignored>:若为true,表示不支持EL 语法;
- <scripting-invalid>:若为true,表示不支持<% scripting %>语法;
- <page-encoding>:设定JSP 网页的编码;
- <include-prelude>:设置JSP 网页的抬头,扩展名为.jsp
- <include-coda>:设置JSP 网页的结尾,扩展名为.jsp
那是从哪个xml中读取的呢?进入WebXml的构造函数 。
public WebXml(ServletContext ctxt) throws IOException { // Is a web.xml provided as context attribute? String webXml = (String) ctxt.getAttribute("org.apache.tomcat.util.scan.MergedWebXml"); if (webXml != null) { source = new InputSource(new StringReader(webXml)); systemId = org.apache.tomcat.util.scan.Constants.MERGED_WEB_XML; } // If not available as context attribute, look for an alternative // location if (source == null) { // Acquire input stream to web application deployment descriptor String altDDName = (String)ctxt.getAttribute( Constants.ALT_DD_ATTR); if (altDDName != null) { try { URL uri = new URL(FILE_PROTOCOL+altDDName.replace('\\', '/')); stream = uri.openStream(); source = new InputSource(stream); systemId = uri.toExternalForm(); } catch (MalformedURLException e) { log.warn(Localizer.getMessage( "jsp.error.internal.filenotfound", altDDName)); } } } // Finally, try the default /WEB-INF/web.xml if (source == null) { URL uri = ctxt.getResource(WEB_XML); if (uri == null) { log.warn(Localizer.getMessage( "jsp.error.internal.filenotfound", WEB_XML)); } else { stream = uri.openStream(); source = new InputSource(stream); systemId = uri.toExternalForm(); } } if (source == null) { systemId = null; } else { source.setSystemId(systemId); } }
在WebXml构造函数中,加粗代码就是获取xml的来源,也就是说,jsp-config相关配置又来源于ServletContext,那org.apache.tomcat.util.scan.MergedWebXml属性是何时被设置到ServletContext中的呢?在代码中寻寻觅觅 ,最终发现是webConfig方法中设置。
protected void webConfig() { Set<WebXml> defaults = new HashSet<WebXml>(); // 1. 解析默认的配置,生成WebXml对象(Tomcat使用该对象表示web.xml的解析结果),先解析容器级别的配置,然后再解析Host级别的配置。 // 这样对于同名的配置,Host级别将覆盖容器级别,为了便于后续过程描述,我们暂且称为 "默认WebXml",为了提升性能,ContextConfig // 对默认的WebXml进行了缓存,以避免重复解析 defaults.add(getDefaultWebXmlFragment()); WebXml webXml = createWebXml(); // Parse context level web.xml // 2. 解析web.xml文件,如果StandardContext的altDDName不为空,则将该属性指向文件作为web.xml,否则使用默认的路径,即WEB-INF/web.xml // 解析结果同样为WebXml对象(此时创建的对象主为主WebXml),其他的解析结果要合并到该对象上来,暂时将其称为主WebXml InputSource contextWebXml = getContextWebXmlSource(); parseWebXml(contextWebXml, webXml, false); ServletContext sContext = context.getServletContext(); // Ordering is important here // Step 1. Identify all the JARs packaged with the application // If the JARs have a web-fragment.xml it will be parsed at this // point. // 3. 扫描Web应用所有的Jar包,如果包含META-INF/web-fragment.xml,则解析文件并创建WebXml对象,暂时将其称为片断WebXml Map<String,WebXml> fragments = processJarsForWebFragments(webXml); // Step 2. Order the fragments. Set<WebXml> orderedFragments = null; // 4. 将web-fragment.xml创建的WebXml对象按照Servlet规范进行排序,同时将排序结果对应的JAR文件名列表设置到ServletContext属性中 // 属性名为javax.servlet.context.orderedLibs,该排序非常重要,因为这决定了Filter等执行顺序 。 // 【注意】:尽管Servlet规范定义了web-fragment.xml的排序(绝对排序和相对排序),但是为了降低各个模块的耦合度,Web应用在定义web-fragment.xml // 时应尽量保证相对独立性,减少相互间的依赖,将产生依赖过多的配置尝试放到web.xml中 orderedFragments = WebXml.orderWebFragments(webXml, fragments, sContext); // Step 3. Look for ServletContainerInitializer implementations // 处理ServletContainerInitializers if (ok) { // 5.查找ServletContainerInitializer实现,并创建实例,查找范围分为两部分。 // 5.1 Web应用下的包,如果javax.servlet.context.orderedLibs不为空,仅搜索该属性包含的包,否则搜索WEB-INF/lib下所有的包 // 5.2 容器包:搜索所有的包 // Tomcat返回查找结果列表时,确保Web应用的顺序的容器后,因此容器中实现将先加载 。 // 6. 根据ServletContainerInitializer查询结果以及javax.servlet.annotation.HandleTypes 注解配置,初始化typeInitializerMap和 // initializerClassMap 两个映射(主要用于后续注解检测),前者表示类对应ServletContainerInitializer集合,而后者表示每个 // ServletContainerInitializer 对应的类的集合,具体类由javax.servlet.annotation.HandleTypes注解指定。 processServletContainerInitializers(); } // 7. 当主WebXml 的metadataComplete为false,或者typeInitializerMap不为空时 if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) { // Steps 4 & 5. // 检测javax.servlet.annotation.HandlesTypes注解 // 当WebXml的metadataComplete为false, 查找javax.servlet.annotation.WebServlet ,javax.servlet.annotation.WebFilter // javax.servlet.annotation.WebListener注解配置, 将其合并到WebXml // 处理JAR包内的注解,只处理包含web-fragment.xml的JAR,对于JAR包中的每个类做如下处理。 // 检测javax.servlet.annotation.HandlesTypes注解 // 当 "主WebXml"和片段"WebXml"的metadataComplete均为false,查找javax.servlet.annotation.WebServlet,javax.servlet.annotation.WebFilter // javax.servlet.annotation.WebListener注解配置,将其合并到"片段WebXml" processClasses(webXml, orderedFragments); } if (!webXml.isMetadataComplete()) { // Step 6. Merge web-fragment.xml files into the main web.xml // file. if (ok) { // 如果"主WebXml"的metadataComple为false, 将所有的片段WebXml按照排序合并到"WebXml"中 ok = webXml.merge(orderedFragments); } // Step 7. Apply global defaults // Have to merge defaults before JSP conversion since defaults // provide JSP servlet definition. // 9 将默认的"WebXml" 合并到"主WebXml"中 webXml.merge(defaults); // Step 8. Convert explicitly mentioned jsps to servlets if (ok) { // 配置JspServlet,对于当前Web应用中JspFile属性不为空的Servlet,将其servletClass设置为org.apache.jsper.servlet.JspServlet // (Tomcat提供了JSP引擎),将JspFile设置为Servlet初始化参数,同时将名称 "jsp" 的Servlet(见conf/web.xml) 的初始化参数也 // 复制到该Servlet中 convertJsps(webXml); } // Step 9. Apply merged web.xml to Context if (ok) { // 使用"主WebXml"配置当前StandardContext ,包括Servlet,Filter,Listener 等Servlet 规范中支持的组件,对于ServletContext // 层级对象,直接由StandardContext维护,对于Servlet,则创建StandardWrapper子对象,并添加StandardContext实例。 webXml.configureContext(context); } } else { webXml.merge(defaults); // 默认情况下, defaults就是conf/web.xml文件对应的WebXml对象 convertJsps(webXml); // 将jsp转化为Servlet webXml.configureContext(context); // 根据webxml配置context,比如把定义的servlet转化为wrapper,然后添加到StandardContext中,还包括很多其他的 } // Step 9a. Make the merged web.xml available to other // components, specifically Jasper, to save those components // from having to re-generate it. // TODO Use a ServletContainerInitializer for Jasper // 将合并后的WebXml保存到ServletContext属性中,便于后续处理复用,属性名为org.apache.tomcat.util.scan.MergeWebXml String mergedWebXml = webXml.toXml(); sContext.setAttribute("org.apache.tomcat.util.scan.MergedWebXml", mergedWebXml); if (context.getLogEffectiveWebXml()) { log.info("web.xml:\n" + mergedWebXml); } // Always need to look for static resources // Step 10. Look for static resources packaged in JARs if (ok) { // Spec does not define an order. // Use ordered JARs followed by remaining JARs Set<WebXml> resourceJars = new LinkedHashSet<WebXml>(); for (WebXml fragment : orderedFragments) { // resourceJars.add(fragment); } for (WebXml fragment : fragments.values()) { if (!resourceJars.contains(fragment)) { resourceJars.add(fragment); } } // 查找JAR 包的"META-INF/resource/"下的静态资源,并添加到StandardContext中 processResourceJARs(resourceJars); // See also StandardContext.resourcesStart() for // WEB-INF/classes/META-INF/resources configuration } // Step 11. Apply the ServletContainerInitializer config to the // context if (ok) { // 将ServletContainerInitializer 扫描结果添加到StandardContext,以便StandardContext启动时使用 for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializerClassMap.entrySet()) { if (entry.getValue().isEmpty()) { context.addServletContainerInitializer( entry.getKey(), null); } else { context.addServletContainerInitializer( entry.getKey(), entry.getValue()); } } } // 至此,StandardContext 在正式启动StandardWrapper子对象之前,完成了Web 应用容器的初始化,包括Servlet规范中的各类组件,注解 // 以及可编程方式的支持 // 应用程序注解配置 // 当StandardContext 的ignoreAnnotations 为false时,Tomcat 支持读取如下接口的Java命名服务注解配置,添加相关的JNDI 引用,以便 // 在实例化相关的接口时,进行JNDI 资源依赖注入 . // 支持读取接口如下 : // Web应用程序监听器 // javax.servlet.ServletContextAttributeListener // javax.servlet.ServletRequestListener // javax.servlet.http.HttpSessionAttributeListener // javax.servlet.http.HttpSessionListener // javax.servlet.ServletContextListener // javax.servlet.Filter // javax.servlet. Servlet // 支持读取注解包括注解,属性注解,方法注解,具体注解如下 // 类:javax.annotion.Resource ,javax.annotation.Resources // 属性和方法:javax.annotation.Resource }
上面这段代码的逻辑异常复杂,
- 首先获取全局的catalina.base目录下的conf/web.xml
- 再获取catalina.base目录下的conf/engine名字/host名字/web.xml.default
- 如果配置了org.apache.catalina.deploy.alt_dd,则获取该属性指定的文件,否则获取项目目录下的WEB-INF/web.xml
- 再扫描Web应用所有的Jar包,如果包含META-INF/web-fragment.xml,则获取它。
- 将web-fragment.xml创建的WebXml对象按照Servlet规范进行排序,同时将排序结果对应的JAR文件名列表设置到ServletContext属性中属性名为javax.servlet.context.orderedLibs,该排序非常重要,因为这决定了Filter等执行顺序 。
- 查找javax.servlet.annotation.WebServlet ,javax.servlet.annotation.WebFilter,javax.servlet.annotation.WebListener注解配置, 将其合并到WebXml。
- 将合并后的WebXml保存到ServletContext属性中,便于后续处理复用,属性名为org.apache.tomcat.util.scan.MergeWebXml
所以之前jsp-config配置的由来我相信大家已经知道了,来源特别复杂,后面的博客再来具体的分析这一块了。 接下来看看个例子,在web.xml中添加如下配置。
<jsp-config> <jsp-property-group> <description> Special property group for JSP Configuration JSP example. </description> <display-name>JSPConfiguration</display-name> <url-pattern>*.html</url-pattern> <el-ignored>true</el-ignored> <page-encoding>UTF-8</page-encoding> <scripting-invalid>false</scripting-invalid> <include-prelude></include-prelude> <include-coda></include-coda> </jsp-property-group> <jsp-property-group> <description> Special property group for JSP Configuration JSP example. </description> <display-name>JSPConfiguration</display-name> <url-pattern>*.jsp</url-pattern> <el-ignored>true</el-ignored> <page-encoding>UTF-8</page-encoding> <scripting-invalid>false</scripting-invalid> <include-prelude></include-prelude> <include-coda></include-coda> </jsp-property-group> </jsp-config>
打断点,查看测试结果。
当然,解析xml的代码并没有用之前经典的Digester框架,而是最普通的xml解析,这里就不再深入了 。 如果想深入研究,可以看parseXMLDocument方法和convert()方法的源码。
虽然这些细节的代码对于理解整个框架的大体流程无关大雅,也正是这些细节,才构成了tomcat的强大,我们对tomcat不应该只是将他作为一个工具使用,而应该把它作为艺术来欣赏 。
第一步,先来看指令的解析。
public Node.Nodes parseDirectives(String inFileName) throws FileNotFoundException, JasperException, IOException { // If we're parsing a packaged tag file or a resource included by it // (using an include directive), ctxt.getTagFileJar() returns the // JAR file from which to read the tag file or included resource, // respectively. isTagFile = ctxt.isTagFile(); directiveOnly = true; return doParse(inFileName, null, ctxt.getTagFileJarResource()); }
指令的解析,有一点需要注意,设置了directiveOnly为true,接下来看doParse()方法的内部实现。
private Node.Nodes doParse(String inFileName, Node parent, JarResource jarResource) throws FileNotFoundException, JasperException, IOException { Node.Nodes parsedPage = null; isEncodingSpecifiedInProlog = false; isBomPresent = false; isDefaultPageEncoding = false; JarFile jarFile = (jarResource == null) ? null : jarResource.getJarFile(); String absFileName = resolveFileName(inFileName); String jspConfigPageEnc = getJspConfigPageEncoding(absFileName); // 找出我们正在处理的JSP文档类型和编码类型 determineSyntaxAndEncoding(absFileName, jarFile, jspConfigPageEnc); if (parent != null) { // Included resource, add to dependent list if (jarFile == null) { compiler.getPageInfo().addDependant(absFileName, ctxt.getLastModified(absFileName)); } else { String entry = absFileName.substring(1); compiler.getPageInfo().addDependant( jarResource.getEntry(entry).toString(), Long.valueOf(jarFile.getEntry(entry).getTime())); } } if ((isXml && isEncodingSpecifiedInProlog) || isBomPresent) { /* * Make sure the encoding explicitly specified in the XML * prolog (if any) matches that in the JSP config element * (if any), treating "UTF-16", "UTF-16BE", and "UTF-16LE" as * identical. */ if (jspConfigPageEnc != null && !jspConfigPageEnc.equals(sourceEnc) && (!jspConfigPageEnc.startsWith("UTF-16") || !sourceEnc.startsWith("UTF-16"))) { err.jspError("jsp.error.prolog_config_encoding_mismatch", sourceEnc, jspConfigPageEnc); } } // Dispatch to the appropriate parser if (isXml) { // JSP document (XML syntax) // InputStream for jspx page is created and properly closed in // JspDocumentParser. parsedPage = JspDocumentParser.parse(this, absFileName, jarFile, parent, isTagFile, directiveOnly, sourceEnc, jspConfigPageEnc, isEncodingSpecifiedInProlog, isBomPresent); } else { // Standard syntax InputStreamReader inStreamReader = null; try { inStreamReader = JspUtil.getReader(absFileName, sourceEnc, jarFile, ctxt, err, skip); JspReader jspReader = new JspReader(ctxt, absFileName, sourceEnc, inStreamReader, err); // 开始解析jsp parsedPage = Parser.parse(this, jspReader, parent, isTagFile, directiveOnly, jarResource, sourceEnc, jspConfigPageEnc, isDefaultPageEncoding, isBomPresent); } finally { if (inStreamReader != null) { try { inStreamReader.close(); } catch (Exception any) { } } } } if (jarFile != null) { try { jarFile.close(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } } baseDirStack.pop(); return parsedPage; }
parse方法中的sourceEnc,isDefaultPageEncoding,isBomPresent参数来源于determineSyntaxAndEncoding()方法,因此感兴趣可以研究determineSyntaxAndEncoding()方法的内部实现。 我们解析的是parseDirectives()方法,因此directiveOnly的值为true,进入parse方法 。
public static Node.Nodes parse(ParserController pc, JspReader reader, Node parent, boolean isTagFile, boolean directivesOnly, JarResource jarResource, String pageEnc, String jspConfigPageEnc, boolean isDefaultPageEncoding, boolean isBomPresent) throws JasperException { Parser parser = new Parser(pc, reader, isTagFile, directivesOnly, jarResource); Node.Root root = new Node.Root(reader.mark(), parent, false); // 下面4个参数来源于determineSyntaxAndEncoding()方法 root.setPageEncoding(pageEnc); // UTF-8 root.setJspConfigPageEncoding(jspConfigPageEnc); root.setIsDefaultPageEncoding(isDefaultPageEncoding); root.setIsBomPresent(isBomPresent);// false PageInfo pageInfo = pc.getCompiler().getPageInfo(); if (parent == null && !isTagFile) { // 将web.xml中jsp-config中的配置添加到parser中 parser.addInclude(root, pageInfo.getIncludePrelude()); } if (directivesOnly) { parser.parseFileDirectives(root); } else { while (reader.hasMoreInput()) { parser.parseElements(root); } } if (parent == null && !isTagFile) { parser.addInclude(root, pageInfo.getIncludeCoda()); } Node.Nodes page = new Node.Nodes(root); return page; }
我们在研究如何解析<jsp:directive. , <%@ 之前,先来研究工具类reader,在reader工具类中,有几个经典的方法skipUntil,hasMoreInput,mark,matches方法 。 接下来,我们看看JspReader工具类的实现。首先,抽取出JspReader的工具类。创建工程JspReader
https://github.com/quyixiao/JspReader.git
- JspReader工具类支持多文件读取,先来看一个例子。
public class Test1 { public static void main(String[] args) throws Exception { JspReader reader = new JspReader("/Users/quyixiao/Desktop/testfile/yy.txt", "UTF-8", new ErrorDispatcher()); InputStreamReader inputStreamReader = JspReader.getReader("/Users/quyixiao/Desktop/testfile/xx.txt", "UTF-8", new ErrorDispatcher()); Method method = JspReader.class.getDeclaredMethod("pushFile", String.class, String.class, InputStreamReader.class); method.setAccessible(true); method.invoke(reader, "/Users/quyixiao/Desktop/testfile/xx.txt", "UTF-8", inputStreamReader); for (int i = 0; i < 10; i++) { System.out.println((char) reader.nextChar()); } } }
上面这个例子的意图是什么呢? 创建两个文件xx.txt,yy.txt ,分别在两个文件中添加字符串"aaa",“bbb”,先创建JspReader对象传入yy.txt文件,再通过反射调用pushFile方法将xx.txt文件加入到reader中(pushFile方法是私有方法,只能通过反射调用),再循环调用10次reader.nextChar(),看输出结果 ,结果打印出两个文件的内容 。 如下图所示 。
在这个例子中,可以看到JspReader能够读取两个文件的内容。
public JspReader( String fname, String encoding, ErrorDispatcher err) throws JasperException, FileNotFoundException, IOException { this(fname, encoding, getReader(fname, encoding, err), err); } public static InputStreamReader getReader(String fname, String encoding, ErrorDispatcher err ) throws JasperException, IOException { InputStreamReader reader = null; InputStream in = new FileInputStream(fname); try { // 将资源文件转化为流 reader = new InputStreamReader(in, encoding); } catch (UnsupportedEncodingException ex) { err.jspError("jsp.error.unsupported.encoding", encoding); } return reader; } public JspReader( String fname, String encoding, InputStreamReader reader, ErrorDispatcher err) throws JasperException { this.err = err; sourceFiles = new Vector<String>(); currFileId = 0; size = 0; singleFile = false; pushFile(fname, encoding, reader); } private void pushFile(String file, String encoding, InputStreamReader reader) throws JasperException { String longName = file; // 注册资源文件 int fileid = registerSourceFile(longName); if (fileid == -1) { // Bugzilla 37407: http://bz.apache.org/bugzilla/show_bug.cgi?id=37407 if (reader != null) { try { // 如果注册文件失败,则关闭reader reader.close(); } catch (Exception any) { if (log.isDebugEnabled()) { log.debug("Exception closing reader: ", any); } } } err.jspError("jsp.error.file.already.registered", file); } currFileId = fileid; try { CharArrayWriter caw = new CharArrayWriter(); char buf[] = new char[1024]; for (int i = 0; (i = reader.read(buf)) != -1; ) // 将reader中的内容读取到caw中,每一次读取1024个字节 caw.write(buf, 0, i); caw.close(); if (current == null) { // 第一次添加文件时会创建一个Mark current = new Mark(this, caw.toCharArray(), fileid, getFile(fileid), master, encoding); } else { // 第二次以及第n交添加文件时,会将新加入的文件放到栈顶 ,每一次nextChar()时,都会先从栈顶开始读取文件内容 current.pushStream(caw.toCharArray(), fileid, getFile(fileid), longName, encoding); } } catch (Throwable ex) { ex.printStackTrace(); log.error("Exception parsing file ", ex); // 如果添加文件失败,则将文件 popFile(); err.jspError("jsp.error.file.cannot.read", file); } finally { if (reader != null) { try { reader.close(); } catch (Exception any) { if (log.isDebugEnabled()) { log.debug("Exception closing reader: ", any); } } } } } private int registerSourceFile(final String file) { if (sourceFiles.contains(file)) { return -1; } sourceFiles.add(file); this.size++; return sourceFiles.size() - 1; }
从上面源码中可以看到,可以通过putFile()方法向reader中添加多个文件,第一个文件添加时,会走current = new Mark(this, caw.toCharArray(), fileid, getFile(fileid), master, encoding); 代码,创建一个Mark,当第二个文件添加到reader中时,调用了pushStream()方法,那我们来看看pushStream()方法的内部实现。
public void pushStream(char[] inStream, int inFileId, String name, String inBaseDir, String inEncoding) { // 将之前的mark封装成IncludeState存储到includeStack栈中 includeStack.push(new IncludeState(cursor, line, col, fileId, fileName, baseDir, encoding, stream) ); cursor = 0; line = 1; col = 1; fileId = inFileId; fileName = name; baseDir = inBaseDir; encoding = inEncoding; stream = inStream; }
从pushStream的源码也很简单,就是创建了一个栈includeStack,将之前的mark封装成IncludeState推到操作数栈顶。上面是多文件添加的相关操作,那读取字符操作又做了哪些事情呢?
int nextChar() throws JasperException { if (!hasMoreInput()) return -1; int ch = current.stream[current.cursor]; current.cursor++; if (ch == '\n') { current.line++; current.col = 0; } else { current.col++; } return ch; } boolean hasMoreInput() throws JasperException { // 当前读取流的光标是否大于当前文件的长度 if (current.cursor >= current.stream.length) { // 如果是单文件读取,则文件读取结束 if (singleFile) return false; // 如果是多文件读取,看栈includeStack中是否还有IncludeState元素 // 如果有,则将栈顶中的元素存储的数据替换掉当前已经读取完的文件流 while (popFile()) { if (current.cursor < current.stream.length) return true; } return false; } return true; } private boolean popFile() throws JasperException { if (current == null || currFileId < 0) { return false; } String fName = getFile(currFileId); // 通过currFileId判断栈中是否还有未读取完的文件 currFileId = unregisterSourceFile(fName); if (currFileId < -1) { err.jspError("jsp.error.file.not.registered", fName); } Mark previous = current.popStream(); if (previous != null) { master = current.baseDir; current = previous; return true; } return false; } public Mark popStream() { if ( includeStack.size() <= 0 ) { return null; } IncludeState state = includeStack.pop( ); //将操作数栈顶中的数据覆盖掉当前读取完的文件信息 cursor = state.cursor; line = state.line; col = state.col; fileId = state.fileId; fileName = state.fileName; baseDir = state.baseDir; stream = state.stream; return this; }
看源码可能有点难懂,我们来看个图吧。
我相信有这个图后,对上面一系列操作你更容易理解 。
那来看第二个例子。
public class Test2 { public static void main(String[] args) throws Exception { JspReader reader = new JspReader("/Users/quyixiao/Desktop/testfile/zz.txt", "UTF-8", new ErrorDispatcher()); Mark mark0 = reader.mark(); Mark mark = reader.skipUntil("\""); System.out.println(mark); System.out.println(reader.getText(mark0,mark)); String str = "<%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %>"; int index = str.indexOf("\""); System.out.println(index); } }
这个文件要测试的方法也很简单,首先在zz.txt文件中保存了字符串<%@ page contentType=“text/html;charset=UTF-8” language=“java” %> , 调用skipUntil()方法获取zz.txt文件中第一个出现 " 又引号位置mark,再调用reader.getText(mark0,mark)方法,截取出mark0和mark之间的字符串。
有了这个例子后,再来看skipUntil()和getText()方法就简单多了。 先看skipUntil()方法 。
Mark skipUntil(String limit) throws JasperException { Mark ret = mark(); int limlen = limit.length(); char firstChar = limit.charAt(0); Boolean result = null; Mark restart = null; skip: while ((result = indexOf(firstChar, ret)) != null) { if (result.booleanValue()) { if (restart != null) { restart.init(current, singleFile); } else { restart = mark(); } // 因为indexOf方法只匹配到了limit的第一个字符 // 因此需要对limit除第0个字符外的后面字符进行匹配 for (int i = 1; i < limlen; i++) { if (peekChar() == limit.charAt(i)) { nextChar(); } else { // 如果匹配失败,则从mark=restart开始,重新进行第一个字符的匹配 current.init(restart, singleFile); continue skip; } } return ret; } } return null; } private Boolean indexOf(char c, Mark mark) throws JasperException { if (!hasMoreInput()) return null; int end = current.stream.length; int ch; int line = current.line; int col = current.col; int i = current.cursor; for (; i < end; i++) { // 每一次进来,先读取第i个字符 // 当找到对应的字符后调用 current.update(i + 1, line, col); // cursor = i + 1 ,保证下次读取时当前 current.cursor是未读取过的字符 ch = current.stream[i]; if (ch == c) { mark.update(i, line, col); } // 如果当前字符是\n ,则行号+1,重新初始化列 if (ch == '\n') { line++; col = 0; } else { col++; } if (ch == c) { current.update(i + 1, line, col); return Boolean.TRUE; } } current.update(i, line, col); return Boolean.FALSE; }
仔细看代码,看是很容易理解的,为了方便理解,还是先来看个图吧。
再来看getText()方法 。
String getText(Mark start, Mark stop) throws JasperException { Mark oldstart = mark(); reset(start); CharArrayWriter caw = new CharArrayWriter(); while (!markEquals(stop)) { caw.write(nextChar()); } caw.close(); setCurrent(oldstart); return caw.toString(); } void reset(Mark mark) { current = new Mark(mark); }
getText()方法的原理就很简单了,先保存当前mark(),再将当前光标重置到start位置,再从start位置向后一个一个的读取字符,直到stop,再将光标重置到之前保存的oldstart。 返回读取到的字符即可。
再来看tomcat的JspReader工具类。
再来看看matches()方法 。
boolean matches(String string) throws JasperException { int len = string.length(); int cursor = current.cursor; int streamSize = current.stream.length; if (cursor + len < streamSize) { //Try to scan in memory int line = current.line; int col = current.col; int ch; int i = 0; for (; i < len; i++) { ch = current.stream[i + cursor]; if (string.charAt(i) != ch) { return false; } if (ch == '\n') { line++; col = 0; } else { col++; } } current.update(i + cursor, line, col); } else { Mark mark = mark(); int ch = 0; int i = 0; do { ch = nextChar(); char c = string.charAt(i++); if (((char) ch) != c) { setCurrent(mark); return false; } } while (i < len); } return true; }
matches方法的原理很简单,但是为了避免数组越界,分为两种情况,当cursor + len < streamSize和cursor + len >= streamSize两种情况 。进行匹配得到的结果都一样,都是从cursor向后进行匹配string长度的字符,如果每一个字符都匹配上了,则更新cursor并返回true,只要有其中一个字符没有匹配上,则不更新cursor,返回false。
我们再来看在源码中使用得比较多的一个方法 skipELExpression(),从名字上来看,是跳过EL表达式,那么实现原理是什么呢?
Mark skipELExpression() throws JasperException { // ELExpressionBody. // Starts with "#{" or "${". Ends with "}". // May contain quoted "{", "}", '{', or '}'. Mark last = mark(); boolean singleQuoted = false, doubleQuoted = false; int currentChar; do { currentChar = nextChar(last); while (currentChar == '\\' && (singleQuoted || doubleQuoted)) { // 如果之前有双引号或单引号,当前字符是\ ,则反斜线后面无论是什么字符都越过它 nextChar(); currentChar = nextChar(); } if (currentChar == -1) { return null; } // 第一次读取到双引号后doubleQuoted = true // 第二次读取到双引号后doubleQuoted 为false // 只要双引号读取到的是单数,即使遇到 } ,也不退出循环 if (currentChar == '"' && !singleQuoted) { doubleQuoted = !doubleQuoted; } else if (currentChar == '\'' && !doubleQuoted) { singleQuoted = !singleQuoted; } // 如果之前读到过单引号或又引号,即使当前字符是} ,也不退出循环 } while (currentChar != '}' || (singleQuoted || doubleQuoted)); return last; }
先来看个例子。
public class Test3 { public static void main(String[] args) throws Exception { JspReader reader = new JspReader("/Users/quyixiao/gitlab/JspReader/src/test/tmp/bb.txt", "UTF-8", new ErrorDispatcher()); Mark mark = reader.skipELExpression(); System.out.println(mark); } }
- 如果bb.txt文件内容为abc#{xxx}dd,结果输出/Users/quyixiao/gitlab/JspReader/src/test/tmp/bb.txt(1,9),刚好跳过#{xxx}。
- 如果文件内容为ab"{aa}"c#{xxx}dd,输出为/Users/quyixiao/gitlab/JspReader/src/test/tmp/bb.txt(1,15),则也刚好是越过#{xxx}。
- 当文件内容为ab"{a"a}c#{xxx}dd时,则输出mark为 /Users/quyixiao/gitlab/JspReader/src/test/tmp/bb.txt(1,8),则刚好越过"{a"a} 。
- 当文件内容为ab"{a\"a}"c#{xxx}dd,则输出mark为/Users/quyixiao/gitlab/JspReader/src/test/tmp/bb.txt(1,17),刚好越过#{xxx}。
通过这些例子的分析,我相信你再遇到源码中使用skipELExpression()方法,你也不再感觉到陌生 。
再来看这样一个工具类。
Mark skipUntilIgnoreEsc(String limit, boolean ignoreEL) throws JasperException { Mark ret = mark(); int limlen = limit.length(); int ch; int prev = 'x'; // Doesn't matter char firstChar = limit.charAt(0); skip: for (ch = nextChar(ret); ch != -1; prev = ch, ch = nextChar(ret)) { // 如果前一个字符也是\ ,则当前\ 不用作转义字符 if (ch == '\\' && prev == '\\') { ch = 0; // Double \ is not an escape char anymore // 如果前一个字符是\ ,当前字符不是\ ,则无论当前字符是什么,都跳过它 } else if (prev == '\\') { continue; } else if (!ignoreEL && (ch == '$' || ch == '#') && peekChar() == '{') { // 如果ignoreEL为false 忽略掉el表达式 // 则跳过当前字符流的el表达式 nextChar(); // 定位到下一个 } skipELExpression(); } else if (ch == firstChar) { // 从流中匹配到limit.get(0)个字符后面的每个字符逐一匹配 for (int i = 1; i < limlen; i++) { if (peekChar() == limit.charAt(i)) nextChar(); else // 只要其中一个字符不匹配,则从刚刚匹配到的第一个字符的后一个字符开始 // 重新开始第一个字符的匹配 continue skip; } return ret; } } return null; }
tomcat的JspReader比我们例子中多了一个JspCompilationContext,主要区别在于查找InputStream上,tomcat可以通从JspCompilationContext获取文件流信息而已,而我们例子中是直接通过读取本地文件获取文件流,其他源码的没有什么区别。
言归正传,继续 parseFileDirectives()方法的解析 。
private void parseFileDirectives(Node parent) throws JasperException { // 是否是单个文件解析,true表示为单文件解析 reader.setSingleFile(true); reader.skipUntil("<"); while (reader.hasMoreInput()) { start = reader.mark(); // 如果是<%-- Created by xxx --%> 注释,则跳过 if (reader.matches("%--")) { // Comment reader.skipUntil("--%>"); } else if (reader.matches("%@")) { parseDirective(parent); } else if (reader.matches("jsp:directive.")) { parseXMLDirective(parent); // <%!%>称作声明,其中写的内容将来会直接翻译在Servlet类中,因为我们可以在类中定义方法和属性以及全局变量,所以我们可以在<%!%>中声 // 明方法、属性、全局变量 // 如 <%! int sum=1; %>,则忽略掉 } else if (reader.matches("%!")) { // Declaration reader.skipUntil("%>"); // <%=%>称作jsp表达式,用于将已经声明的变量或者表达式输出到网页上面 // 如 <%= this.sum %>,则忽略掉 } else if (reader.matches("%=")) { // Expression reader.skipUntil("%>"); // <% %>叫做脚本片段,其中写的内容会翻译在Servlet的Service方法中,显然我们可以在Service方法中定义局部变量或者调用其他方法,但是不能 // 在Service中再定义其他的方法,也就是我们可以在<%%>中定义局部变量或者调用方法,但不能定义方法。在jsp页面可以有多个脚本片段,但是多 // 个脚本片段之间要保证结构完整 // 如果是 <% for (int i=0;i<3;i++) {%> 忽略掉 } else if (reader.matches("%")) { // Scriptlet reader.skipUntil("%>"); } reader.skipUntil("<"); } }
经过分析,只有以<%@开头的行才会进入parseDirective()方法,而<%@ %>:这表示指令,主要用来提供整个jsp页面相关的信息,并且用来设定jsp页面的相关属性,例如网页的编码格式、语法、信息等。目前有三种指令:page、include、taglib。page指令是最复杂的jsp指令,它的主要功能为设定整个jsp页面的的属性和相关功能。include指令表示在jsp编译时引入一个文件包,这个引入过程是静态的,而引入的文件可以是jsp页面、html页面、文本文件或是一段java程序。taglib能让用户自定义新的标签,如本例中的。
- <%@ page contentType=“text/html;charset=UTF-8” language=“java” %>
- <%@ page import=“java.sql.,javax.sql.,javax.naming.*” %>
- <%@ page import=“com.example.servelettest.Person” %>
- <%@ taglib prefix=“ex” uri=“/WEB-INF/hello.tld” %>
接下来进入parseDirective()方法 。
private void parseDirective(Node parent) throws JasperException { //跳过所有的空格 reader.skipSpaces(); String directive = null; if (reader.matches("page")) { directive = "<%@ page"; if (isTagFile) { err.jspError(reader.mark(), "jsp.error.directive.istagfile", directive); } // 解析page指令 parsePageDirective(parent); } else if (reader.matches("include")) { directive = "<%@ include"; parseIncludeDirective(parent); } else if (reader.matches("taglib")) { if (directivesOnly) { return; } directive = "<%@ taglib"; // 如解析<%@ taglib prefix="ex" uri="/WEB-INF/hello.tld" %> parseTaglibDirective(parent); } else if (reader.matches("tag")) { directive = "<%@ tag"; if (!isTagFile) { err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", directive); } parseTagDirective(parent); } else if (reader.matches("attribute")) { directive = "<%@ attribute"; if (!isTagFile) { err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", directive); } parseAttributeDirective(parent); } else if (reader.matches("variable")) { directive = "<%@ variable"; if (!isTagFile) { err.jspError(reader.mark(), "jsp.error.directive.isnottagfile", directive); } parseVariableDirective(parent); } else { err.jspError(reader.mark(), "jsp.error.invalid.directive"); } // 跳过所有空格 reader.skipSpaces(); // 解析完上面的内容后,如果不是以%> 结尾,则语法不正确,抛出异常 if (!reader.matches("%>")) { err.jspError(start, "jsp.error.unterminated", directive); } }
对于指令的解析,分两种情况,普通的指令有如下三种page、include、taglib,但对于tag文件,则还有tag,attribute,variable三种指令 ,六种指令,这里也不一一分析,只选page指令的源码来分析好了,其他指令大同小异,遇到具体问题再做分析 。 进入parsePageDirective()方法 。
private void parsePageDirective(Node parent) throws JasperException { Attributes attrs = parseAttributes(true); Node.PageDirective n = new Node.PageDirective(attrs, start, parent); for (int i = 0; i < attrs.getLength(); i++) { // 如果page指令是<%@ page import="java.sql.*,javax.sql.*,javax.naming.*" %> // 则将import的值添加到Node.PageDirective的imports属性中 if ("import".equals(attrs.getQName(i))) { n.addImport(attrs.getValue(i)); } } }
继续进入parseAttributes()方法 。
Attributes parseAttributes(boolean pageDirective) throws JasperException { UniqueAttributesImpl attrs = new UniqueAttributesImpl(pageDirective); //跳过所有空格 reader.skipSpaces(); int ws = 1; try { // 关键代码还是解析属性 while (parseAttribute(attrs)) { // 按照严格的语法 ,attribute之间是有空格的,如果没有 // 空格,则抛出异常 if (ws == 0 && STRICT_WHITESPACE) { err.jspError(reader.mark(), "jsp.error.attribute.nowhitespace"); } ws = reader.skipSpaces(); } } catch (IllegalArgumentException iae) { // Duplicate attribute err.jspError(reader.mark(), "jsp.error.attribute.duplicate"); } return attrs; }
还是看parseAttributes如何实现。
private boolean parseAttribute(AttributesImpl attrs) throws JasperException { String qName = parseName(); if (qName == null) return false; boolean ignoreEL = pageInfo.isELIgnored(); String localName = qName; String uri = ""; int index = qName.indexOf(':'); if (index != -1) { String prefix = qName.substring(0, index); uri = pageInfo.getURI(prefix); if (uri == null) { err.jspError(reader.mark(), "jsp.error.attribute.invalidPrefix", prefix); } localName = qName.substring(index + 1); } // 跳过所有的空格 reader.skipSpaces(); // 如果跳过所有空格后的字符不是 = ,则抛出jsp语法异常 if (!reader.matches("=")) err.jspError(reader.mark(), "jsp.error.attribute.noequal"); // 跳过所有的空格 reader.skipSpaces(); char quote = (char) reader.nextChar(); // 如果等于号后面不是单引号或双引号,则抛出jsp 语法异常 if (quote != '\'' && quote != '"') err.jspError(reader.mark(), "jsp.error.attribute.noquote"); String watchString = ""; if (reader.matches("<%=")) { watchString = "%>"; ignoreEL = true; } // 如果 双引号或单引号之后是 "<%=或 '<%=(包括自身单引号或双引号) ,则 // watchString为 %>" 或 %>' // 如果只是单纯的以双引号或单引号开头,并没有<%=,则watchString为 " 或 ' watchString = watchString + quote; // 截取出双引号或单引号内的内容 String attrValue = parseAttributeValue(watchString, ignoreEL); attrs.addAttribute(uri, localName, qName, "CDATA", attrValue); return true; } /** * Name ::= (Letter | '_' | ':') (Letter | Digit | '.' | '_' | '-' | ':')* */ private String parseName() throws JasperException { char ch = (char) reader.peekChar(); if (Character.isLetter(ch) || ch == '_' || ch == ':') { StringBuilder buf = new StringBuilder(); buf.append(ch); reader.nextChar(); ch = (char) reader.peekChar(); while (Character.isLetter(ch) || Character.isDigit(ch) || ch == '.' || ch == '_' || ch == '-' || ch == ':') { buf.append(ch); reader.nextChar(); ch = (char) reader.peekChar(); } return buf.toString(); } return null; }
上面的代码看上去很复杂,实际上也只是对指令的基本较验,先获取名称,再获取相应的值 ,接下来看值的获取代码 。
/** * AttributeValueDouble ::= (QuotedChar - '"')* ('"' | <TRANSLATION_ERROR>) * RTAttributeValueDouble ::= ((QuotedChar - '"')* - ((QuotedChar-'"')'%>"') * ('%>"' | TRANSLATION_ERROR) */ private String parseAttributeValue(String watch, boolean ignoreEL) throws JasperException { boolean quoteAttributeEL = ctxt.getOptions().getQuoteAttributeEL(); Mark start = reader.mark(); // In terms of finding the end of the value, quoting EL is equivalent to // ignoring it. Mark stop = reader.skipUntilIgnoreEsc(watch, ignoreEL || quoteAttributeEL); if (stop == null) { err.jspError(start, "jsp.error.attribute.unterminated", watch); } String ret = null; try { char quote = watch.charAt(watch.length() - 1); // If watch is longer than 1 character this is a scripting // expression and EL is always ignored boolean isElIgnored = pageInfo.isELIgnored() || watch.length() > 1; ret = AttributeParser.getUnquoted(reader.getText(start, stop), quote, isElIgnored, pageInfo.isDeferredSyntaxAllowedAsLiteral(), quoteAttributeEL); } catch (IllegalArgumentException iae) { err.jspError(start, iae.getMessage()); } if (watch.length() == 1) // quote return ret; // Put back delimiter '<%=' and '%>', since they are needed if the // attribute does not allow RTexpression. return "<%=" + ret + "%>"; }
上面代码通过skipUntilIgnoreEsc()方法获取stop的mark,再截取start到stop之间的字符串即可,只不过skipUntilIgnoreEsc方法的参数可能是单引号,或双引号或 %>’ 或 %>" 而已。 如
<%@ page contentType=“text/html;charset=UTF-8” language=“java” %>
如<%@ page import=“java.sql.,javax.sql.,javax.naming.*” %>
如index.jsp
最终被解析成
上面截图中,有几个重要的变量,首先当前节点类型为Node.Nodes,而startMark对应jsp中每一行读取<% page的起始位置,理解了这个方法后,再来看validateDirectives()方法 。
public static void validateDirectives(Compiler compiler, Node.Nodes page) throws JasperException { page.visit(new DirectiveVisitor(compiler)); }
看到visit()方法,大家第一想到的肯定是访问者模式,这种模式往往用来处理比较复杂但有规率的代码逻辑,很多的底层框架都用到这种模式,比如ASM框架, 这种模式说容易也容易,说复杂也复杂,容易在于有规率,大量的访问visit()方法,一般从父节点向子节点逐一遍历,到最后,打断点,自己都不知道自己身处何处。因此在看这种模式的源码时,一定要清楚当前this是谁,不然肯定被绕晕了,在本例中page为Node.Nodes。
public void visit(Visitor v) throws JasperException { Iterator<Node> iter = list.iterator(); while (iter.hasNext()) { Node n = iter.next(); n.accept(v); } }
从validateDirectives()方法中可以看出 。 此时的Visitor为DirectiveVisitor,而list中有Node.Root节点,因此最终调用Node.Root的accept()方法。
public void accept(Visitor v) throws JasperException { v.visit(this); }
因此不要被绕晕了,v为Validator.DirectiveVisitor,而this为Node.Root,进入Validator.DirectiveVisitor的visit方法 。
public void visit(Root n) throws JasperException { doVisit(n); visitBody(n); } protected void doVisit(Node n) throws JasperException { // NOOP by default }
我们要清楚 ,doVisit()和visitBody()都是Validator.DirectiveVisitor中的方法 。 而n是Node.Root节点,doVisit()为空实现,接着看visitBody()方法的内部实现。
protected void visitBody(Node n) throws JasperException { if (n.getBody() != null) { n.getBody().visit(this); } }
从之前的截图中得到n.getBody()为Node.Nodes,而this依然是Validator.DirectiveVisitor,再次进入visit()方法,但此时需要注意的是List<Node> 的iter不再是Node.Root,而是Node.PageDirective,再次进入accept(),但需要注意,v 依然是Validator.DirectiveVisitor,但this却是Node.PageDirective,从下图中可以看出, PageDirective重载了visit的几个方法 。
- public void visit(Node.IncludeDirective n)
- public void visit(Node.PageDirective n)
- public void visit(Node.TagDirective n)
- public void visit(Node.AttributeDirective n)
- public void visit(Node.VariableDirective n)
我们进入PageDirective的visit方法中。
public void visit(Node.PageDirective n) throws JasperException { JspUtil.checkAttributes("Page directive", n, pageDirectiveAttrs, err); Attributes attrs = n.getAttributes(); for (int i = 0; attrs != null && i < attrs.getLength(); i++) { String attr = attrs.getQName(i); String value = attrs.getValue(i); // 脚本语言 指定页面中使用的脚本语言 <%@ page language= "java" %> if ("language".equals(attr)) { if (pageInfo.getLanguage(false) == null) { pageInfo.setLanguage(value, n, err, true); } else if (!pageInfo.getLanguage(false).equals(value)) { err.jspError(n, "jsp.error.page.conflict.language", pageInfo.getLanguage(false), value); } // 包名.类名 指定当前页面继承的父类,一般很少使用 <%@ page extends="mypackage.SampleClass"%> } else if ("extends".equals(attr)) { if (pageInfo.getExtends(false) == null) { pageInfo.setExtends(value); } else if (!pageInfo.getExtends(false).equals(value)) { err.jspError(n, "jsp.error.page.conflict.extends", pageInfo.getExtends(false), value); } // text/html; charset = ISO-8859-1、 // text/xml;charset = UTF-8 等 指定 MIME 类型和字符编码 <%@ page contentType="text/html;charset=UTF-8" %> } else if ("contentType".equals(attr)) { if (pageInfo.getContentType() == null) { pageInfo.setContentType(value); } else if (!pageInfo.getContentType().equals(value)) { err.jspError(n, "jsp.error.page.conflict.contenttype", pageInfo.getContentType(), value); } // true(默认值)、false 指定页面是否使用 session <%@ page session="false" %> } else if ("session".equals(attr)) { if (pageInfo.getSession() == null) { pageInfo.setSession(value, n, err); } else if (!pageInfo.getSession().equals(value)) { err.jspError(n, "jsp.error.page.conflict.session", pageInfo.getSession(), value); } // none、缓冲区大小(默认值为 8kb) 指定输出流是否有缓冲区 <%@ page buffer="16kb" %> } else if ("buffer".equals(attr)) { if (pageInfo.getBufferValue() == null) { pageInfo.setBufferValue(value, n, err); } else if (!pageInfo.getBufferValue().equals(value)) { err.jspError(n, "jsp.error.page.conflict.buffer", pageInfo.getBufferValue(), value); } // true(默认值)、false 指定缓冲区是否自动清除 <%@ page autoFlush="true" %> } else if ("autoFlush".equals(attr)) { if (pageInfo.getAutoFlush() == null) { pageInfo.setAutoFlush(value, n, err); } else if (!pageInfo.getAutoFlush().equals(value)) { err.jspError(n, "jsp.error.page.conflict.autoflush", pageInfo.getAutoFlush(), value); } // true(默认值)、false 是否允许多线程使用 <%@ page isThreadSafe="false" %> } else if ("isThreadSafe".equals(attr)) { if (pageInfo.getIsThreadSafe() == null) { pageInfo.setIsThreadSafe(value, n, err); } else if (!pageInfo.getIsThreadSafe().equals(value)) { err.jspError(n, "jsp.error.page.conflict.isthreadsafe", pageInfo.getIsThreadSafe(), value); } // true(默认值)、false 指定页面是否忽略 JSP 中的 EL <%@ page isELIgnored="false" %> } else if ("isELIgnored".equals(attr)) { if (pageInfo.getIsELIgnored() == null) { pageInfo.setIsELIgnored(value, n, err, true); } else if (!pageInfo.getIsELIgnored().equals(value)) { err.jspError(n, "jsp.error.page.conflict.iselignored", pageInfo.getIsELIgnored(), value); } // true、false(默认值) 指定当前页面为错误页面 <%@ page isErrorpage="true" %> } else if ("isErrorPage".equals(attr)) { if (pageInfo.getIsErrorPage() == null) { pageInfo.setIsErrorPage(value, n, err); } else if (!pageInfo.getIsErrorPage().equals(value)) { err.jspError(n, "jsp.error.page.conflict.iserrorpage", pageInfo.getIsErrorPage(), value); } // 页面路径 指定当前 JSP 页面发生异常时,需要重定向的错误页面 <%@ page errorpage="myerrorpage.jsp" %> // 注意:myerrorpage.jsp 的 isErrorpage 值必须为 true } else if ("errorPage".equals(attr)) { if (pageInfo.getErrorPage() == null) { pageInfo.setErrorPage(value); } else if (!pageInfo.getErrorPage().equals(value)) { err.jspError(n, "jsp.error.page.conflict.errorpage", pageInfo.getErrorPage(), value); } // 页面的描述信息 定义 JSP 页面的描述信息,可以使用 getServletInfo() 方法获取 <%@ page info="这里是编程帮的页面信息"%> } else if ("info".equals(attr)) { if (pageInfo.getInfo() == null) { pageInfo.setInfo(value); } else if (!pageInfo.getInfo().equals(value)) { err.jspError(n, "jsp.error.page.conflict.info", pageInfo.getInfo(), value); } // jsp文件本身的编码 } else if ("pageEncoding".equals(attr)) { if (pageEncodingSeen) err.jspError(n, "jsp.error.page.multi.pageencoding"); // 'pageEncoding' can occur at most once per file pageEncodingSeen = true; String actual = comparePageEncodings(value, n); n.getRoot().setPageEncoding(actual); // 该属性指示在JSP页面的模板文本中是否允许出现字符序列#{。如果该属性的值为false(默认值),当模板文本中出现字符序列#{时,将引发页面转换错误。 // 注意,该属性是在JSP 2.1规范中引入的,JSP 2.1规范对JSP 2.0和Java Server Faces 1.1中的表达式语言进行了统一。在JSP 2.1中,字符序列#{被保留给表达式语言使用,你不能在模板本中使用字符序列#{。如果JSP页面运行在JSP 2.1之前版本的容器中,则没有这个限制。对于JSP 2.1的容器,如果在模板文本中需要出现字符序列#{,那么可以将该属性设置为true。 } else if ("deferredSyntaxAllowedAsLiteral".equals(attr)) { if (pageInfo.getDeferredSyntaxAllowedAsLiteral() == null) { pageInfo.setDeferredSyntaxAllowedAsLiteral(value, n, err, true); } else if (!pageInfo.getDeferredSyntaxAllowedAsLiteral() .equals(value)) { err .jspError( n, "jsp.error.page.conflict.deferredsyntaxallowedasliteral", pageInfo .getDeferredSyntaxAllowedAsLiteral(), value); } // <%@ page trimDirectiveWhitespaces=“true” %> // 这条语句可以使jsp输出的html时去除多余的空行(jsp上使用EL和tag会产生大量的空格和空行)。 } else if ("trimDirectiveWhitespaces".equals(attr)) { if (pageInfo.getTrimDirectiveWhitespaces() == null) { pageInfo.setTrimDirectiveWhitespaces(value, n, err, true); } else if (!pageInfo.getTrimDirectiveWhitespaces().equals( value)) { err .jspError( n, "jsp.error.page.conflict.trimdirectivewhitespaces", pageInfo.getTrimDirectiveWhitespaces(), value); } } } // Check for bad combinations if (pageInfo.getBuffer() == 0 && !pageInfo.isAutoFlush()) err.jspError(n, "jsp.error.page.badCombo"); // Attributes for imports for this node have been processed by // the parsers, just add them to pageInfo. pageInfo.addImports(n.getImports()); }
从visit()方法得知,相当于将jsp开头设置的一些信息合并到pageInfo中。
public Node.Nodes parse(String inFileName) throws FileNotFoundException, JasperException, IOException { // If we're parsing a packaged tag file or a resource included by it // (using an include directive), ctxt.getTagFileJar() returns the // JAR file from which to read the tag file or included resource, // respectively. isTagFile = ctxt.isTagFile(); directiveOnly = false; return doParse(inFileName, null, ctxt.getTagFileJarResource()); }
大家看到没有,此时的parse方法和之前的parseDirectives()方法的唯一区别就是 directiveOnly = false; 这一行代码 。
/* * AllBody ::= ( '<%--' JSPCommentBody ) | ( '<%@' DirectiveBody ) | ( '<jsp:directive.' * XMLDirectiveBody ) | ( '<%!' DeclarationBody ) | ( '<jsp:declaration' * XMLDeclarationBody ) | ( '<%=' ExpressionBody ) | ( '<jsp:expression' * XMLExpressionBody ) | ( '${' ELExpressionBody ) | ( '<%' ScriptletBody ) | ( '<jsp:scriptlet' * XMLScriptletBody ) | ( '<jsp:text' XMLTemplateText ) | ( '<jsp:' * StandardAction ) | ( '<' CustomAction CustomActionBody ) | TemplateText */ private void parseElements(Node parent) throws JasperException { if (scriptlessCount > 0) { // vc: ScriptlessBody // We must follow the ScriptlessBody production if one of // our parents is ScriptlessBody. parseElementsScriptless(parent); return; } start = reader.mark(); // 如果是注释 if (reader.matches("<%--")) { parseComment(parent); // 如果是jsp指令 } else if (reader.matches("<%@")) { parseDirective(parent); } else if (reader.matches("<jsp:directive.")) { parseXMLDirective(parent); // <%!%>称作声明,其中写的内容将来会直接翻译在Servlet类中,因为我们可以在类中定义方法和属性以及全局变量, // 所以我们可以在<%!%>中声明方法、属性、全局变量。 } else if (reader.matches("<%!")) { parseDeclaration(parent); // 声明语句 ,等同于 <% %> } else if (reader.matches("<jsp:declaration")) { parseXMLDeclaration(parent); // <%=%>称作jsp表达式,用于将已经声明的变量或者表达式输出到网页上面。 } else if (reader.matches("<%=")) { parseExpression(parent); // 等同于<%= 表达式 %> } else if (reader.matches("<jsp:expression")) { parseXMLExpression(parent); // <% %>叫做脚本片段,其中写的内容会翻译在Servlet的Service方法中,显然我们可以在Service方法中定义局部变量或者调用其他方法,但是不能 // 在Service中再定义其他的方法,也就是我们可以在<%%>中定义局部变量或者调用方法,但不能定义方法。在jsp页面可以有多个脚本片段,但是多 // 个脚本片段之间要保证结构完整。 } else if (reader.matches("<%")) { parseScriptlet(parent); // JSP 脚本可以包含任意数量的 Java 语句,变量、方法和表达式。JSP 脚本会把包含的内容插入到 Servlet 的 service() 方法中。 } else if (reader.matches("<jsp:scriptlet")) { parseXMLScriptlet(parent); // 标签用于封装模板内容。嵌套在标签中的内容被作为模板内容输出给客户端。标签没有属性,不能在该标签中嵌套任何形式的子标签,但可以包含EL表达式(不能包含JSP表达式或Java代码)。 // 如果输出给客户端的内容包含HTML或XML格式的内容,可以将这些内容放在CDATA区中。下面是使用标签的一个例子: // <jsp:text> // <![CDATA[<mumble></foobar>]]> // ${param.value} // </jsp:text> // 在浏览器地址栏中输入如下的URL: // http://localhost:8080/demo/chapter6/text.jsp?value=xyz // 该页面生成的内容如下: // <mumble> // xyz } else if (reader.matches("<jsp:text")) { parseXMLTemplateText(parent); // EL表达式 } else if (!pageInfo.isELIgnored() && reader.matches("${")) { parseELExpression(parent, '$'); // isDeferredSyntaxAllowedAsLiteral该属性指示在JSP页面的模板文本中是否允许出现字符序列#{。如果该属性的值为false(默认值),当模板文本中出现字符序列#{时,将引发页面转换错误。 // 注意,该属性是在JSP 2.1规范中引入的,JSP 2.1规范对JSP 2.0和Java Server Faces 1.1中的表达式语言进行了统一。在JSP 2.1中,字符序列#{被保留给表达式语言使用,你不能在模板本中使用字符序列#{。如果JSP页面运行在JSP 2.1之前版本的容器中,则没有这个限制。对于JSP 2.1的容器,如果在模板文本中需要出现字符序列#{,那么可以将该属性设置为true。 } else if (!pageInfo.isELIgnored() && !pageInfo.isDeferredSyntaxAllowedAsLiteral() && reader.matches("#{")) { parseELExpression(parent, '#'); } else if (reader.matches("<jsp:")) { parseStandardAction(parent); // 解析自定义标签 如 , 在index.jsp中的 <ex:hello/> } else if (!parseCustomTag(parent)) { checkUnbalancedEndTag(); parseTemplateText(parent); } }
其他的jsp标签,我相信大家不陌生,对于 parseCustomTag还是不知道使用场景 。 请看下面使用场景
parseElements()方法中对jsp标签的每一种情况都做了处理,我们就选几个方法进行分析吧,其他方法实现类似,那先来看 parseDeclaration方法。
private void parseDeclaration(Node parent) throws JasperException { start = reader.mark(); Mark stop = reader.skipUntil("%>"); if (stop == null) { err.jspError(start, "jsp.error.unterminated", "<%!"); } new Node.Declaration(parseScriptText(reader.getText(start, stop)), start, parent); }
这个方法的原理也很简单,skipUntil方法,找到从当前位置开始,下一个出现%>的位置,getText()方法截取出start到stop之间的字符串。 再来看parseScriptText()方法 。
private String parseScriptText(String tx) { CharArrayWriter cw = new CharArrayWriter(); int size = tx.length(); int i = 0; while (i < size) { char ch = tx.charAt(i); if (i + 2 < size && ch == '%' && tx.charAt(i + 1) == '\\' && tx.charAt(i + 2) == '>') { cw.write('%'); cw.write('>'); i += 3; } else { cw.write(ch); ++i; } } cw.close(); return cw.toString(); }
parseScriptText() 方法原理也很简单,就是将转%> 转化为%>,如aaa%\> 转化为aaa%>,接下来,再来看parseCustomTag()方法解析。
private boolean parseCustomTag(Node parent) throws JasperException { if (reader.peekChar() != '<') { return false; } // Parse 'CustomAction' production (tag prefix and custom action name) reader.nextChar(); // skip '<' String tagName = reader.parseToken(false); int i = tagName.indexOf(':'); if (i == -1) { reader.reset(start); return false; } // 以<%@ taglib prefix="ex" uri="/WEB-INF/hello.tld" %> // <ex:hello/>为例 // ex String prefix = tagName.substring(0, i); // hello String shortTagName = tagName.substring(i + 1); // uri=/WEB-INF/hello.tld String uri = pageInfo.getURI(prefix); if (uri == null) { if (pageInfo.isErrorOnUndeclaredNamespace()) { err.jspError(start, "jsp.error.undeclared_namespace", prefix); } else { reader.reset(start); // Remember the prefix for later error checking pageInfo.putNonCustomTagPrefix(prefix, reader.mark()); return false; } } // 根据uri从pageInfo中找TagLibraryInfo TagLibraryInfo tagLibInfo = pageInfo.getTaglib(uri); TagInfo tagInfo = tagLibInfo.getTag(shortTagName); TagFileInfo tagFileInfo = tagLibInfo.getTagFile(shortTagName); if (tagInfo == null && tagFileInfo == null) { err.jspError(start, "jsp.error.bad_tag", shortTagName, prefix); } Class<?> tagHandlerClass = null; if (tagInfo != null) { // Must be a classic tag, load it here. // tag files will be loaded later, in TagFileProcessor String handlerClassName = tagInfo.getTagClassName(); try { tagHandlerClass = ctxt.getClassLoader().loadClass( handlerClassName); } catch (Exception e) { err.jspError(start, "jsp.error.loadclass.taghandler", handlerClassName, tagName); } } // Parse 'CustomActionBody' production: // At this point we are committed - if anything fails, we produce // a translation error. // Parse 'Attributes' production: Attributes attrs = parseAttributes(); reader.skipSpaces(); // Parse 'CustomActionEnd' production: if (reader.matches("/>")) { if (tagInfo != null) { new Node.CustomTag(tagName, prefix, shortTagName, uri, attrs, start, parent, tagInfo, tagHandlerClass); } else { new Node.CustomTag(tagName, prefix, shortTagName, uri, attrs, start, parent, tagFileInfo); } return true; } // Now we parse one of 'CustomActionTagDependent', // 'CustomActionJSPContent', or 'CustomActionScriptlessContent'. // depending on body-content in TLD. // Looking for a body, it still can be empty; but if there is a // a tag body, its syntax would be dependent on the type of // body content declared in the TLD. String bc; if (tagInfo != null) { bc = tagInfo.getBodyContent(); } else { bc = tagFileInfo.getTagInfo().getBodyContent(); } Node tagNode = null; if (tagInfo != null) { tagNode = new Node.CustomTag(tagName, prefix, shortTagName, uri, attrs, start, parent, tagInfo, tagHandlerClass); } else { tagNode = new Node.CustomTag(tagName, prefix, shortTagName, uri, attrs, start, parent, tagFileInfo); } parseOptionalBody(tagNode, tagName, bc); return true; }
上面代码getTaglib()方法中获取TagLibraryInfo,这个可能有点迷惑 。
public TagLibraryInfo getTaglib(String uri) { return taglibsMap.get(uri); }
taglibsMap对象的值又是何里添加的呢?
private void parseTaglibDirective(Node parent) throws JasperException { Attributes attrs = parseAttributes(); String uri = attrs.getValue("uri"); String prefix = attrs.getValue("prefix"); if (prefix != null) { Mark prevMark = pageInfo.getNonCustomTagPrefix(prefix); if (prevMark != null) { err.jspError(reader.mark(), "jsp.error.prefix.use_before_dcl", prefix, prevMark.getFile(), "" + prevMark.getLineNumber()); } if (uri != null) { String uriPrev = pageInfo.getURI(prefix); if (uriPrev != null && !uriPrev.equals(uri)) { err.jspError(reader.mark(), "jsp.error.prefix.refined", prefix, uri, uriPrev); } if (pageInfo.getTaglib(uri) == null) { TagLibraryInfoImpl impl = null; if (ctxt.getOptions().isCaching()) { impl = (TagLibraryInfoImpl) ctxt.getOptions() .getCache().get(uri); } if (impl == null) { TldLocation location = ctxt.getTldLocation(uri); impl = new TagLibraryInfoImpl(ctxt, parserController, pageInfo, prefix, uri, location, err, reader.mark()); if (ctxt.getOptions().isCaching()) { ctxt.getOptions().getCache().put(uri, impl); } } else { // Current compilation context needs location of cached // tag files for (TagFileInfo info : impl.getTagFiles()) { ctxt.setTagFileJarResource(info.getPath(), ctxt.getTagFileJarResource()); } } pageInfo.addTaglib(uri, impl); } pageInfo.addPrefixMapping(prefix, uri); } else { String tagdir = attrs.getValue("tagdir"); if (tagdir != null) { String urnTagdir = URN_JSPTAGDIR + tagdir; if (pageInfo.getTaglib(urnTagdir) == null) { pageInfo.addTaglib(urnTagdir, new ImplicitTagLibraryInfo(ctxt, parserController, pageInfo, prefix, tagdir, err)); } pageInfo.addPrefixMapping(prefix, urnTagdir); } } } new Node.TaglibDirective(attrs, start, parent); } public void addTaglib(String uri, TagLibraryInfo info) { taglibsMap.put(uri, info); }
从对<%@ taglib prefix=“ex” uri=“/WEB-INF/hello.tld” %>解析中得知,最终key为uri,value为TagLibraryInfoImpl对象添加到taglibsMap中。
最终index.jsp
<%-- Created by xxx --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.sql.*,javax.sql.*,javax.naming.*" %> <%@ page import="com.example.servelettest.Person" %> <%@ page import="java.util.ArrayList" %> <%@ taglib prefix="ex" uri="/WEB-INF/hello.tld" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%-- 我在测试 --%> <% Person person =new Person(); person.setName("帅哥"); person.setHeight(167); person.setAge(20); request.setAttribute("person", person); %> 名字 : ${person.name} <br> 人身高 : ${person.height} <ex:hello/>
被解析成Node对象,如下图所示
在generateJava()方法中,有这么两行代码 ,这两行代码主要用于处理哪种情况呢?
tfp = new TagFileProcessor(); tfp.loadTagFiles(this, pageNodes);
我们以show.jsp为例,看其用途。
- 创建show.jsp
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %> <%@ page import="java.util.*" %> <%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>迭代器tag file</title> <meta name="website" content="http://www.linjw.org" /> </head> <body> <h2>迭代器tag file</h2> <% // 创建集合对象,用于测试Tag File所定义的标签 List<String> a = new ArrayList<String>(); a.add("疯狂Java讲义"); a.add("轻量级Java EE企业应用实战"); a.add("疯狂Ajax讲义"); // 将集合对象放入页面范围 request.setAttribute("a" , a); %> <h3>使用自定义标签</h3> <tags:iterator bgColor="#99dd99" cellColor="#9999cc" title="迭代器标签" bean="a" /> </body> </html>
- 在/webapps/servelet-test-1.0/WEB-INF/tags目录下创建iterator.tag文件
<%@ tag pageEncoding="GBK" import="java.util.List" %> <!-- 定义了四个标签属性 --> <%@ attribute name="bgColor" %> <%@ attribute name="cellColor" %> <%@ attribute name="title" %> <%@ attribute name="bean" %> <table border="1" bgcolor="${bgColor}"> <tr> <td><b>${title}</b></td> </tr> <% List<String> list = (List<String>) request.getAttribute("a"); // 遍历输出list集合的元素 for (Object ele : list) { %> <tr> <td bgcolor="${cellColor}"> <%=ele%> </td> </tr> <%}%> </table>
看目录结构
结果输出
最终生成 show_jsp.java和iterator_tag.java文件
当然,iterator_tag.java文件的生成 和show.jsp文件生生成的原理类似。 这里就不再深入研究,但从show_jsp.java文件中可以看出。是show_jsp.java文件调用了org.apache.jsp.tag.web.iterator_tag的doTag()方法。
而org.apache.jsp.tag.web.iterator_tag.doTag()方法的内部实现如下图所示 。
当然,我们还是回归之前的index_jsp.java文件的生成上,如果弄懂了index_jsp.java文件的生成 _tag.java文件的生成也异曲同工。
既然已经解析好了,那下一步,来看java文件的生成 ,可能还有一些较验,或属性的准备没有分析,但后面源码分析过程中,用到时再逐一分析了,接下来,我们就对index.jsp如何一行一行的转化为index_jsp.java文件进行分析。
public static void generate(ServletWriter out, Compiler compiler, Node.Nodes page) throws JasperException { Generator gen = new Generator(out, compiler); if (gen.isPoolingEnabled) { gen.compileTagHandlerPoolList(page); } // java文件注释的生成 gen.generateCommentHeader(); // 将tag文件转化为java文件,感兴趣的小伙伴可以自行分析 if (gen.ctxt.isTagFile()) { JasperTagInfo tagInfo = (JasperTagInfo) gen.ctxt.getTagInfo(); gen.generateTagHandlerPreamble(tagInfo, page); if (gen.ctxt.isPrototypeMode()) { return; } gen.generateXmlProlog(page); gen.fragmentHelperClass.generatePreamble(); page.visit(gen.new GenerateVisitor(gen.ctxt.isTagFile(), out, gen.methodsBuffered, gen.fragmentHelperClass)); gen.generateTagHandlerPostamble(tagInfo); } else { gen.generatePreamble(page); gen.generateXmlProlog(page); gen.fragmentHelperClass.generatePreamble(); page.visit(gen.new GenerateVisitor(gen.ctxt.isTagFile(), out, gen.methodsBuffered, gen.fragmentHelperClass)); gen.generatePostamble(); } }
再次回顾一下 index.jsp文件
<%-- Created by xxx --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.sql.*,javax.sql.*,javax.naming.*" %> <%@ page import="com.example.servelettest.Person" %> <%@ page import="java.util.ArrayList" %> <%@ taglib prefix="ex" uri="/WEB-INF/hello.tld" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%-- 我在测试 --%> <% Person person =new Person(); person.setName("帅哥"); person.setHeight(167); person.setAge(20); request.setAttribute("person", person); %> 名字 : ${person.name} <br> 人身高 : ${person.height} <ex:hello/>
生成的index.jsp文件如下
接下来,就分析index_jsp.java文件是如何生成的,之前我们代码分析的顺序都是从上往下分析,但是这里使用结果索因法,通过index_jsp.java文件反推由tomcat哪些代码生成的。
- 注释的生成
/* * Generated by the Jasper component of Apache Tomcat * Version: Apache Tomcat/@VERSION@ * Generated at: 2022-09-20 15:57:04 UTC * Note: The last modified time of this file was set to * the last modified time of the source file after * generation to assist with modification tracking. */
对应的java代码如下 。
private void generateCommentHeader() { out.println("/*"); out.println(" * Generated by the Jasper component of Apache Tomcat"); out.println(" * Version: " + ctxt.getServletContext().getServerInfo()); out.println(" * Generated at: " + timestampFormat.format(new Date()) + " UTC"); out.println(" * Note: The last modified time of this file was set to"); out.println(" * the last modified time of the source file after"); out.println(" * generation to assist with modification tracking."); out.println(" */"); }
注释生成中,动态的添加了ServerInfo()和创建时间 。
再来看包名,导入包,类名, 继承,静态初始化块,jsp 表达式工厂, 实例管理器工厂 , 初始化方法,销毁方法, service方法的创建 。
private void generatePreamble(Node.Nodes page) throws JasperException { // 获取servlet包名 String servletPackageName = ctxt.getServletPackageName(); // class类名的生成就更加简单了, 如/my/test.jsp // 先截取出test.jsp ,再将test.jsp中的点替换成下划线,最终类名为test_jsp String servletClassName = ctxt.getServletClassName(); // public static final String SERVICE_METHOD_NAME = // System.getProperty("org.apache.jasper.Constants.SERVICE_METHOD_NAME", "_jspService"); // 先从环境中取,如果取不到,默认为_jspService String serviceMethodName = Constants.SERVICE_METHOD_NAME; // package 包名 genPreamblePackage(servletPackageName); // Generate imports genPreambleImports(); // Generate class declaration out.printin("public final class "); out.print(servletClassName); out.print(" extends "); out.println(pageInfo.getExtends()); out.printin(" implements org.apache.jasper.runtime.JspSourceDependent"); // 如果是线程安全的,则实现SingleThreadModel接口 // 对于每一个用户请求,那些Servlet都会用线程的方式给予应答。这样比较节省系统的资源。Sun公司也给出了另外一种方法,就是这节要介绍的SingleThreadModel的方法。当implement这个接口以后,你的Servlet就变成了另外一种模式工作。 // 即,每一个新用户的请求,都会生成一个新的Servlet实例来应答。这种方法有两个方面的弊病。一是性能太差,最后会把机器拖累死。 // 还有一条就是有时解决不了实际问题。每个servlet类实例都有自己独立的变量。如果我们的本意就是想让客户线程之间进行这些变量的交流。这种方法就无法做到。 if (!pageInfo.isThreadSafe()) { out.println(","); out.printin(" javax.servlet.SingleThreadModel"); } out.println(" {"); out.pushIndent(); // Class body begins here generateDeclarations(page); // Static initializations here genPreambleStaticInitializers(); // Class variable declarations genPreambleClassVariableDeclarations(); // Methods here genPreambleMethods(); // Now the service method out.printin("public void "); out.print(serviceMethodName); out.println("(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)"); out.println(" throws java.io.IOException, javax.servlet.ServletException {"); out.pushIndent(); out.println(); // Local variable declarations out.printil("final javax.servlet.jsp.PageContext pageContext;"); if (pageInfo.isSession()) out.printil("javax.servlet.http.HttpSession session = null;"); if (pageInfo.isErrorPage()) { out.printil("java.lang.Throwable exception = org.apache.jasper.runtime.JspRuntimeLibrary.getThrowable(request);"); out.printil("if (exception != null) {"); out.pushIndent(); out.printil("response.setStatus(javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR);"); out.popIndent(); out.printil("}"); } out.printil("final javax.servlet.ServletContext application;"); out.printil("final javax.servlet.ServletConfig config;"); out.printil("javax.servlet.jsp.JspWriter out = null;"); out.printil("final java.lang.Object page = this;"); out.printil("javax.servlet.jsp.JspWriter _jspx_out = null;"); out.printil("javax.servlet.jsp.PageContext _jspx_page_context = null;"); out.println(); declareTemporaryScriptingVars(page); out.println(); out.printil("try {"); out.pushIndent(); out.printin("response.setContentType("); out.print(quote(pageInfo.getContentType())); out.println(");"); if (ctxt.getOptions().isXpoweredBy()) { out.printil("response.addHeader(\"X-Powered-By\", \"JSP/2.2\");"); } out.printil("pageContext = _jspxFactory.getPageContext(this, request, response,"); out.printin("\t\t\t"); out.print(quote(pageInfo.getErrorPage())); out.print(", " + pageInfo.isSession()); out.print(", " + pageInfo.getBuffer()); out.print(", " + pageInfo.isAutoFlush()); out.println(");"); out.printil("_jspx_page_context = pageContext;"); out.printil("application = pageContext.getServletContext();"); out.printil("config = pageContext.getServletConfig();"); if (pageInfo.isSession()) out.printil("session = pageContext.getSession();"); out.printil("out = pageContext.getOut();"); out.printil("_jspx_out = out;"); out.println(); }
先来看包名的获取 。
public String getServletPackageName() { // tag文件先不分析 if (isTagFile()) { String className = tagInfo.getTagClassName(); int lastIndex = className.lastIndexOf('.'); String pkgName = ""; if (lastIndex != -1) { pkgName = className.substring(0, lastIndex); } return pkgName; } else { String dPackageName = getDerivedPackageName(); if (dPackageName.length() == 0) { return basePackageName; } return basePackageName + '.' + getDerivedPackageName(); } } protected String getDerivedPackageName() { if (derivedPackageName == null) { int iSep = jspUri.lastIndexOf('/'); derivedPackageName = (iSep > 0) ? JspUtil.makeJavaPackage(jspUri.substring(1,iSep)) : ""; } return derivedPackageName; }
先看加粗basePackageName的包名。 在JspCompilationContext构造函数中。
public JspCompilationContext(String jspUri, Options options, ServletContext context, JspServletWrapper jsw, JspRuntimeContext rctxt) { ... this.rctxt = rctxt; this.tagFileJarUrls = new HashMap<String, JarResource>(); this.basePackageName = Constants.JSP_PACKAGE_NAME; } public static final String JSP_PACKAGE_NAME = System.getProperty("org.apache.jasper.Constants.JSP_PACKAGE_NAME", "org.apache.jsp");
从JspCompilationContext的构造函数中可以看出 , basePackageName默认初始化为org.apache.jsp,再回头看getDerivedPackageName()方法的意图。
当jsp的路径为/my/test.jsp,它会截取出my作为包名的一部分,因此生成最终的test.jsp的包名为 org.apache.jsp + my = org.apache.jsp.my;
接下来看package包名生成的方法 。
private void genPreamblePackage(String packageName) { if (!"".equals(packageName) && packageName != null) { out.printil("package " + packageName + ";"); out.println(); } }
经过上面这个方法调用,生成了package org.apache.jsp;这一行代码。
接下来,import包代码生成方法 。
import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; import java.sql.*; import javax.sql.*; import javax.naming.*; import com.example.servelettest.Person; import java.util.ArrayList;
private void genPreambleImports() { Iterator<String> iter = pageInfo.getImports().iterator(); while (iter.hasNext()) { out.printin("import "); out.print(iter.next()); out.println(";"); } out.println(); }
在之前的parsePageDirective()方法中,将解析指令得到的包全部添加到pageInfo中,此时只需要取出,写到out中即可。
接下来看pushIndent()方法 ,不过在pushIndent()方法调用之前,下面这段代码已经生成了。
public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent {
public static final int TAB_WIDTH = 2; public static final String SPACES = " "; public void pushIndent() { virtual_indent += TAB_WIDTH; if (virtual_indent >= 0 && virtual_indent <= SPACES.length()) indent = virtual_indent; }
tomcat使用默认的TAB_WIDTH=2,我们自己开发,一般一个TAB键是4个空格,但tomcat默认为两个空格,在不影响代码阅读的同时节省更多的内存空间吧。 SPACES默认为30个空格 。 每次调用pushIndent() 。 indent = indent + 2 ,再来看printin(),printil()方法 。
而这样做有什么用呢?
public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent {
在输出上面代码后调用了pushIndent()方法,而在调用out.printil(“private static final javax.servlet.jsp.JspFactory _jspxFactory =”);之前,indent=2 。
也就是在源码中输出private static final javax.servlet.jsp.JspFactory _jspxFactory =之前需要先输出两个空格。
现在应该明白pushIndent()方法的用途了吧,同理当popIndent()方法时,你将不会再陌生,也就是输出代码时,比上一行少输出两个空格 。
不过在此之前,再来看println()方法中有一个javaLine++。
在这里维护了一个当前生成的java代码在文件中的第几行,这个有什么用呢?看下面代码
private void generateDeclarations(Node.Nodes page) throws JasperException { class DeclarationVisitor extends Node.Visitor { private boolean getServletInfoGenerated = false; .... @Override public void visit(Node.Declaration n) throws JasperException { n.setBeginJavaLine(out.getJavaLine()); out.printMultiLn(n.getText()); out.println(); n.setEndJavaLine(out.getJavaLine()); } ... } }
在生成java文件内容时,如果是解析到的元素,会设置其java代码开始行和结束行,而从之前的分析中得知,startMark表示当前节点对应jsp中的行。
冥冥之中,是不是发现startMark记录了jsp中的行, beginJavaLine和endJavaLine记录了生成_jsp.java文件中的行,这样做有什么用处呢? 不知道听过JSR-45没有,这里就要简单介绍一下JSR-45
JSR-45(Debugging Support for Other Languages)为那些非 JAVA 语言写成,却需要编译成 JAVA 代码,运行在 JVM 中的程序,提供了一个进行调试的标准机制。也许字面的意思有点不好理解,什么算是非 JAVA 语言呢?其实 JSP 就是一个再好不过的例子,JSR-45 的样例就是一个 JSP。
JSP的调试一直依赖于具体应用服务器的实现,没有一个统一的模式,JSR-45 针对这种情况,提供了一个标准的模式。我们知道,JAVA 的调试中,主要根据行号作为标志,进行定位。但是 JSP 被编译为 JAVA 代码之后,JAVA 行号与 JSP 行号无法一一对应,怎样解决呢?
JSR-45 是这样规定的:JSP 被编译成 JAVA 代码时,同时生成一份 JSP 文件名和行号与 JAVA 行号之间的对应表(SMAP)。JVM 在接受到调试客户端请求后,可以根据这个对应表(SMAP),从 JSP 的行号转换到 JAVA 代码的行号;JVM 发出事件通知前, 也根据对应表(SMAP)进行转化,直接将 JSP 的文件名和行号通知调试客户端。
当参数支持JSR-45则产生的smap就是jsp的行号与生产.java行号的对应关系。
最简单的用处当运行时jsp报错时,能准确的指向jsp哪一行代码,后面再来看如何使用,继续看代码 。
private void genPreambleStaticInitializers() { out.printil("private static final javax.servlet.jsp.JspFactory _jspxFactory ="); out.printil(" javax.servlet.jsp.JspFactory.getDefaultFactory();"); out.println(); // Static data for getDependants() out.printil("private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;"); out.println(); Map<String,Long> dependants = pageInfo.getDependants(); Iterator<Entry<String,Long>> iter = dependants.entrySet().iterator(); if (!dependants.isEmpty()) { out.printil("static {"); out.pushIndent(); out.printin("_jspx_dependants = new java.util.HashMap<java.lang.String,java.lang.Long>("); out.print("" + dependants.size()); out.println(");"); while (iter.hasNext()) { Entry<String,Long> entry = iter.next(); out.printin("_jspx_dependants.put(\""); out.print(entry.getKey()); out.print("\", Long.valueOf("); out.print(entry.getValue().toString()); out.println("L));"); } out.popIndent(); out.printil("}"); out.println(); } }
这段代码最终生成静态代码块。
private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants; static { _jspx_dependants = new java.util.HashMap<java.lang.String,java.lang.Long>(2); _jspx_dependants.put("/WEB-INF/c.tld", Long.valueOf(1618992210000L)); _jspx_dependants.put("/WEB-INF/hello.tld", Long.valueOf(1662049153000L)); }
但需要注意, pageInfo.getDependants()从何而来。
最终发现在解析parseTaglibDirective()方法时,会将/WEB-INF/c.tld和/WEB-INF/hello.tld添加到pageInfo的dependants中,因此就可以从dependants中获取到值添加到静态初始化代码中。
接着看下面代码的生成逻辑
private volatile javax.el.ExpressionFactory _el_expressionfactory; private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;
先看生成代码的方法genPreambleClassVariableDeclarations()
private static final String VAR_EXPRESSIONFACTORY = System.getProperty("org.apache.jasper.compiler.Generator.bVAR_EXPRESSIONFACTORY", "_el_expressionfactory"); private static final String VAR_INSTANCEMANAGER = System.getProperty("org.apache.jasper.compiler.Generator.VAR_INSTANCEMANAGER", "_jsp_instancemanager"); private void genPreambleClassVariableDeclarations() { if (isPoolingEnabled && !tagHandlerPoolNames.isEmpty()) { for (int i = 0; i < tagHandlerPoolNames.size(); i++) { out.printil("private org.apache.jasper.runtime.TagHandlerPool " + tagHandlerPoolNames.elementAt(i) + ";"); } out.println(); } out.printin("private volatile javax.el.ExpressionFactory "); out.print(VAR_EXPRESSIONFACTORY); out.println(";"); out.printin("private volatile org.apache.tomcat.InstanceManager "); out.print(VAR_INSTANCEMANAGER); out.println(";"); out.println(); }
上面的代码if (isPoolingEnabled && !tagHandlerPoolNames.isEmpty()) 这个判断是有点费解的,在哪种情况下会进入private org.apache.jasper.runtime.TagHandlerPool的声明呢?先来看一个例子。 我觉得网上 Java第四十八天,Jsp之Taglib,自定义标签详解 这篇博客写得非常好,我也是参考其中的例子,先来看例子。
- 先看目录结构
- 创建MyTaglib类文件
package com.luban; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspTagException; import javax.servlet.jsp.PageContext; import javax.servlet.jsp.tagext.Tag; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; public class MyTaglib implements Tag { private PageContext pageContext; private Tag parent; public MyTaglib() { super(); } @Override public void setPageContext(PageContext pageContext) { this.pageContext = pageContext; } @Override public void setParent(Tag tag) { this.parent = tag; } @Override public Tag getParent() { return this.parent; } @Override public int doStartTag() throws JspException { //返回 SKIP_BODY,表示不计算标签体 return SKIP_BODY; } @Override public int doEndTag() throws JspException { try { SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern("yyyy-MM-dd HH:mm:ss"); Date date = new Date();// 获取当前时间 pageContext.getOut().write(sdf.format(date)); } catch (IOException e) { throw new JspTagException(e.getMessage()); } return EVAL_PAGE; } @Override public void release() { } }
- 创建MyTaglib.tld
<?xml version="1.0" encoding="ISO-8859-1" ?> <taglib xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0"> <description>学习如何使用 Tag 接口</description> <tlib-version>1.0</tlib-version> <!--该属性是设置该标签库下所有标签的前缀--> <short-name>tools</short-name> <!--该属性是设置该标签库的唯一 url--> <uri>/learnTag</uri> <!--为标签库添加 标签--> <tag> <description>基于 Tag 接口的自定义标签</description> <!--为本标签设置所在标签库下唯一标签名--> <name>showTime</name> <!--指明标签处理类的位置--> <tag-class>com.luban.MyTaglib</tag-class> <!--因为没有标签体所以设为 empty--> <body-content>empty</body-content> </tag> </taglib>
- 创建tag.jsp
<%@ page contentType="text/html; charset=GBK" language="java" errorPage="" %> <%@ page import="java.util.*" %> <%@ taglib prefix="tools" uri="/learnTag" %> <html> <body> <!--采取 标签库名:标签名 格式使用指定标签库下的指定标签--> <tools:showTime/> </body> </html>
-
测试结果
-
看最后生成的tag_jsp.java文件
大家可能感兴趣的是我怎么知道这个例子会进入到if (isPoolingEnabled && !tagHandlerPoolNames.isEmpty()) 条件判断内部呢?
在代码中寻寻觅觅,发现tagHandlerPoolNames最终被TagHandlerPoolVisitor访问者中使用。进入其中。
只需要tag-class标签类不实现SimpleTag类即可,因此创建MyTaglib类即可,关于内部细节部分,由于篇幅原因,有兴趣的小伙伴自己去研究了哈。
接下来看_jsp_getExpressionFactory(),_jsp_getInstanceManager(), _jspInit(), _jspDestroy() 方法的相关生成代码 。
private static final String VAR_EXPRESSIONFACTORY = System.getProperty("org.apache.jasper.compiler.Generator.VAR_EXPRESSIONFACTORY", "_el_expressionfactory"); private static final String VAR_INSTANCEMANAGER = System.getProperty("org.apache.jasper.compiler.Generator.VAR_INSTANCEMANAGER", "_jsp_instancemanager"); private void genPreambleMethods() { // Method used to get compile time file dependencies out.printil("public java.util.Map<java.lang.String,java.lang.Long> getDependants() {"); out.pushIndent(); out.printil("return _jspx_dependants;"); out.popIndent(); out.printil("}"); out.println(); generateGetters(); generateInit(); generateDestroy(); } private void generateGetters() { out.printil("public javax.el.ExpressionFactory _jsp_getExpressionFactory() {"); out.pushIndent(); if (!ctxt.isTagFile()) { out.printin("if ("); out.print(VAR_EXPRESSIONFACTORY); out.println(" == null) {"); out.pushIndent(); out.printil("synchronized (this) {"); out.pushIndent(); out.printin("if ("); out.print(VAR_EXPRESSIONFACTORY); out.println(" == null) {"); out.pushIndent(); out.printin(VAR_EXPRESSIONFACTORY); out.println(" = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();"); out.popIndent(); out.printil("}"); out.popIndent(); out.printil("}"); out.popIndent(); out.printil("}"); } out.printin("return "); out.print(VAR_EXPRESSIONFACTORY); out.println(";"); out.popIndent(); out.printil("}"); out.println(); out.printil("public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {"); out.pushIndent(); if (!ctxt.isTagFile()) { out.printin("if ("); out.print(VAR_INSTANCEMANAGER); out.println(" == null) {"); out.pushIndent(); out.printil("synchronized (this) {"); out.pushIndent(); out.printin("if ("); out.print(VAR_INSTANCEMANAGER); out.println(" == null) {"); out.pushIndent(); out.printin(VAR_INSTANCEMANAGER); out.println(" = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());"); out.popIndent(); out.printil("}"); out.popIndent(); out.printil("}"); out.popIndent(); out.printil("}"); } out.printin("return "); out.print(VAR_INSTANCEMANAGER); out.println(";"); out.popIndent(); out.printil("}"); out.println(); } private void generateInit() { if (ctxt.isTagFile()) { out.printil("private void _jspInit(javax.servlet.ServletConfig config) {"); } else { out.printil("public void _jspInit() {"); } out.pushIndent(); if (isPoolingEnabled) { for (int i = 0; i < tagHandlerPoolNames.size(); i++) { out.printin(tagHandlerPoolNames.elementAt(i)); out.print(" = org.apache.jasper.runtime.TagHandlerPool.getTagHandlerPool("); if (ctxt.isTagFile()) { out.print("config"); } else { out.print("getServletConfig()"); } out.println(");"); } } // Tag files can't (easily) use lazy init for these so initialise them // here. if (ctxt.isTagFile()) { out.printin(VAR_EXPRESSIONFACTORY); out.println(" = _jspxFactory.getJspApplicationContext(config.getServletContext()).getExpressionFactory();"); out.printin(VAR_INSTANCEMANAGER); out.println(" = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(config);"); } out.popIndent(); out.printil("}"); out.println(); } private void generateDestroy() { out.printil("public void _jspDestroy() {"); out.pushIndent(); if (isPoolingEnabled) { for (int i = 0; i < tagHandlerPoolNames.size(); i++) { out.printin(tagHandlerPoolNames.elementAt(i)); out.println(".release();"); } } out.popIndent(); out.printil("}"); out.println(); }
上面代码纯粹的就是字符串拼接,因此最终生成如下java代码 。
private volatile javax.el.ExpressionFactory _el_expressionfactory; private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map<java.lang.String,java.lang.Long> getDependants() { return _jspx_dependants; } public javax.el.ExpressionFactory _jsp_getExpressionFactory() { if (_el_expressionfactory == null) { synchronized (this) { if (_el_expressionfactory == null) { _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory(); } } } return _el_expressionfactory; } public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() { if (_jsp_instancemanager == null) { synchronized (this) { if (_jsp_instancemanager == null) { _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig()); } } } return _jsp_instancemanager; } public void _jspInit() { } public void _jspDestroy() { }
继续接着分析 ,在generatePreamble()的后续代码中,双生成了如下java代码,也是字符串的拼接,这里就不再赘述 。
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out;
接下来,看generateXmlProlog()方法的实现。
private void generateXmlProlog(Node.Nodes page) { /* * An XML declaration is generated under the following conditions: - * 'omit-xml-declaration' attribute of <jsp:output> action is set to * "no" or "false" - JSP document without a <jsp:root> */ String omitXmlDecl = pageInfo.getOmitXmlDecl(); if ((omitXmlDecl != null && !JspUtil.booleanValue(omitXmlDecl)) || (omitXmlDecl == null && page.getRoot().isXmlSyntax() && !pageInfo.hasJspRoot() && !ctxt.isTagFile())) { String cType = pageInfo.getContentType(); String charSet = cType.substring(cType.indexOf("charset=") + 8); out.printil("out.write(\"<?xml version=\\\"1.0\\\" encoding=\\\"" + charSet + "\\\"?>\\n\");"); } /* * Output a DOCTYPE declaration if the doctype-root-element appears. If * doctype-public appears: <!DOCTYPE name PUBLIC "doctypePublic" * "doctypeSystem"> else <!DOCTYPE name SYSTEM "doctypeSystem" > */ String doctypeName = pageInfo.getDoctypeName(); if (doctypeName != null) { String doctypePublic = pageInfo.getDoctypePublic(); String doctypeSystem = pageInfo.getDoctypeSystem(); out.printin("out.write(\"<!DOCTYPE "); out.print(doctypeName); if (doctypePublic == null) { out.print(" SYSTEM \\\""); } else { out.print(" PUBLIC \\\""); out.print(doctypePublic); out.print("\\\" \\\""); } out.print(doctypeSystem); out.println("\\\">\\n\");"); } }
这个方法看上去也是字符串的拼接,但在哪种场景中会使用到上面代码呢?那再来看一个例子。
创建omit.jsp
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.0"> <jsp:output omit-xml-declaration="no" doctype-root-element="number" doctype-system="number.dtd"/> <number> <jsp:expression>2+3</jsp:expression> </number> </jsp:root>
测试generateXmlProlog()方法中打断点 。
结果输出
当然看一下最终生成的omit_jsp.java代码。
接下来,看在index_jsp.java中的9个 \n
pageNodes的结果如下。
从pageNodes中的数据来看, 在Scriptlet的Node前有9个\n,显然下面代码就是之前的9个\n生成的。
out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write('\n');
进入GenerateVisitor的visit(Node.TemplateText n) 方法 ,在其中打断点。
从图中来看,是不是有点诡异,之前分析过Node.TemplateText 最多只有两个\n ,但这里一次出现8个\n,这是怎么回事呢?再回到之前的generateJava方法,其中有一行TextOptimizer.concatenate(this, pageNodes);这样的代码,之前可能不起眼,但此时发现还是有用处的。
static class TextCatVisitor extends Node.Visitor { private static final String EMPTY_TEXT = ""; private Options options; private PageInfo pageInfo; private int textNodeCount = 0; private Node.TemplateText firstTextNode = null; private StringBuilder textBuffer; public TextCatVisitor(Compiler compiler) { options = compiler.getCompilationContext().getOptions(); pageInfo = compiler.getPageInfo(); } @Override public void doVisit(Node n) throws JasperException { collectText(); } /* * The following directives are ignored in text concatenation */ @Override public void visit(Node.PageDirective n) throws JasperException { } @Override public void visit(Node.TagDirective n) throws JasperException { } @Override public void visit(Node.TaglibDirective n) throws JasperException { } @Override public void visit(Node.AttributeDirective n) throws JasperException { } @Override public void visit(Node.VariableDirective n) throws JasperException { } /* * Don't concatenate text across body boundaries */ @Override public void visitBody(Node n) throws JasperException { super.visitBody(n); collectText(); } @Override public void visit(Node.TemplateText n) throws JasperException { // 如果jsp-config 中 配置了trimSpaces 或trimDirectiveWhitespaces 为true,则直接用空串代替 TemplateText的内容 if ((options.getTrimSpaces() || pageInfo.isTrimDirectiveWhitespaces()) && n.isAllSpace()) { n.setText(EMPTY_TEXT); return; } // 将所有的 text保存到textBuffer 中 if (textNodeCount++ == 0) { firstTextNode = n; textBuffer = new StringBuilder(n.getText()); } else { // Append text to text buffer textBuffer.append(n.getText()); n.setText(EMPTY_TEXT); } } /** * This method breaks concatenation mode. As a side effect it copies * the concatenated string to the first text node */ private void collectText() { // 将所有的textBuffer 中的内容合并到第一个TemplateText 中 if (textNodeCount > 1) { // Copy the text in buffer into the first template text node. firstTextNode.setText(textBuffer.toString()); } textNodeCount = 0; } } public static void concatenate(Compiler compiler, Node.Nodes page) throws JasperException { TextCatVisitor v = new TextCatVisitor(compiler); page.visit(v); // Cleanup, in case the page ends with a template text v.collectText(); }
从之前的断点来看 ,但Scriptlet 之前有8个Node.TemplateText,其中7个Node.TemplateText只有一个\n , 倒数第2个Node.TemplateText有两个\n。
但是发现一个特点,就是最后一个TemplateText之前是一个Comment。在之前的代码中分析得到,每次调用collectText()方法都会将之前textBuffer缓存中的text合并到第一个TemplateText中,因此在collectText()方法中打断点 。
每一次访问Comment节点时,TextCatVisitor都会调用collectText()方法将之前的TemplateText的text合并,这就是为什么在GenerateVisitor的visit(Node.TemplateText n)方法中第一个Node.TemplateText 会有8个\n了,因为前面7个Node.TemplateText中有6个拥有一个\n的text,最后一个Node.TemplateText拥有两个\n,合并起来就是8个\n,这样做的目的为了在out.println()代码时以及后续的Smap计算更好的提高效率。
从下图中可以看出,GenerateVisitor类提供了很多的visit()方法。
对于访问者模式,之前分析过,这里就不再深入分析,而只关注具体的visit()方法,之前也截图看过visit(Node.TemplateText n)方法,下面就来看visit(Node.TemplateText n)方法的具体实现。
public void visit(Node.TemplateText n) throws JasperException { String text = n.getText(); int textSize = text.length(); if (textSize == 0) { return; } if (textSize <= 3) { // Special case small text strings n.setBeginJavaLine(out.getJavaLine()); int lineInc = 0; for (int i = 0; i < textSize; i++) { char ch = text.charAt(i); out.printil("out.write(" + quote(ch) + ");"); if (i > 0) { n.addSmap(lineInc); } if (ch == '\n') { lineInc++; } } n.setEndJavaLine(out.getJavaLine()); return; } // 为了在一些情况下提高性能,是否应将文本字符串生成字符数组?默认为 false。 if (ctxt.getOptions().genStringAsCharArray()) { // Generate Strings as char arrays, for performance ServletWriter caOut; if (charArrayBuffer == null) { charArrayBuffer = new GenBuffer(); caOut = charArrayBuffer.getOut(); caOut.pushIndent(); textMap = new HashMap<String,String>(); } else { caOut = charArrayBuffer.getOut(); } // UTF-8 is up to 4 bytes per character // String constants are limited to 64k bytes // Limit string constants here to 16k characters int textIndex = 0; int textLength = text.length(); while (textIndex < textLength) { int len = 0; if (textLength - textIndex > 16384) { len = 16384; } else { len = textLength - textIndex; } String output = text.substring(textIndex, textIndex + len); String charArrayName = textMap.get(output); if (charArrayName == null) { charArrayName = "_jspx_char_array_" + charArrayCount++; textMap.put(output, charArrayName); caOut.printin("static char[] "); caOut.print(charArrayName); caOut.print(" = "); caOut.print(quote(output)); caOut.println(".toCharArray();"); } n.setBeginJavaLine(out.getJavaLine()); out.printil("out.write(" + charArrayName + ");"); n.setEndJavaLine(out.getJavaLine()); textIndex = textIndex + len; } return; } n.setBeginJavaLine(out.getJavaLine()); out.printin(); StringBuilder sb = new StringBuilder("out.write(\""); int initLength = sb.length(); int count = JspUtil.CHUNKSIZE; int srcLine = 0; // relative to starting source line for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); --count; switch (ch) { case '"': sb.append('\\').append('\"'); break; case '\\': sb.append('\\').append('\\'); break; case '\r': sb.append('\\').append('r'); break; case '\n': sb.append('\\').append('n'); srcLine++; if (breakAtLF || count < 0) { // Generate an out.write() when see a '\n' in template sb.append("\");"); out.println(sb.toString()); if (i < text.length() - 1) { out.printin(); } sb.setLength(initLength); count = JspUtil.CHUNKSIZE; } // add a Smap for this line n.addSmap(srcLine); break; case '\t': // Not sure we need this sb.append('\\').append('t'); break; default: sb.append(ch); } } if (sb.length() > initLength) { sb.append("\");"); out.println(sb.toString()); } n.setEndJavaLine(out.getJavaLine()); } public void addSmap(int srcLine) { if (extraSmap == null) { extraSmap = new ArrayList<Integer>(); } extraSmap.add(Integer.valueOf(srcLine)); }
上面方法分为三种情况,当textSize<=3 时, 以一种简单的方式直接out.write(" + quote(ch) + “); 输出, 当textSize > 3 时,又分为两种情况, 如果配置了genStringAsCharArray为true,则以数组的形式一次性输出,如果没有配置,虽然也是一行一行的输出out.write(”\n"); 当然,区别在于addSmap()方法上,这样方便后面建立java行号与jsp中行号映射关系。仔细阅读源码,第一种情况和第三种情况还是好理解的。 现在来分析第二种情况 。
其他的代码都不需要动,只需要在web.xml中添加
<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> ... <init-param> <param-name>genStringAsCharArray</param-name> <param-value>true</param-value> </init-param> ... </servlet>
初始化参数genStringAsCharArray为true即可。
最后结果输出的index_jsp.java的区别如下。
下图中看区别。
从新旧的index_jsp.java文件生成对比得知,原来的
out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write('\n');
被替换成。
static char[] _jspx_char_array_0 = "\n\n\n\n\n\n\n\n".toCharArray(); out.write(_jspx_char_array_0);
我们知道<% %> 最终被解析为Scriptlet节点,进入Scriptlet节点的代码visit()
public void visit(Node.Scriptlet n) throws JasperException { n.setBeginJavaLine(out.getJavaLine()); out.printMultiLn(n.getText()); out.println(); n.setEndJavaLine(out.getJavaLine()); } public void printMultiLn(String s) { int index = 0; // look for hidden newlines inside strings while ((index=s.indexOf('\n',index)) > -1 ) { javaLine++; index++; } writer.print(s); }
visit(Node.Scriptlet n)方法的原理很简单,最终生成了如下代码。
Person person =new Person(); person.setName("帅哥"); person.setHeight(167); person.setAge(20); request.setAttribute("person", person);
对于TemplateText 的text为\n名字的解析就很简单了。
毫无悬念的生成如下代码 。
out.write("\n"); out.write("名字 : ");
接着看visit(Node.ELExpression n)方法 。
public void visit(Node.ELExpression n) throws JasperException { n.setBeginJavaLine(out.getJavaLine()); // 如果不忽略EL 表达式 if (!pageInfo.isELIgnored() && (n.getEL() != null)) { out.printil("out.write(" + elInterpreter.interpreterCall(ctxt, this.isTagFile, n.getType() + "{" + n.getText() + "}", String.class, n.getEL().getMapName(), false) + ");"); } else { // 如果忽略EL表达式 out.printil("out.write(" + quote(n.getType() + "{" + n.getText() + "}") + ");"); } n.setEndJavaLine(out.getJavaLine()); } public static class DefaultELInterpreter implements ELInterpreter { @Override public String interpreterCall(JspCompilationContext context, boolean isTagFile, String expression, Class<?> expectedType, String fnmapvar, boolean xmlEscape) { return JspUtil.interpreterCall(isTagFile, expression, expectedType, fnmapvar, xmlEscape); } }
上面方法分两种情况,忽略EL表达式,则直接输出文本即可,如果不忽略EL表达式,则调用DefaultELInterpreter的interpreterCall方法,接下来,我们进入interpreterCall()方法 。
public static String interpreterCall(boolean isTagFile, String expression, Class<?> expectedType, String fnmapvar, boolean XmlEscape) { /* * Determine which context object to use. */ String jspCtxt = null; if (isTagFile) { jspCtxt = "this.getJspContext()"; } else { jspCtxt = "_jspx_page_context"; } /* * Determine whether to use the expected type's textual name or, if it's * a primitive, the name of its correspondent boxed type. */ String targetType = expectedType.getCanonicalName(); String primitiveConverterMethod = null; if (expectedType.isPrimitive()) { if (expectedType.equals(Boolean.TYPE)) { targetType = Boolean.class.getName(); primitiveConverterMethod = "booleanValue"; } else if (expectedType.equals(Byte.TYPE)) { targetType = Byte.class.getName(); primitiveConverterMethod = "byteValue"; } else if (expectedType.equals(Character.TYPE)) { targetType = Character.class.getName(); primitiveConverterMethod = "charValue"; } else if (expectedType.equals(Short.TYPE)) { targetType = Short.class.getName(); primitiveConverterMethod = "shortValue"; } else if (expectedType.equals(Integer.TYPE)) { targetType = Integer.class.getName(); primitiveConverterMethod = "intValue"; } else if (expectedType.equals(Long.TYPE)) { targetType = Long.class.getName(); primitiveConverterMethod = "longValue"; } else if (expectedType.equals(Float.TYPE)) { targetType = Float.class.getName(); primitiveConverterMethod = "floatValue"; } else if (expectedType.equals(Double.TYPE)) { targetType = Double.class.getName(); primitiveConverterMethod = "doubleValue"; } } if (primitiveConverterMethod != null) { XmlEscape = false; } /* * Build up the base call to the interpreter. */ // XXX - We use a proprietary call to the interpreter for now // as the current standard machinery is inefficient and requires // lots of wrappers and adapters. This should all clear up once // the EL interpreter moves out of JSTL and into its own project. // In the future, this should be replaced by code that calls // ExpressionEvaluator.parseExpression() and then cache the resulting // expression objects. The interpreterCall would simply select // one of the pre-cached expressions and evaluate it. // Note that PageContextImpl implements VariableResolver and // the generated Servlet/SimpleTag implements FunctionMapper, so // that machinery is already in place (mroth). targetType = toJavaSourceType(targetType); StringBuilder call = new StringBuilder( "(" + targetType + ") " + "org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate" + "(" + Generator.quote(expression) + ", " + targetType + ".class, " + "(javax.servlet.jsp.PageContext)" + jspCtxt + ", " + fnmapvar + ", " + XmlEscape + ")"); /* * Add the primitive converter method if we need to. */ if (primitiveConverterMethod != null) { call.insert(0, "("); call.append(")." + primitiveConverterMethod + "()"); } return call.toString(); }
EL表达式的代码组装如上图所示 ,其实原理也很简单,也是字符串的拼接而已,最终生成如下代码 。
out.write("\n"); out.write("名字 : "); out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${person.name}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null, false)); out.write(" <br>\n"); out.write("人身高 : "); out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${person.height}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null, false));
接着继续分析TemplateText还是原来的套路,生成5个out.write(“\n”);
因此生成如下代码 。
out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n"); out.write("\n");
继续看visit(Node.CustomTag n)方法的解析
public void visit(Node.CustomTag n) throws JasperException { // Use plugin to generate more efficient code if there is one. if (n.useTagPlugin()) { generateTagPlugin(n); return; } TagHandlerInfo handlerInfo = getTagHandlerInfo(n); // Create variable names String baseVar = createTagVarName(n.getQName(), n.getPrefix(), n .getLocalName()); String tagEvalVar = "_jspx_eval_" + baseVar; String tagHandlerVar = "_jspx_th_" + baseVar; String tagPushBodyCountVar = "_jspx_push_body_count_" + baseVar; // If the tag contains no scripting element, generate its codes // to a method. ServletWriter outSave = null; Node.ChildInfo ci = n.getChildInfo(); if (ci.isScriptless() && !ci.hasScriptingVars()) { // The tag handler and its body code can reside in a separate // method if it is scriptless and does not have any scripting // variable defined. String tagMethod = "_jspx_meth_" + baseVar; // Generate a call to this method out.printin("if ("); out.print(tagMethod); out.print("("); if (parent != null) { out.print(parent); out.print(", "); } out.print("_jspx_page_context"); if (pushBodyCountVar != null) { out.print(", "); out.print(pushBodyCountVar); } out.println("))"); out.pushIndent(); out.printil((methodNesting > 0) ? "return true;" : "return;"); out.popIndent(); // Set up new buffer for the method outSave = out; /* * For fragments, their bodies will be generated in fragment * helper classes, and the Java line adjustments will be done * there, hence they are set to null here to avoid double * adjustments. */ GenBuffer genBuffer = new GenBuffer(n, n.implementsSimpleTag() ? null : n.getBody()); methodsBuffered.add(genBuffer); out = genBuffer.getOut(); methodNesting++; // Generate code for method declaration out.println(); out.pushIndent(); out.printin("private boolean "); out.print(tagMethod); out.print("("); if (parent != null) { out.print("javax.servlet.jsp.tagext.JspTag "); out.print(parent); out.print(", "); } out.print("javax.servlet.jsp.PageContext _jspx_page_context"); if (pushBodyCountVar != null) { out.print(", int[] "); out.print(pushBodyCountVar); } out.println(")"); out.printil(" throws java.lang.Throwable {"); out.pushIndent(); // Initialize local variables used in this method. if (!isTagFile) { out.printil("javax.servlet.jsp.PageContext pageContext = _jspx_page_context;"); } out.printil("javax.servlet.jsp.JspWriter out = _jspx_page_context.getOut();"); generateLocalVariables(out, n); } ... if (n.implementsSimpleTag()) { generateCustomDoTag(n, handlerInfo, tagHandlerVar); } else { ... } if (ci.isScriptless() && !ci.hasScriptingVars()) { // Generate end of method if (methodNesting > 0) { out.printil("return false;"); } out.popIndent(); out.printil("}"); out.popIndent(); methodNesting--; // restore previous writer out = outSave; } }
最终生成如下代码,原理也很简单,将解析出来的信息通过字符串拼接即可。
重点关注_jspx_meth_ex_005fhello_005f0(),这个方法的调用主要是为了输出<ex:hello/>内容。
private boolean _jspx_meth_ex_005fhello_005f0(javax.servlet.jsp.PageContext _jspx_page_context) throws java.lang.Throwable { javax.servlet.jsp.PageContext pageContext = _jspx_page_context; javax.servlet.jsp.JspWriter out = _jspx_page_context.getOut(); // ex:hello com.luban.HelloWorldTag _jspx_th_ex_005fhello_005f0 = new com.luban.HelloWorldTag(); _jsp_getInstanceManager().newInstance(_jspx_th_ex_005fhello_005f0); try { _jspx_th_ex_005fhello_005f0.setJspContext(_jspx_page_context); _jspx_th_ex_005fhello_005f0.doTag(); } finally { _jsp_getInstanceManager().destroyInstance(_jspx_th_ex_005fhello_005f0); } return false; }
_jspx_meth_ex_005fhello_005f0()方法的原理很简单,先实例化HelloWorldTag对象,再设置HelloWorldTag的setJspContext(_jspx_page_context);,最后调用HelloWorldTag对象的doTag()方法 。
这也是最终界面输出Hello Custom Tag!,如下图所示 。
private void generatePostamble() { out.popIndent(); out.printil("} catch (java.lang.Throwable t) {"); out.pushIndent(); out.printil("if (!(t instanceof javax.servlet.jsp.SkipPageException)){"); out.pushIndent(); out.printil("out = _jspx_out;"); out.printil("if (out != null && out.getBufferSize() != 0)"); out.pushIndent(); out.printil("try {"); out.pushIndent(); out.printil("if (response.isCommitted()) {"); out.pushIndent(); out.printil("out.flush();"); out.popIndent(); out.printil("} else {"); out.pushIndent(); out.printil("out.clearBuffer();"); out.popIndent(); out.printil("}"); out.popIndent(); out.printil("} catch (java.io.IOException e) {}"); out.popIndent(); out.printil("if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);"); out.printil("else throw new ServletException(t);"); out.popIndent(); out.printil("}"); out.popIndent(); out.printil("} finally {"); out.pushIndent(); out.printil("_jspxFactory.releasePageContext(_jspx_page_context);"); out.popIndent(); out.printil("}"); // Close the service method out.popIndent(); out.printil("}"); // Generated methods, helper classes, etc. genCommonPostamble(); }
上面这段代码也是就最后catch()代码块的生成 。
其实之前在解析visit(Node.CustomTag n) 方法时。发现下面代码。
并不是在visit(Node.CustomTag n)方法中生成,而是在genCommonPostamble()方法中生成 。
private void genCommonPostamble() { // Append any methods that were generated in the buffer. for (int i = 0; i < methodsBuffered.size(); i++) { GenBuffer methodBuffer = methodsBuffered.get(i); methodBuffer.adjustJavaLines(out.getJavaLine() - 1); out.printMultiLn(methodBuffer.toString()); } // Append the helper class if (fragmentHelperClass.isUsed()) { fragmentHelperClass.generatePostamble(); fragmentHelperClass.adjustJavaLines(out.getJavaLine() - 1); out.printMultiLn(fragmentHelperClass.toString()); } // Append char array declarations if (charArrayBuffer != null) { out.printMultiLn(charArrayBuffer.toString()); } // Close the class definition out.popIndent(); out.printil("}"); }
代码的执行效果如下图所示 。
分析到这里,整个index_jsp.java文件的生成已经解析完成 。
16.2 从Servlet到Class字节码
那么现在已经有了Servlet对应的Java源码了, 接下来就是下一阶段, 把Java编译成Class字节码 。
16.2.1 JSR45标准
我们知道Java虚拟机只认识Class文件,要在虚拟机上运行,就必须要遵守Class文件的格式 ,所以把JSP编译成Servelt还需要进一步编译成Class 文件,但从JSP文件到Java 文件再到Class 文件的过程需要考虑的事情比较多, 其中一个比较重要的问题就是调试问题, 由于语法不一样,JSP 中的代码在执行时需要通过某种机制与Java 文件对应起来 , 这样在JVM 执行过程中发生异常或错误才能找到JSP 对应的行,提供了一个友好的调试信息,类似的, JSP 文件名编译后的Java 文件同样也要有映射关系 。
为了解决从非Java语言到Java 语言调试时文件名及行号映射问题, Java Community Process 组织提出了JSR-45(Debugging Suppert for Other Languages) 规范, 它为非Java 语言提供了一个进行调试的标准机制,这里的JSP 其实就是属于非Java 语言, JSP 如果想要方便开发者开发 , 它就必须遵循JSR-45 规范,其实,简单的来说,就是为了解决JSP 编译后的Java文件与JSP文件的对应关系,而且提供了一个统一的标准,从而避免不同的厂商有不同的实现方式 。
JSR-45 规范的核心对象就是资源映射表(Source Map)简称SMAP,在这里它指的是JSP 文件名及行号的映射表, 把这个映射表存在Class 文件中, 在基于JPDA 的调试工具中就可以通过此映射表获取到对应的JSP 文件及行号, 向开发者提示对应的JSP文件的信息。
以前面的index.jsp为例,看看SMAP 映射表是如何映射的, index.jsp 文件经过编译后变成了index_jsp.java ,根据JSR-45的规范,最后我们会生成一份如下的映射表, 这里不打算研究SMAP 的整个语法 , 只专注于映射相关的部分,从 * L 到 * E 之间的内容,其实1,10:62 表示index.jsp 文件与 index_jsp.java的映射关系为1-62,2-63,3-64 ,… 10-71 , 同样的,10,3:72 表示对应的关系为10-72,11-73,12-74 ,有了这些映射表。
就可以文件地将Java 执行的代码行号与JSP的行号对应起来了。
SMAP
HelloWorld_jsp.java
JSP
* S JSP
* F
+ 0 HelloWorld.jsp
HelloWorld.jsp
* L
1,10:62
10,3:72
* E
讨论完SMAP ,我们已经知道了生成SMAP 的格式,那么如何保存它呢? 保存到哪里呢? 因为JVM 只会通过Class 文件加载相关的信息, 所以唯一的办法就是通过Class文件附带SMAP 消息,Class 文件格式中可以附带的信息就只有属于集合,Class 文件格式的其他数据项都有严格的长度,顺序和格式,而属于的列表集合则没有严格的要求,只要属性名不与已有的属性冲突即可, 任何人都可以向Class 文件的属性列表中写入自己的属性,虚拟机会自动忽略不认识的属性, 我们需要在支持调试信息的JVM中附带些属性,这里的属性名称就是SourceDebugExtension 属性,这个属性的结构如下 , 前面两个字节表示名称的索引值,接下来4个字节表示属性的长度,最后一个数组的属性值,按照格式写入Class文件JVM 即可识别。
SourceDebugExtension_attribute{
u2 attribute_name_index;
u4 attribute_length;
debug_extension[attribute_length];
}
通过JSR 45 标准解决了JSP到Java之间的映射关系问题, 从而让调试更加方便,在Java 领域中,为了达到统一而又不失灵活,基本上由JavaCommunity Process规范然后由厂商按照规范进行实现。
接下来分析 JSR-45,JSR-45 是这样规定的:JSP 被编译成 JAVA 代码时,同时生成一份 JSP 文件名和行号与 JAVA 行号之间的对应表(SMAP)。JVM 在接受到调试客户端请求后,可以根据这个对应表(SMAP),从 JSP 的行号转换到 JAVA 代码的行号;JVM 发出事件通知前, 也根据对应表(SMAP)进行转化,直接将 JSP 的文件名和行号通知调试客户端。
接下来分析SMAP 。
public static String[] generateSmap( JspCompilationContext ctxt, Node.Nodes pageNodes) throws IOException { // Scan the nodes for presence of Jasper generated inner classes PreScanVisitor psVisitor = new PreScanVisitor(); try { pageNodes.visit(psVisitor); } catch (JasperException ex) { } HashMap<String, SmapStratum> map = psVisitor.getMap(); // set up our SMAP generator SmapGenerator g = new SmapGenerator(); /** Disable reading of input SMAP because: 1. There is a bug here: getRealPath() is null if .jsp is in a jar Bugzilla 14660. 2. Mappings from other sources into .jsp files are not supported. TODO: fix 1. if 2. is not true. // determine if we have an input SMAP String smapPath = inputSmapPath(ctxt.getRealPath(ctxt.getJspFile())); File inputSmap = new File(smapPath); if (inputSmap.exists()) { byte[] embeddedSmap = null; byte[] subSmap = SDEInstaller.readWhole(inputSmap); String subSmapString = new String(subSmap, SMAP_ENCODING); g.addSmap(subSmapString, "JSP"); } **/ // now, assemble info about our own stratum (JSP) using JspLineMap SmapStratum s = new SmapStratum("JSP"); g.setOutputFileName(unqualify(ctxt.getServletJavaFileName())); // Map out Node.Nodes evaluateNodes(pageNodes, s, map, ctxt.getOptions().getMappedFile()); s.optimizeLineSection(); g.addStratum(s, true); // 是否输出 index_jsp.class.smap文件 if (ctxt.getOptions().isSmapDumped()) { File outSmap = new File(ctxt.getClassFileName() + ".smap"); PrintWriter so = new PrintWriter( new OutputStreamWriter( new FileOutputStream(outSmap), SMAP_ENCODING)); so.print(g.getString()); so.close(); } String classFileName = ctxt.getClassFileName(); int innerClassCount = map.size(); String [] smapInfo = new String[2 + innerClassCount*2]; smapInfo[0] = classFileName; smapInfo[1] = g.getString(); int count = 2; Iterator<Map.Entry<String,SmapStratum>> iter = map.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<String,SmapStratum> entry = iter.next(); String innerClass = entry.getKey(); s = entry.getValue(); s.optimizeLineSection(); g = new SmapGenerator(); g.setOutputFileName(unqualify(ctxt.getServletJavaFileName())); g.addStratum(s, true); String innerClassFileName = classFileName.substring(0, classFileName.indexOf(".class")) + '$' + innerClass + ".class"; if (ctxt.getOptions().isSmapDumped()) { File outSmap = new File(innerClassFileName + ".smap"); PrintWriter so = new PrintWriter( new OutputStreamWriter( new FileOutputStream(outSmap), SMAP_ENCODING)); so.print(g.getString()); so.close(); } smapInfo[count] = innerClassFileName; smapInfo[count+1] = g.getString(); count += 2; } return smapInfo; }
这个方法,主要分为两步,第一步建立起jsp文件与生成的java文件的行号对应关系,第二步以极简的方式合并这些对应关系,从而来节省存储空间。 先来看第一步,创建jsp文件与生成java文件之间的对应关系,进入evaluateNodes()方法 。
public static void evaluateNodes( Node.Nodes nodes, SmapStratum s, HashMap<String, SmapStratum> innerClassMap, boolean breakAtLF) { try { nodes.visit(new SmapGenVisitor(s, breakAtLF, innerClassMap)); } catch (JasperException ex) { } }
又到我们熟悉的访问者模式,进入SmapGenVisitor看看其重载的visit()方法 。
先进入我们最常用的节点TemplateText的访问方法 。
public void visit(Node.TemplateText n) throws JasperException { Mark mark = n.getStart(); if (mark == null) { return; } //Add the file information String fileName = mark.getFile(); smap.addFile(unqualify(fileName), fileName); // Add a LineInfo that corresponds to the beginning of this node // mark中记录了jsp中的行与列 int iInputStartLine = mark.getLineNumber(); // 创建java文件中的行 int iOutputStartLine = n.getBeginJavaLine(); //breakAtLF = ctxt.getOptions().getMappedFile() // 而mappedFile 是否对每个输入行都用一条 print 语句来生成静态内容,以方便调试。true 或 false,缺省为 true。 // 默认情况为true ,也就是代表java代码一行一行输出 int iOutputLineIncrement = breakAtLF? 1: 0; smap.addLineData(iInputStartLine, fileName, 1, iOutputStartLine, iOutputLineIncrement); // Output additional mappings in the text java.util.ArrayList<Integer> extraSmap = n.getExtraSmap(); if (extraSmap != null) { for (int i = 0; i < extraSmap.size(); i++) { iOutputStartLine += iOutputLineIncrement; smap.addLineData( iInputStartLine+extraSmap.get(i).intValue(), fileName, 1, iOutputStartLine, iOutputLineIncrement); } } } public void addLineData( int inputStartLine, String inputFileName, int inputLineCount, int outputStartLine, int outputLineIncrement) { // check the input - what are you doing here?? int fileIndex = filePathList.indexOf(inputFileName); if (fileIndex == -1) // still throw new IllegalArgumentException( "inputFileName: " + inputFileName); //Jasper incorrectly SMAPs certain Nodes, giving them an //outputStartLine of 0. This can cause a fatal error in //optimizeLineSection, making it impossible for Jasper to //compile the JSP. Until we can fix the underlying //SMAPping problem, we simply ignore the flawed SMAP entries. if (outputStartLine == 0) return; // build the LineInfo LineInfo li = new LineInfo(); li.setInputStartLine(inputStartLine); li.setInputLineCount(inputLineCount); li.setOutputStartLine(outputStartLine); li.setOutputLineIncrement(outputLineIncrement); if (fileIndex != lastFileID) li.setLineFileID(fileIndex); lastFileID = fileIndex; // save it lineData.add(li); }
在这里主要弄清楚 LineInfo的几个变量 。
public static class LineInfo { // jsp对应的行 private int inputStartLine = -1; // 生成java文件对应的行 private int outputStartLine = -1; private int lineFileID = 0; // jsp中行增幅 private int inputLineCount = 1; // java 中行增幅 ,默认都是1 private int outputLineIncrement = 1; private boolean lineFileIDSet = false; }
最终jsp和java行号之间的对应关系构建成LineInfo添加到lineData中。其中需要注意的一行代码是java.util.ArrayList extraSmap = n.getExtraSmap();不知道大家还记得之前分析过的Node.TemplateText 的8个\n合并的例子没有,再回头看之前的代码 。
再在public void visit(Node.TemplateText n)方法中打一个断点看看。
是不是之前的8个\n对应的索引被加到了extrapSmap中去,而在第一次访问public void visit(Node.TemplateText n)时,即将所有的jsp行号与生成java文件之间的行号对应关系添加到smap的lineData对象中。而大家可能感到好奇,在第一个Node.TemplateText合并\n加入到lineData后,后面的Node.TemplateText难道就不进入 public void visit(Node.TemplateText n) throws JasperException 方法了不? 答案是还会进入,那什么原因导致被合并后的Node.TemplateText不再被加入到lineData中了呢?
先来看TextCatVisitor的public void visit(Node.TemplateText n) throws JasperException方法 。
再来看GenerateVisitor的public void visit(Node.TemplateText n) throws JasperException方法 。
现在终于明白TemplateText被合并后,不会被重复添加到lineData中了吧。
接下来看public void visit(Node.Scriptlet n) throws JasperException方法 。
public void visit(Node.Scriptlet n) throws JasperException { doSmapText(n); } private void doSmapText(Node n) { String text = n.getText(); int index = 0; int next = 0; int lineCount = 1; int skippedLines = 0; boolean slashStarSeen = false; boolean beginning = true; // Count lines inside text, but skipping comment lines at the // beginning of the text. while ((next = text.indexOf('\n', index)) > -1) { if (beginning) { String line = text.substring(index, next).trim(); if (!slashStarSeen && line.startsWith("/*")) { slashStarSeen = true; } if (slashStarSeen) { skippedLines++; int endIndex = line.indexOf("*/"); if (endIndex >= 0) { // End of /* */ comment slashStarSeen = false; if (endIndex < line.length() - 2) { // Some executable code after comment skippedLines--; beginning = false; } } // 如果java脚本的开头是\n或是注释时,则忽略的行 + 1 } else if (line.length() == 0 || line.startsWith("//")) { skippedLines++; } else { beginning = false; } } lineCount++; index = next + 1; } doSmap(n, lineCount, 1, skippedLines); }
其实对于上面代码的理解,只需要弄明白skippedLines和lineCount这两个变量的含义,其他的就很好理解,因为在jsp用<% %>写java脚本时,可以在内部写java注释,而java注释有两种写法, 以/* */ 或 // 两种方式写java注释,因此在生成jsp 行号与java行号之间的对应关系时,需要跳过这些行,还有另外一种情况,当java脚本是“\n” +
" Person person =new Person();\n" +
" person.setName(“帅哥”);\n" +
" person.setHeight(167);\n" +
" person.setAge(20);\n" +
" request.setAttribute(“person”, person);\n";
时,第一个\n也将导致skippedLines + 1 。 而lineCount变量则是每一行java代码,注释,第一个\n都会导致 lineCount+1,有了这些基础,再来理解doSmap()方法,将会很好理解 。
private void doSmap( Node n, int inLineCount, int outIncrement, int skippedLines) { Mark mark = n.getStart(); if (mark == null) { return; } String unqualifiedName = unqualify(mark.getFile()); smap.addFile(unqualifiedName, mark.getFile()); smap.addLineData( mark.getLineNumber() + skippedLines, mark.getFile(), inLineCount - skippedLines, n.getBeginJavaLine() + skippedLines, outIncrement); }
因为要忽略掉注释 + 第一个出现的\n ,因此jsp的行号从 mark.getLineNumber() + skippedLines开始,而java的行号从n.getBeginJavaLine() + skippedLines开始,inputLineCount = inLineCount - skippedLines,画图来理解,可能更加方便,看下图。
上面这个图的意图也很简单, 只是想说明public void visit(Node.Scriptlet n) throws JasperException 这个方法生成在jsp代码中的java脚本
<% Person person =new Person(); person.setName("帅哥"); person.setHeight(167); person.setAge(20); request.setAttribute("person", person); %>
转化为java代码之间的对应关系为11,10:103,对应的含义为11-103,12-104,13-105,14-106,15-107,16-108,17-109,18-110,19-111,20-112 ,也就是jsp中的11行对应生成java代码中的103行,以此类推。
接下来,继续看jsp中EL表达式的行号与java行号之间的对应转化关系 。
public void visit(Node.ELExpression n) throws JasperException { doSmap(n); } private void doSmap(Node n) { doSmap(n, 1, n.getEndJavaLine() - n.getBeginJavaLine(), 0); }
当然呢?public void visit(Node.ELExpression n) 方法的访问没有弯弯绕绕,来得直接 。 接下来看public void visit(Node.CustomTag n) 方法的访问。
public void visit(Node.CustomTag n) throws JasperException { doSmap(n); visitBody(n); } private void doSmap(Node n) { doSmap(n, 1, n.getEndJavaLine() - n.getBeginJavaLine(), 0); }
对于tag标签的访问,同样也没有太多的弯弯绕绕 。 看下图。
最终生成的lineData为
lineData数据已经生成好了,接下来看lineData合并成精简数据 ,进入optimizeLineSection()方法 。
/** * Combines consecutive LineInfos wherever possible */ public void optimizeLineSection() { /* Some debugging code for (int i = 0; i < lineData.size(); i++) { LineInfo li = (LineInfo)lineData.get(i); System.out.print(li.toString()); } */ //Incorporate each LineInfo into the previous LineInfo's //outputLineIncrement, if possible // 第一步分 int i = 0; while (i < lineData.size() - 1) { LineInfo li = lineData.get(i); LineInfo liNext = lineData.get(i + 1); if (!liNext.lineFileIDSet && liNext.inputStartLine == li.inputStartLine && liNext.inputLineCount == 1 && li.inputLineCount == 1 && liNext.outputStartLine == li.outputStartLine + li.inputLineCount * li.outputLineIncrement) { li.setOutputLineIncrement( liNext.outputStartLine - li.outputStartLine + liNext.outputLineIncrement); lineData.remove(i + 1); } else { i++; } } //Incorporate each LineInfo into the previous LineInfo's //inputLineCount, if possible // 第二部分 i = 0; while (i < lineData.size() - 1) { LineInfo li = lineData.get(i); LineInfo liNext = lineData.get(i + 1); if (!liNext.lineFileIDSet && liNext.inputStartLine == li.inputStartLine + li.inputLineCount && liNext.outputLineIncrement == li.outputLineIncrement && liNext.outputStartLine == li.outputStartLine + li.inputLineCount * li.outputLineIncrement) { li.setInputLineCount(li.inputLineCount + liNext.inputLineCount); lineData.remove(i + 1); } else { i++; } } }
上面这部分代码分两部分,第一部分是同一行jsp指向不同的java代码行号合并,第二分部就是1-93,2-94,3-95,4-96,5-97,6-98,7-99,8-100,9-101 合并成 1,9:93 或 21-114~116 , 22-117~119 合并成21,2:114,3 ,li和liNext 两个变量,我相信大家应该很清楚,就是看li和liNext两个lineInfo是否可以合并,如果可以合并,则从列表中移除掉liNext,而继续循环,liNext=liNext.next(),来看个例子就明白了。
从上图中lineData索引为12,13,14的java代码可以合并,15,16,17 的java 代码可以合并,先来看索引为12和13的合并原因 。
索引为12的LineInfo
- inputStartLine=21
- outputStartLine=114
- inputLineCount=1
- outputLineIncrement=1
索引为13的LineInfo
- inputStartLine=21
- outputStartLine=115
- inputLineCount=1
- outputLineIncrement=1
接下来看li为索引为12的lineInfo 和 liNext 索引为13的lineInfo , 看他们之间的合并条件。
- liNext.inputStartLine == li.inputStartLine=21
- liNext.inputLineCount == li.inputLineCount == 1
- liNext.outputStartLine == li.outputStartLine + li.inputLineCount * li.outputLineIncrement 等价于 115 = 114 + 1 * 1 == 115
而合并时
liNext.outputStartLine - li.outputStartLine + liNext.outputLineIncrement
等于li.outputLineIncrement = 115 - 114 + 1 = 2 ,此时索引为12的lineInfo的outputLineIncrement字段的值为12
上面条件符合条件,所以索引为12的lineInfo 和索引为13的lineInfo 可以合并,合并之后
索引为12的LineInfo
- inputStartLine=21
- outputStartLine=114
- inputLineCount=1
- outputLineIncrement=2
索引为14的lineInfo
- inputStartLine=21
- outputStartLine=114
- inputLineCount=1
- outputLineIncrement=2
接下来看li为索引为12的lineInfo 和 liNext 索引为14的lineInfo , 看他们之间的合并条件。
- liNext.inputStartLine == li.inputStartLine=21
- liNext.inputLineCount == li.inputLineCount == 1
- liNext.outputStartLine == li.outputStartLine + li.inputLineCount * li.outputLineIncrement 等价于 116 = 114 + 1 * 2 == 116
因此索引为12的lineInfo和索引为14的lineInfo可以合并。
因此合并之后。
那索引为12的lineInfo能否和之前索引为15的lineInfo合并呢?显然索引为12的 li.inputStartLine = 21 ,而索引为15的lineInfo的liNext.inputStartLine=22,不相等,不符合合并条件,因此不能合并 。
接下来看第二部分合并 。
先来看第一种情况 1-93,2-94,3-95,4-96,5-97,6-98,7-99,8-100,9-101 合并成1,9:93, 大家应该看得懂吧。 如 1-93 表示 jsp中的第一行对应java中93行, 2-94 中第2行对应生成java代码的94行,而21-114~116表示jsp中第21对应java代码从114行到116行 ,接下来看索引为1的lineInfo和索引为2的lineInfo合并 。
先看索引为0的lineInfo
- inputStartLine = 1
- outputStartLine = 93
- inputLineCount = 1
- outputLineIncrement = 1
先看索引为1的lineInfo
- inputStartLine = 2
- outputStartLine = 94
- inputLineCount = 1
- outputLineIncrement = 1
看合并条件 此时li是索引为0的lineInfo ,而 liNext是索引为1的lineInfo
- liNext.inputStartLine == li.inputStartLine + li.inputLineCount 等价于 2 = 1 + 1 = 2
- liNext.outputLineIncrement == li.outputLineIncrement = 1
- liNext.outputStartLine == li.outputStartLine + li.inputLineCount * li.outputLineIncrement 等价于 94 = 93 + 1 * 1 = 94
所以符合条件因此索引为0的lineInfo和索引为1的lineInfo是可以合并的。
其他的以此类推。 再继续看之前索引为12的lineInfo和索引为13的lineInfo 合并 。
索引为12的lineInfo为
- inputStartLine = 21
- outputStartLine = 114
- inputLineCount = 1
- outputLineIncrement = 3
先看索引为3的lineInfo
- inputStartLine = 22
- outputStartLine = 117
- inputLineCount = 1
- outputLineIncrement = 3
合并的判断条件
- liNext.inputStartLine == li.inputStartLine + li.inputLineCount = 22 = 21 + 1 = 22
- liNext.outputLineIncrement == li.outputLineIncrement = 3
- liNext.outputStartLine == li.outputStartLine + li.inputLineCount * li.outputLineIncrement = 117 = 114 + 1 * 3 = 117
因此索引为12和索引为13的lineInfo是可以合并的,合并之后变成了21,2:114,3 。
最终通过optimizeLineSection()方法调用后合并结果如下
当然可以在web.xml中初始化dumpSmap的值,从而打印出smap 。
<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>dumpSmap</param-name> <param-value>true</param-value> </init-param> </servlet>
费了那么大的劲得到smap有什么用呢?接下来继续看compile()方法 。
public void compile(boolean compileClass, boolean jspcMode) throws FileNotFoundException, JasperException, Exception { if (errDispatcher == null) { this.errDispatcher = new ErrorDispatcher(jspcMode); } try { String[] smap = generateJava(); File javaFile = new File(ctxt.getServletJavaFileName()); Long jspLastModified = ctxt.getLastModified(ctxt.getJspFile()); javaFile.setLastModified(jspLastModified.longValue()); if (compileClass) { generateClass(smap); // Fix for bugzilla 41606 // Set JspServletWrapper.servletClassLastModifiedTime after successful compile File targetFile = new File(ctxt.getClassFileName()); if (targetFile.exists()) { // 在访问uri对应的jsp文件时,如果文件内容被修改 // 则重新编译解析编译jsp文件 targetFile.setLastModified(jspLastModified.longValue()); if (jsw != null) { jsw.setServletClassLastModifiedTime( jspLastModified.longValue()); } } } } finally { if (tfp != null && ctxt.isPrototypeMode()) { tfp.removeProtoTypeFiles(null); } // Make sure these object which are only used during the // generation and compilation of the JSP page get // dereferenced so that they can be GC'd and reduce the // memory footprint. tfp = null; errDispatcher = null; pageInfo = null; // Only get rid of the pageNodes if in production. // In development mode, they are used for detailed // error messages. // http://bz.apache.org/bugzilla/show_bug.cgi?id=37062 if (!this.options.getDevelopment()) { pageNodes = null; } if (ctxt.getWriter() != null) { ctxt.getWriter().close(); ctxt.setWriter(null); } } }
在public void compile(boolean compileClass, boolean jspcMode)方法中有一个方法generateClass(String[] smap)值得我们注意 。
有两个实现类。而tomcat默认是JDTCompiler编译类,接下来我们对这个方法展开分析 。 在generateClass()方法中,代码有很多很多,但和smap相关的,就是 SmapUtil.installSmap(smap); 这样一行代码 ,关于tomcat如何编译class文件,并装载到类加载器中,后面再来分析,先来看smap的用途。进入installSmap()方法 。
public static void installSmap(String[] smap) throws IOException { if (smap == null) { return; } for (int i = 0; i < smap.length; i += 2) { File outServlet = new File(smap[i]); SDEInstaller.install(outServlet, smap[i+1].getBytes(Charset.defaultCharset())); } }
本例中
smap[0]= /Users/quyixiao/gitlab/tomcat/work/Catalina/localhost/servelet-test-1.0/org/apache/jsp/index_jsp.class,
smap[1] =
SMAP index_jsp.java JSP *S JSP *F + 0 index.jsp index.jsp *L 1,9:93 9:101 11,10:103 20:113 21,2:114,3 23,5:120 27:150,9 27:126 *E
smap[0] 和smap[1] 将作为install(File classFile, byte[] smap) 的两个参数传入。进入install()方法 。
static void install(File classFile, byte[] smap) throws IOException { File tmpFile = new File(classFile.getPath() + "tmp"); // 修改class文件的字节码 new SDEInstaller(classFile, smap, tmpFile); // 删除修改之前的class文件 if (!classFile.delete()) { throw new IOException(Localizer.getMessage("jsp.error.unable.deleteClassFile", classFile.getAbsolutePath())); } // 将修改之后的文件命名为classFile文件 if (!tmpFile.renameTo(classFile)) { throw new IOException(Localizer.getMessage("jsp.error.unable.renameClassFile", tmpFile.getAbsolutePath(), classFile.getAbsolutePath())); } }
上面这个方法原理也很简单。
- 修改class文件字节码
- 删除原来文件的class字节码
- 将修改后的临时文件改为原来被删除的文件 文件名,第2步和第3步的就是将修改后的class文件字节码替换掉之前的class文件。
接下来的重头戏,修改class文件字节码 ,进入SDEInstaller的构造函数 。
SDEInstaller(File inClassFile, byte[] sdeAttr, File outClassFile) throws IOException { if (!inClassFile.exists()) { throw new FileNotFoundException("no such file: " + inClassFile); } System.out.println(inClassFile.getAbsolutePath()); byte [] origin = ClassReaderUtils.readClass(inClassFile.getAbsolutePath()); ClassReaderUtils. writeBytesToFile(origin,"/Users/quyixiao/gitlab/tomcat/output/origin.class"); System.out.println(outClassFile.getAbsolutePath()); this.sdeAttr = sdeAttr; // get the bytes orig = readWhole(inClassFile); gen = new byte[orig.length + sdeAttr.length + 100]; // 修改字节码 addSDE(); // write result FileOutputStream outStream = null; try { outStream = new FileOutputStream(outClassFile); outStream.write(gen, 0, genPos); } finally { if (outStream != null) { try { outStream.close(); System.out.println("=========================================="); byte [] out = ClassReaderUtils.readClass(outClassFile.getAbsolutePath()); ClassReaderUtils. writeBytesToFile(out,"/Users/quyixiao/gitlab/tomcat/output/out.class"); } catch (Exception e) { } } } }
蓝色代码是我自己添加的代码,主要是将修改之前和修改之后的字节码输出到文件中,方便对比,因为在install()方法中会删除掉原始的class文件,因此这里新创建了两个文件,将字节码写入其中,再进行对比,再分析addSDE()对字节码做了哪些事情,先看字节码做了哪些改动。
没有办法CSDN博客只能写20万字左右,这里接着上一篇博客继续分析 。请看另外一篇博客 Tomcat 源码解析一JSP编译器Jasper-佛怒火莲(下)