没有办法CSDN博客只能写20万字左右,这里接着上一篇博客继续分析 。
用在线对比工具比对比,发现class字节码中有2处增加了内容,有一处由01 修改 成了02 , 先来看增加的16进制转化为字符串后是什么 。
先来看 01 00 14 53 6F 75 72 63 65 44 65 62 75 67 45 78 74 65 6E 73 69 6F 6E
先来看常量池中UTF8常量结构
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
在常量池中第一个字节表示当前常量所属类型,之前我写了一个项目,叫自己手写虚拟机,项目中有一段下面截图中的代码,如第一个字节是1 ,表示utf8类型,tag = 7 ,则表示class类型, 10 为方法引用等
自己手写虚拟机博客
自己动手写Java虚拟机 (Java核心技术系列)_java版
还有一篇
Java字节码文件结构剖析(一) 关于字节码解析的,如果不懂,可以去看看这两篇博客 。
再来看 01 00 14 53 6F 75 72 63 65 44 65 62 75 67 45 78 74 65 6E 73 69 6F 6E , 01 表示当前常量池 utf8类型, u2 表示后面两个字节表示utf8字符串的长度,00 14 从16进制转化为10进制为1 * 16 + 4 = 20,也就是向后取20个字节为53 6F 75 72 63 65 44 65 62 75 67 45 78 74 65 6E 73 69 6F 6E,刚好转化为字符串类型为SourceDebugExtension。
其他的,自己去查吧,从字节码的修改来看,是在常量池中添加了一个utf8的常量,并且常量的内容为SourceDebugExtension。
接下来,看第二段增加的内容,00 FB 00 00 00 82 53 4D 41 50 0A 69 6E 64 65 78 5F 6A 73 70 2E 6A 61 76 61 0A 4A 53 50 0A 2A 53 20 4A 53 50 0A 2A 46 0A 2B 20 30 20 69 6E 64 65 78 2E 6A 73 70 0A 69 6E 64 65 78 2E 6A 73 70 0A 2A 4C 0A 31 2C 39 3A 39 33 0A 39 3A 31 30 31 0A 31 31 2C 31 30 3A 31 30 33 0A 32 30 3A 31 31 33 0A 32 31 2C 32 3A 31 31 34 2C 33 0A 32 33 2C 35 3A 31 32 30 0A 32 37 3A 31 35 30 2C 39 0A 32 37 3A 31 32 36 0A 2A 45 0A, 既然常量池中加了SourceDebugExtension常量,那推测上面这段字节码和SourceDebugExtension属性有关,查一下他的结构 。
00 FB表示指向常量池的索引,转化为10进制为251
00 00 00 82 后面4个字节表示attribute_length 转化为10进制为130 。
53 4D 41 50 0A 69 6E 64 65 78 5F 6A 73 70 2E 6A 61 76 61 0A 4A 53 50 0A 2A 53 20 4A 53 50 0A 2A 46 0A 2B 20 30 20 69 6E 64 65 78 2E 6A 73 70 0A 69 6E 64 65 78 2E 6A 73 70 0A 2A 4C 0A 31 2C 39 3A 39 33 0A 39 3A 31 30 31 0A 31 31 2C 31 30 3A 31 30 33 0A 32 30 3A 31 31 33 0A 32 31 2C 32 3A 31 31 34 2C 33 0A 32 33 2C 35 3A 31 32 30 0A 32 37 3A 31 35 30 2C 39 0A 32 37 3A 31 32 36 0A 2A 45 0A 其余的转化为字符串为
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
现在应该比较明白了吧。 那下面红框里的这一段00 F9 00 00 00 02 00 FA 字节码又表示的是什么呢?
之前我们见过SourceFile的属性结构如下。
00 F9 :F9 指向常量池的索引,转化为10进制为249
00 00 00 02 : 属性长度占4个字节 2 ,因此向后读取两个字节。
00 FA :指向常量池中的索引 250
有人会说,你说得好听的,但是怎么来证明呢? 我们用jclasslib bytecode viewer打印修改过的class文件 。
是不是和我们之前的分析一模一样,当然啦,上面是对字节码的添加做的事情,下面来分析对字节码从01 修改为02 又是什么意思呢?还是借助于之前手动写java虚拟机的代码来看。
因此进入readAttributes()方法 。
java字节码规定,在读取属性表之前,有2个字节标识当前属性表长度,也就是原文件的00 01 和 修改后00 02表示属性表长度,显然之前只有SourceFile属性,现在增加了SourceDebugExtension属性信息,所以需要将原来的长度由1 修改为2 。
到这里,我们来总结一下 static void install(File classFile, byte[] smap) 到底对字节码做了哪些事情 。
- 在常量池中增加Utf8的SourceDebugExtension常量。
- 修改属性表长度
- 在属性表中添加SourceDebugExtension属性。
- 当然常量池的个数也做了修改,这里就不分析了。 一样的道理 。
从结果来看,tomcat对原字节码做了以上修改,接下来看代码如何实现,不过在看这段代码之前,继续回顾一下class文件的字节码结构 。
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
有了字节码结构,再来看源码就清淅多了。
void addSDE() throws UnsupportedEncodingException, IOException { // 我们知道前面4个字节是魔数,2 个字节是次版本号,2个字节是主版本号, // 这里主要是复制前面8个字节 copy(4 + 2 + 2); int constantPoolCountPos = genPos; // 再读取两个字节,为常量池个数 int constantPoolCount = readU2(); if (log.isDebugEnabled()) log.debug("constant pool count: " + constantPoolCount); // 将常量池个数以字节码形式写到gen的byte数组中 writeU2(constantPoolCount); // copy old constant pool return index of SDE symbol, if found // 复制常量池,如果返回值sdeIndex > 0 ,则表示常量池中已经有SourceDebugExtension // 的UTF8常量 sdeIndex = copyConstantPool(constantPoolCount); if (sdeIndex < 0) { // if "SourceDebugExtension" symbol not there add it writeUtf8ForSDE(); // increment the constantPoolCount sdeIndex = constantPoolCount; ++constantPoolCount; // 修改常量池个数constantPoolCountPos = constantPoolCountPos + 1 randomAccessWriteU2(constantPoolCountPos, constantPoolCount); if (log.isDebugEnabled()) log.debug("SourceDebugExtension not found, installed at: " + sdeIndex); } else { if (log.isDebugEnabled()) log.debug("SourceDebugExtension found at: " + sdeIndex); } // access, this, super // access_flags 2 个字节指向常量池索引,当前类的访问标识 // this_class 2个字节指向当前类 // super_class 2 个字节指向父类,这些都不需要修改,直接复制即可 copy(2 + 2 + 2); //2 个字节,表示当前类实现接口数 int interfaceCount = readU2(); // 将接口数以2个字节写到新byte[]数组中 writeU2(interfaceCount); if (log.isDebugEnabled()) log.debug("interfaceCount: " + interfaceCount); // 因为接口也是指向常量池中的索引 // interfaces[interfaces_count] ,而指向常量池的索引占两个字节 // 因此最终需要复制的字节数 = 接口数 * 2 copy(interfaceCount * 2); copyMembers(); // 复制类中所有field属性 copyMembers(); // 复制类中所有的方法表 int attrCountPos = genPos; // 获取属性个数 attributes_count ,当然属性个数占两个字节 int attrCount = readU2(); writeU2(attrCount); if (log.isDebugEnabled()) log.debug("class attrCount: " + attrCount); // copy the class attributes, return true if SDE attr found (not copied) // 如果属性中没有 SourceDebugExtension,则增加个数 if (!copyAttrs(attrCount)) { // 修改attributes_count = attributes_count + 1 ++attrCount; // 将修改的值写到attributes_count 对应的字节码位置 randomAccessWriteU2(attrCountPos, attrCount); if (log.isDebugEnabled()) log.debug("class attrCount incremented"); } // sdeIndex 表示SourceDebugExtension的UTF8所在常量池中索引 ,将smap解析得到的行号对应关系写到字节码中 writeAttrForSDE(sdeIndex); }
下面来看复制常量池方法 ,不过在看常量池结构之前先来看常量池中每种类型及其含义,以及每种类型所点字节。
int copyConstantPool(int constantPoolCount) throws UnsupportedEncodingException, IOException { int sdeIndex = -1; // copy const pool index zero not in class file for (int i = 1; i < constantPoolCount; ++i) { int tag = readU1(); writeU1(tag); switch (tag) { case 7 : // Class case 8 : // String case 16 : // MethodType if (log.isDebugEnabled()) log.debug(i + " copying 2 bytes"); copy(2); break; case 15 : // MethodHandle if (log.isDebugEnabled()) log.debug(i + " copying 3 bytes"); copy(3); break; case 9 : // Field case 10 : // Method case 11 : // InterfaceMethod case 3 : // Integer case 4 : // Float case 12 : // NameAndType case 18 : // InvokeDynamic if (log.isDebugEnabled()) log.debug(i + " copying 4 bytes"); copy(4); break; case 5 : // Long case 6 : // Double if (log.isDebugEnabled()) log.debug(i + " copying 8 bytes"); copy(8); i++; break; case 1 : // Utf8 int len = readU2(); writeU2(len); byte[] utf8 = readBytes(len); String str = new String(utf8, "UTF-8"); if (log.isDebugEnabled()) log.debug(i + " read class attr -- '" + str + "'"); if (str.equals("SourceDebugExtension")) { sdeIndex = i; } writeBytes(utf8); break; default : throw new IOException("unexpected tag: " + tag); } } return sdeIndex; }
简单分析一下,如CONSTANT_Long_info,CONSTANT_Double_info类型结构为
{
tag u1
bytes u8
}
为什么复制字节码时是copy(8);而不是copy(9)呢,大家注意
int tag = readU1();
writeU1(tag);
这两行代码,tag = readU1(); 读取到tag标识当前常量属于常量池中哪种类型,并且将tag写到新的byte[] 数组中 ,因此后面只需要拷贝8个字节,并且有一行代码 if (str.equals(“SourceDebugExtension”)) 判断,如果当前UTF8类型是SourceDebugExtension,则不需要向常量池中写入UTF8的SourceDebugExtension类型了。 并将UTF8类型是SourceDebugExtension 常量的索引返回,方便写入attributeInfo时引用此索引 。
如果常量池中没有UTF8类型是SourceDebugExtension 常量,则调用writeUtf8ForSDE()向常量池中写入。
void writeUtf8ForSDE() { int len = "SourceDebugExtension".length(); writeU1(1); // 1 表示是UTF8常量 writeU2(len); // 2个字节表示常量字符串的长度 // 将 "SourceDebugExtension"字符串以16进制写入新byte[]数组中 for (int i = 0; i < len; ++i) { writeU1("SourceDebugExtension".charAt(i)); } }
修改常量池个数方法。从pos位置开始,将val转化为两个字节,从pos位置写入,再恢复之前的写入位置。
void randomAccessWriteU2(int pos, int val) { // 保存之前的位置 int savePos = genPos; genPos = pos; // 修改pos位置2个字节的内容 writeU2(val); // 恢复成之前的位置 genPos = savePos; }
将smap解析得到的jsp行号与生成java代码的行号对应关系写到字节码中。
void writeAttrForSDE(int index) { // 2 个字节指向常量池中SourceDebugExtension writeU2(index); // 用4个字节存储smap信息 writeU4(sdeAttr.length); // 将smap中信息存储到新的byte[]数组中 for (int i = 0; i < sdeAttr.length; ++i) { writeU1(sdeAttr[i]); } }
下面这个方法中需要注意一点,当attribute_info中有SourceDebugExtension时,是不会复制之前的SourceDebugExtension信息的,因此attrCount是不需要++,直接将新的SourceDebugExtension替代掉之前的SourceDebugExtension即可,这也是attrCount 不需要++,直接调用writeAttrForSDE()就可以的原因。
boolean copyAttrs(int attrCount) { boolean sdeFound = false; for (int i = 0; i < attrCount; ++i) { int nameIndex = readU2(); // don't write old SDE if (nameIndex == sdeIndex) { sdeFound = true; if (log.isDebugEnabled()) log.debug("SDE attr found"); } else { writeU2(nameIndex); // name int len = readU4(); writeU4(len); copy(len); if (log.isDebugEnabled()) log.debug("attr len: " + len); } } return sdeFound; }
我相信到这里,大家肯定已经明白smap是怎样被加到字节码中去了,那SourceDebugExtension加到字节码中有什么用呢?看了
《Java虚拟机规范.Java SE 8版》的第101页,也没有看到如何使用。
难道是在代码抛出异常时,定位异常用的不?来看一个例子。
生成的index_jsp.java的代码是
再看_jspx_page_context是个什么对象。
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; ...
从生成的jsp代码中得知,_jspx_page_context = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);,那_jspxFactory又是怎样来的呢?从生成的jsp文件中得知。
private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory.getDefaultFactory();
进入JspFactory类
public static void setDefaultFactory(JspFactory deflt) { JspFactory.deflt = deflt; } public static JspFactory getDefaultFactory() { return deflt; }
发现_jspxFactory最终又来源于setDefaultFactory()方法 ,查找一下,发现setDefaultFactory()方法又在JspRuntimeContext的静态初始化方法中调用了。
static { JspFactoryImpl factory = new JspFactoryImpl(); SecurityClassLoad.securityClassLoad(factory.getClass().getClassLoader()); if( System.getSecurityManager() != null ) { String basePackage = "org.apache.jasper."; try { ... } catch (ClassNotFoundException ex) { throw new IllegalStateException(ex); } } JspFactory.setDefaultFactory(factory); }
最终得到_jspxFactory实际上是JspFactoryImpl,而进入其getPageContext()方法 。
public PageContext getPageContext(Servlet servlet, ServletRequest request, ServletResponse response, String errorPageURL, boolean needsSession, int bufferSize, boolean autoflush) { if( Constants.IS_SECURITY_ENABLED ) { PrivilegedGetPageContext dp = new PrivilegedGetPageContext( this, servlet, request, response, errorPageURL, needsSession, bufferSize, autoflush); return AccessController.doPrivileged(dp); } else { return internalGetPageContext(servlet, request, response, errorPageURL, needsSession, bufferSize, autoflush); } } private PageContext internalGetPageContext(Servlet servlet, ServletRequest request, ServletResponse response, String errorPageURL, boolean needsSession, int bufferSize, boolean autoflush) { try { PageContext pc; if (USE_POOL) { PageContextPool pool = localPool.get(); if (pool == null) { pool = new PageContextPool(); localPool.set(pool); } pc = pool.get(); if (pc == null) { pc = new PageContextImpl(); } } else { pc = new PageContextImpl(); } pc.initialize(servlet, request, response, errorPageURL, needsSession, bufferSize, autoflush); return pc; } catch (Throwable ex) { ExceptionUtils.handleThrowable(ex); if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } log.fatal("Exception initializing page context", ex); return null; } }
最终得到PageContext实际上是PageContextImpl对象,而其private void doHandlePageException(Throwable t)方法如下 。
private void doHandlePageException(Throwable t) throws IOException, ServletException { if (errorPageURL != null && !errorPageURL.equals("")) { /* * Set request attributes. Do not set the * javax.servlet.error.exception attribute here (instead, set in the * generated servlet code for the error page) in order to prevent * the ErrorReportValve, which is invoked as part of forwarding the * request to the error page, from throwing it if the response has * not been committed (the response will have been committed if the * error page is a JSP page). */ request.setAttribute(PageContext.EXCEPTION, t); request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, Integer.valueOf(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)); request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, ((HttpServletRequest) request).getRequestURI()); request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME, config.getServletName()); try { forward(errorPageURL); } catch (IllegalStateException ise) { include(errorPageURL); } // The error page could be inside an include. Object newException = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); // t==null means the attribute was not set. if ((newException != null) && (newException == t)) { request.removeAttribute(RequestDispatcher.ERROR_EXCEPTION); } // now clear the error code - to prevent double handling. request.removeAttribute(RequestDispatcher.ERROR_STATUS_CODE); request.removeAttribute(RequestDispatcher.ERROR_REQUEST_URI); request.removeAttribute(RequestDispatcher.ERROR_SERVLET_NAME); request.removeAttribute(PageContext.EXCEPTION); } else { // Otherwise throw the exception wrapped inside a ServletException. // Set the exception as the root cause in the ServletException // to get a stack trace for the real problem if (t instanceof IOException) throw (IOException) t; if (t instanceof ServletException) throw (ServletException) t; if (t instanceof RuntimeException) throw (RuntimeException) t; Throwable rootCause = null; if (t instanceof JspException) { rootCause = ((JspException) t).getCause(); } else if (t instanceof ELException) { rootCause = ((ELException) t).getCause(); } if (rootCause != null) { throw new ServletException(t.getClass().getName() + ": " + t.getMessage(), rootCause); } throw new ServletException(t); } }
因为没有配置错误页面,因此最终进入上面的加粗代码 ,抛出java.lang.ArithmeticException: / by zero 异常。
在servlet.service()方法调用时,抛出了java.lang.ArithmeticException: / by zero 异常,最终被handleJspException()方法捕获到。
进入 handleJspException(Exception ex)方法 。
protected JasperException handleJspException(Exception ex) { try { Throwable realException = ex; if (ex instanceof ServletException) { realException = ((ServletException) ex).getRootCause(); } // 获取到异常堆栈 StackTraceElement[] frames = realException.getStackTrace(); StackTraceElement jspFrame = null; for (int i=0; i<frames.length; ++i) { // 遍历所有的异常堆栈,如果栈中的类名与servlet中类名一样,则所在的栈帧的行号就是生成_jsp.java文件抛出异常所在的java行号 if ( frames[i].getClassName().equals(this.getServlet().getClass().getName()) ) { jspFrame = frames[i]; break; } } if (jspFrame == null || this.ctxt.getCompiler().getPageNodes() == null) { // If we couldn't find a frame in the stack trace corresponding // to the generated servlet class or we don't have a copy of the // parsed JSP to hand, we can't really add anything return new JasperException(ex); } // 获取抛出异常的java 行号 int javaLineNumber = jspFrame.getLineNumber(); // 创建异常详细信息 JavacErrorDetail detail = ErrorDispatcher.createJavacError( jspFrame.getMethodName(), this.ctxt.getCompiler().getPageNodes(), null, javaLineNumber, ctxt); // If the line number is less than one we couldn't find out // where in the JSP things went wrong int jspLineNumber = detail.getJspBeginLineNumber(); if (jspLineNumber < 1) { throw new JasperException(ex); } if (options.getDisplaySourceFragment()) { return new JasperException(Localizer.getMessage ("jsp.exception", detail.getJspFileName(), "" + jspLineNumber) + Constants.NEWLINE + Constants.NEWLINE + detail.getJspExtract() + Constants.NEWLINE + Constants.NEWLINE + "Stacktrace:", ex); } return new JasperException(Localizer.getMessage ("jsp.exception", detail.getJspFileName(), "" + jspLineNumber), ex); } catch (Exception je) { // If anything goes wrong, just revert to the original behaviour if (ex instanceof JasperException) { return (JasperException) ex; } return new JasperException(ex); } }
如何得到异常的详细信息呢?进入createJavacError()方法 。
public static JavacErrorDetail createJavacError(String fname, Node.Nodes page, StringBuilder errMsgBuf, int lineNum, JspCompilationContext ctxt) throws JasperException { JavacErrorDetail javacError; // Attempt to map javac error line number to line in JSP page ErrorVisitor errVisitor = new ErrorVisitor(lineNum); page.visit(errVisitor); // 得到抛出异常的Node Node errNode = errVisitor.getJspSourceNode(); if ((errNode != null) && (errNode.getStart() != null)) { // If this is a scriplet node then there is a one to one mapping // between JSP lines and Java lines if (errVisitor.getJspSourceNode() instanceof Node.Scriptlet || errVisitor.getJspSourceNode() instanceof Node.Declaration) { javacError = new JavacErrorDetail( fname, lineNum, errNode.getStart().getFile(), // 通过java代码抛出异常的行号计算出jsp抛出异常行号 errNode.getStart().getLineNumber() + lineNum - errVisitor.getJspSourceNode().getBeginJavaLine(), errMsgBuf, ctxt); } else { javacError = new JavacErrorDetail( fname, lineNum, errNode.getStart().getFile(), errNode.getStart().getLineNumber(), errMsgBuf, ctxt); } } else { /* * javac error line number cannot be mapped to JSP page * line number. For example, this is the case if a * scriptlet is missing a closing brace, which causes * havoc with the try-catch-finally block that the code * generator places around all generated code: As a result * of this, the javac error line numbers will be outside * the range of begin and end java line numbers that were * generated for the scriptlet, and therefore cannot be * mapped to the start line number of the scriptlet in the * JSP page. * Include just the javac error info in the error detail. */ javacError = new JavacErrorDetail( fname, lineNum, errMsgBuf); } return javacError; }
先通过调用ErrorVisitor的访问者模式,得到抛出异常的Node,我们之前不是分析过,jsp最终被解析成一个一个Node,接下来看ErrorVisitor内部做了哪些事情 。
static class ErrorVisitor extends Node.Visitor { // Java source line number to be mapped private int lineNum; /* * JSP node whose Java source code range in the generated servlet * contains the Java source line number to be mapped */ Node found; /* * Constructor. * * @param lineNum Source line number in the generated servlet code */ public ErrorVisitor(int lineNum) { this.lineNum = lineNum; } @Override public void doVisit(Node n) throws JasperException { if ((lineNum >= n.getBeginJavaLine()) && (lineNum < n.getEndJavaLine())) { found = n; } } /* * Gets the JSP node to which the source line number in the generated * servlet code was mapped. * * @return JSP node to which the source line number in the generated * servlet code was mapped */ public Node getJspSourceNode() { return found; } }
上面代码重点就是加粗代码,如果抛出异常的行号在Node的java代码开始行和结束行之间,那么抛出异常的地方肯定在这个Node中。在JavacErrorDetail对象创建之前,有一个变量 errNode.getStart().getLineNumber() + lineNum -
errVisitor.getJspSourceNode().getBeginJavaLine(),这个是什么意思呢? 如Node节点的java开始行为102 , 对应的jsp 代码是第10行,而java 代码抛出异常的行号是109 ,那么jsp中抛出异常的行号是多少呢? 不就是109 - 102 + 10 = 17 不?因此这个变量就是计算jsp抛出异常的代码行号。
接下来进入JavacErrorDetail的构造函数 。
public JavacErrorDetail(String javaFileName, int javaLineNum, String jspFileName, int jspBeginLineNum, StringBuilder errMsg, JspCompilationContext ctxt) { this(javaFileName, javaLineNum, errMsg); this.jspFileName = jspFileName; this.jspBeginLineNum = jspBeginLineNum; if (jspBeginLineNum > 0 && ctxt != null) { InputStream is = null; FileInputStream fis = null; try { // Read both files in, so we can inspect them JarFile jarFile = null; JarResource tagJarResource = ctxt.getTagFileJarResource(); if (tagJarResource != null) { jarFile = tagJarResource.getJarFile(); } is = JspUtil.getInputStream(jspFileName, jarFile, ctxt, null); // 读取jsp文件 String[] jspLines = readFile(is); fis = new FileInputStream(ctxt.getServletJavaFileName()); // 读取生成的java文件 String[] javaLines = readFile(fis); if (jspLines.length < jspBeginLineNum) { // Avoid ArrayIndexOutOfBoundsException // Probably bug 48498 but could be some other cause jspExtract = Localizer.getMessage("jsp.error.bug48498"); return; } // If the line contains the opening of a multi-line scriptlet // block, then the JSP line number we got back is probably // faulty. Scan forward to match the java line... // 正常情况都是 // <% // int a = 0; // int b = 1 / 0 ; // %> // 但如果出现 // // int a = 0 ; // %> // <% int b = 1/0; // 时, 需要对Node对应的jsp代码行重新遍历,直到jsp和java行号相等,重新更新 // jspBeginLineNum 行号 if (jspLines[jspBeginLineNum-1].lastIndexOf("<%") > jspLines[jspBeginLineNum-1].lastIndexOf("%>")) { String javaLine = javaLines[javaLineNum-1].trim(); for (int i=jspBeginLineNum-1; i<jspLines.length; i++) { if (jspLines[i].indexOf(javaLine) != -1) { // Update jsp line number this.jspBeginLineNum = i+1; break; } } } // copy out a fragment of JSP to display to the user StringBuilder fragment = new StringBuilder(1024); // 打印出抛出异常行号 -4 到 抛出异常行号 + 2 之间的jsp 代码 // 更加有利于开发者定位问题 int startIndex = Math.max(0, this.jspBeginLineNum-1-3); int endIndex = Math.min( jspLines.length-1, this.jspBeginLineNum-1+3); for (int i=startIndex;i<=endIndex; ++i) { // jsp 对应的行号, 但这里 i + 1 ,实际上打印的是 抛出异常代码行号 -3 ~ + 3之间的jsp代码 fragment.append(i+1); fragment.append(": "); // jsp对应行号 jsp 代码 fragment.append(jspLines[i]); fragment.append(Constants.NEWLINE); } jspExtract = fragment.toString(); } catch (JasperException je) { // Exception is never thrown - ignore } catch (IOException ioe) { // Can't read files - ignore } finally { if (is != null) { try { is.close(); } catch (IOException ioe) { // Ignore } } if (fis != null) { try { fis.close(); } catch (IOException ioe) { // Ignore } } } } }
关于jsp抛出异常的细节部分都已经分析过了。 最终通过JasperException异常抛出到前端页面 。
return new JasperException(Localizer.getMessage "jsp.exception", detail.getJspFileName(), "" + jspLineNumber) + Constants.NEWLINE + Constants.NEWLINE + detail.getJspExtract() + Constants.NEWLINE + Constants.NEWLINE + "Stacktrace:", ex);
我们分析了那么多,终于知道当jsp文件抛出异常时,tomcat是如何处理,但遗憾的是,依然不知道SourceDebugExtension怎样用。
从网上资料显示 SourceDebugExtension属性用于存储额外的代码调试信息。 典型的场景是在进行JSP文件调试时, 没法经过Java堆栈来定位到JSP文件的行号。 可能是用来做调试用的吧。关于smap的分析到这里了,接下来,使是生成的jsp代码编译的分析 。
16.2.2 JDT Compiler编译器
通过JSP编译器编译后,生成了对应的Java文件,接下来, 要把Java文件编译成Class文件,对于 这部分,完全没有必要重复造轮子,常见的优秀编译工具有Eclispe JDT Java编译器和Ant编译器,Tomcat 其实同时支持两个编译器, 通过配置可以选择,而默认使用Eclipse JDT 编译器。
通过调用这些现在的编译器的API ,就可以方便的实现对Java 文件的编译,由于两个编译器的功能一样,因此就选 默认的Eclipse JDT 编译器,下面看如何用Eclipse JDT 编译器编译java文件 。
Eclipse JDT 提供了Compiler 类用于编译,它的构造函数比较复杂,如下所示 ,其实,实现自定义的构造函数包含的参数即基本完成了编译工作 。
public Compiler {
INameEnvironment environment ;
IErrorHandlingPolicy policy ;
CompilerOptions options ;
final ICompilerRequestor requestor ,
IProblemFactory problemFactory ;
}
为了方便说明,直接给出一个简单的编译实现,如下图所示 。
/** * JDT class compiler. This compiler will load source dependencies from the * context classloader, reducing dramatically disk access during * the compilation process. * * @author Cocoon2 * @author Remy Maucherat */ public class JDTCompiler extends org.apache.jasper.compiler.Compiler { private static final String JDT_JAVA_9_VERSION; static { // The constant for Java 9 changed between 4.6 and 4.7 in a way that is // not backwards compatible. Need to figure out which version is in use // so the correct constant value is used. String jdtJava9Version = null; Class<?> clazz = CompilerOptions.class; for (Field field : clazz.getFields()) { if ("VERSION_9".equals(field.getName())) { // 4.7 onwards: CompilerOptions.VERSION_9 jdtJava9Version = "9"; break; } } if (jdtJava9Version == null) { // 4.6 and earlier: CompilerOptions.VERSION_1_9 jdtJava9Version = "1.9"; } JDT_JAVA_9_VERSION = jdtJava9Version; } private final Log log = LogFactory.getLog(JDTCompiler.class); // must not be static /** * Compile the servlet from .java file to .class file */ @Override protected void generateClass(String[] smap) throws FileNotFoundException, JasperException, Exception { long t1 = 0; if (log.isDebugEnabled()) { t1 = System.currentTimeMillis(); } final String sourceFile = ctxt.getServletJavaFileName(); final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); String packageName = ctxt.getServletPackageName(); final String targetClassName = ((packageName.length() != 0) ? (packageName + ".") : "") + ctxt.getServletClassName(); final ClassLoader classLoader = ctxt.getJspLoader(); String[] fileNames = new String[] {sourceFile}; String[] classNames = new String[] {targetClassName}; final ArrayList<JavacErrorDetail> problemList = new ArrayList<JavacErrorDetail>(); class CompilationUnit implements ICompilationUnit { private final String className; private final String sourceFile; CompilationUnit(String sourceFile, String className) { this.className = className; this.sourceFile = sourceFile; } @Override public char[] getFileName() { return sourceFile.toCharArray(); } @Override public char[] getContents() { char[] result = null; FileInputStream is = null; InputStreamReader isr = null; Reader reader = null; try { is = new FileInputStream(sourceFile); isr = new InputStreamReader(is, ctxt.getOptions().getJavaEncoding()); reader = new BufferedReader(isr); char[] chars = new char[8192]; StringBuilder buf = new StringBuilder(); int count; while ((count = reader.read(chars, 0, chars.length)) > 0) { buf.append(chars, 0, count); } result = new char[buf.length()]; buf.getChars(0, result.length, result, 0); } catch (IOException e) { log.error("Compilation error", e); } finally { if (reader != null) { try { reader.close(); } catch (IOException ioe) {/*Ignore*/} } if (isr != null) { try { isr.close(); } catch (IOException ioe) {/*Ignore*/} } if (is != null) { try { is.close(); } catch (IOException exc) {/*Ignore*/} } } return result; } @Override public char[] getMainTypeName() { int dot = className.lastIndexOf('.'); if (dot > 0) { return className.substring(dot + 1).toCharArray(); } return className.toCharArray(); } @Override public char[][] getPackageName() { StringTokenizer izer = new StringTokenizer(className, "."); char[][] result = new char[izer.countTokens()-1][]; for (int i = 0; i < result.length; i++) { String tok = izer.nextToken(); result[i] = tok.toCharArray(); } return result; } @Override public boolean ignoreOptionalProblems() { return false; } } final INameEnvironment env = new INameEnvironment() { @Override public NameEnvironmentAnswer findType(char[][] compoundTypeName) { StringBuilder result = new StringBuilder(); String sep = ""; for (int i = 0; i < compoundTypeName.length; i++) { result.append(sep); result.append(compoundTypeName[i]); sep = "."; } return findType(result.toString()); } @Override public NameEnvironmentAnswer findType(char[] typeName, char[][] packageName) { StringBuilder result = new StringBuilder(); String sep = ""; for (int i = 0; i < packageName.length; i++) { result.append(sep); result.append(packageName[i]); sep = "."; } result.append(sep); result.append(typeName); return findType(result.toString()); } private NameEnvironmentAnswer findType(String className) { InputStream is = null; try { if (className.equals(targetClassName)) { ICompilationUnit compilationUnit = new CompilationUnit(sourceFile, className); return new NameEnvironmentAnswer(compilationUnit, null); } String resourceName = className.replace('.', '/') + ".class"; is = classLoader.getResourceAsStream(resourceName); if (is != null) { byte[] classBytes; byte[] buf = new byte[8192]; ByteArrayOutputStream baos = new ByteArrayOutputStream(buf.length); int count; while ((count = is.read(buf, 0, buf.length)) > 0) { baos.write(buf, 0, count); } baos.flush(); classBytes = baos.toByteArray(); char[] fileName = className.toCharArray(); ClassFileReader classFileReader = new ClassFileReader(classBytes, fileName, true); return new NameEnvironmentAnswer(classFileReader, null); } } catch (IOException exc) { log.error("Compilation error", exc); } catch (org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException exc) { log.error("Compilation error", exc); } finally { if (is != null) { try { is.close(); } catch (IOException exc) { // Ignore } } } return null; } private boolean isPackage(String result) { if (result.equals(targetClassName)) { return false; } String resourceName = result.replace('.', '/') + ".class"; InputStream is = null; try { is = classLoader.getResourceAsStream(resourceName); return is == null; } finally { if (is != null) { try { is.close(); } catch (IOException e) { } } } } @Override public boolean isPackage(char[][] parentPackageName, char[] packageName) { StringBuilder result = new StringBuilder(); String sep = ""; if (parentPackageName != null) { for (int i = 0; i < parentPackageName.length; i++) { result.append(sep); result.append(parentPackageName[i]); sep = "."; } } if (Character.isUpperCase(packageName[0])) { if (!isPackage(result.toString())) { return false; } } result.append(sep); result.append(packageName); return isPackage(result.toString()); } @Override public void cleanup() { } }; final IErrorHandlingPolicy policy = DefaultErrorHandlingPolicies.proceedWithAllProblems(); final Map<String,String> settings = new HashMap<String,String>(); settings.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.GENERATE); settings.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.GENERATE); settings.put(CompilerOptions.OPTION_ReportDeprecation, CompilerOptions.IGNORE); if (ctxt.getOptions().getJavaEncoding() != null) { settings.put(CompilerOptions.OPTION_Encoding, ctxt.getOptions().getJavaEncoding()); } if (ctxt.getOptions().getClassDebugInfo()) { settings.put(CompilerOptions.OPTION_LocalVariableAttribute, CompilerOptions.GENERATE); } // Source JVM if(ctxt.getOptions().getCompilerSourceVM() != null) { String opt = ctxt.getOptions().getCompilerSourceVM(); if(opt.equals("1.1")) { settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_1); } else if(opt.equals("1.2")) { settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_2); } else if(opt.equals("1.3")) { settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_3); ... } else { log.warn(Localizer.getMessage("jsp.warning.unknown.sourceVM", opt)); settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_6); } } else { // Default to 1.6 settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_6); } // Target JVM if(ctxt.getOptions().getCompilerTargetVM() != null) { String opt = ctxt.getOptions().getCompilerTargetVM(); settings.put(CompilerOptions.OPTION_Compliance, JDT_JAVA_9_VERSION); } else if(opt.equals("10")) { // Constant not available in latest ECJ version that runs on // Java 6. // This is checked against the actual version below. settings.put(CompilerOptions.OPTION_TargetPlatform, "10"); settings.put(CompilerOptions.OPTION_Compliance, "10"); } else if(opt.equals("11")) { // Constant not available in latest ECJ version that runs on // Java 6. // This is checked against the actual version below. settings.put(CompilerOptions.OPTION_TargetPlatform, "11"); settings.put(CompilerOptions.OPTION_Compliance, "11"); } else if(opt.equals("12")) { // Constant not available in latest available ECJ version. // May be supported in a snapshot build. // This is checked against the actual version below. settings.put(CompilerOptions.OPTION_TargetPlatform, "12"); settings.put(CompilerOptions.OPTION_Compliance, "12"); } else if(opt.equals("13")) { // Constant not available in latest available ECJ version. // May be supported in a snapshot build. // This is checked against the actual version below. settings.put(CompilerOptions.OPTION_TargetPlatform, "13"); settings.put(CompilerOptions.OPTION_Compliance, "13"); } else { log.warn(Localizer.getMessage("jsp.warning.unknown.targetVM", opt)); settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_6); } } else { // Default to 1.6 settings.put(CompilerOptions.OPTION_TargetPlatform, ... CompilerOptions.VERSION_1_6); settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_1_6); } final IProblemFactory problemFactory = new DefaultProblemFactory(Locale.getDefault()); final ICompilerRequestor requestor = new ICompilerRequestor() { @Override public void acceptResult(CompilationResult result) { try { if (result.hasProblems()) { IProblem[] problems = result.getProblems(); for (int i = 0; i < problems.length; i++) { IProblem problem = problems[i]; if (problem.isError()) { String name = new String(problems[i].getOriginatingFileName()); try { problemList.add(ErrorDispatcher.createJavacError (name, pageNodes, new StringBuilder(problem.getMessage()), problem.getSourceLineNumber(), ctxt)); } catch (JasperException e) { log.error("Error visiting node", e); } } } } if (problemList.isEmpty()) { ClassFile[] classFiles = result.getClassFiles(); for (int i = 0; i < classFiles.length; i++) { ClassFile classFile = classFiles[i]; char[][] compoundName = classFile.getCompoundName(); StringBuilder classFileName = new StringBuilder(outputDir).append('/'); for (int j = 0; j < compoundName.length; j++) { if(j > 0) { classFileName.append('/'); } classFileName.append(compoundName[j]); } byte[] bytes = classFile.getBytes(); classFileName.append(".class"); FileOutputStream fout = null; BufferedOutputStream bos = null; try { fout = new FileOutputStream(classFileName.toString()); bos = new BufferedOutputStream(fout); bos.write(bytes); } finally { if (bos != null) { try { bos.close(); } catch (IOException e) { } } } } } } catch (IOException exc) { log.error("Compilation error", exc); } } }; ICompilationUnit[] compilationUnits = new ICompilationUnit[classNames.length]; for (int i = 0; i < compilationUnits.length; i++) { String className = classNames[i]; compilationUnits[i] = new CompilationUnit(fileNames[i], className); } CompilerOptions cOptions = new CompilerOptions(settings); // Check source/target JDK versions as the newest versions are allowed // in Tomcat configuration but may not be supported by the ECJ version // being used. String requestedSource = ctxt.getOptions().getCompilerSourceVM(); if (requestedSource != null) { String actualSource = CompilerOptions.versionFromJdkLevel(cOptions.sourceLevel); if (!requestedSource.equals(actualSource)) { log.warn(Localizer.getMessage("jsp.warning.unsupported.sourceVM", requestedSource, actualSource)); } } String requestedTarget = ctxt.getOptions().getCompilerTargetVM(); if (requestedTarget != null) { String actualTarget = CompilerOptions.versionFromJdkLevel(cOptions.targetJDK); if (!requestedTarget.equals(actualTarget)) { log.warn(Localizer.getMessage("jsp.warning.unsupported.targetVM", requestedTarget, actualTarget)); } } cOptions.parseLiteralExpressionsAsConstants = true; Compiler compiler = new Compiler(env, policy, cOptions, requestor, problemFactory); compiler.compile(compilationUnits); if (!ctxt.keepGenerated()) { File javaFile = new File(ctxt.getServletJavaFileName()); javaFile.delete(); } if (!problemList.isEmpty()) { JavacErrorDetail[] jeds = problemList.toArray(new JavacErrorDetail[0]); errDispatcher.javacError(jeds); } if( log.isDebugEnabled() ) { long t2=System.currentTimeMillis(); log.debug("Compiled " + ctxt.getServletJavaFileName() + " " + (t2-t1) + "ms"); } if (ctxt.isPrototypeMode()) { return; } // JSR45 Support if (! options.isSmapSuppressed()) { SmapUtil.installSmap(smap); } } }
为了有助于理解,我们根据构造函数的参数依次看它的作用。
- INameEnvironment 接口,它需要实现的主要方法是findType和isPackage ,FindType有助于JDT 找到相应的Java源文件或者Class字节码,根据传进来的包名和类名去寻找,例如,传入了java.lang.String或org.apache.jsp.HelloWorld_jsp ,则分别找到JDK自带的String字节码及Tomcat 中编译的HelloWorld_jsp.java文件,接着,按要求封装这些对象,返回JDT 规定的NameEnvironmentAnswer对象,而isPackage则提供是否是包的判断 。
- IErrorHandlingPolicy 接口, 用于描述错误策略, 可直接使用DefaultErrorHandlingPolicies.exitOnFirstError(),如果表示第一个错误,就退出编译。
- CompilerOptions对象,指定编译时的一些参数,例如,这里指定的编译的java版本为1.7
- ICompilerRequestor接口,它只有一个acceptResult方法,这个方法用于处理编译后的结果,如果包含了错误信息,则抛出异常, 否则,把编译成功的字节码写到指定的路径的HelloWorld_jsp.class文件中,即生成字节码 。
- IProblemFactory接口, 主要用于控制编译错误的信息格式 。
所有Compiler构造函数需要的参数对象都已经准备后, 传入这些参数后创建一个Compiler对象,然后调用compile方法即可对指定的Java文件进行编译,这里完成了HelloWorld_jsp.java的编译, 结果生成了HelloWorld_jsp.class字节码, 实际上,Tomcat中基本上也这样使用了JDT 实现Servlet的编译,但它使用的某些策略可能不同,例如,使用DefaultErrorHandlingPolicies.proceedWithAllProblems()作为错误策略。
至此,我们已经清楚Tomcat 对JSP编译处理的整个过程了。 它首先根据JSP语法解析生成类似xxx.java的Servlet ,然后再通过Eclipse JDT 对xxx.java编译,最后生成 JVM 能识别的Class字节码 。
首先,需要一个后台执行线程,Tomcat 中有专门的一条线程负责处理不同的容器的后台任务,要在不同的容器中执行某些后台任务 , 只需要重写backgroudProcess方法即可实现, 由于JSPServlet对应于Wrapper 级别,因此要在StandardWrapper重写backgroundProcess,它会调用实现了PeriodicEventListener接口的Servlet ,其中,JspServlet就实现了PeriodicEventListener 接口,此接口只有一个periodicEvent方法,具体的检测逻辑在此方法中实现即可。
其次,判断重新编译的根据是什么?重新编译就是再次把JSP变成java再变成Class,而触发这个动作的条件就是,当我们修改JSP文件后, 或者JSP文件引入的资源被修改后, 所以最好判断的依据就是某个JSP或资源的最后修改时间-LastModified属性, 正常顺序是JSP经过编译后生成的Class文件,把此class文件的LastModified 属性值决定是否重新编译,重新编译后,JSP 与Class 文件的LastModified属性再次设置为相同的值,对于引入的资源,内存中维护了上次编译时引入的资源的LastModified属性再次设置相同的值,不断的获取引入的资源LastModified属性并与内存中对应的LastModified属性进行比较,同样可以很容易的判断是否需要重新编译 。
最后,对于本地和远程资源分别如何检测? 对于本地资源来说,使用java.io.File类可以很方便的实现对某JSP文件或其他文件的LastModified属性读取,对于远程资源,比如jar包,为了方便处理Jar 包含的属性,使用java.net.URL可以很方便的操作,它包含了很多协议,它包含了很多的协议,例如 ,常见的Jar , File , Ftp 等协议,使用它相当的方便 。
URL includeUrl = new URL(“jar:http://hostname/third.jar!/”);
URLConnection iuc = includeUrl.openConnection();
long includeLastModified = ((JarURLConnection)iuc).getJarEntry().getTime();
如前所述, 只需要三步可以远程对远程Jar 包的读取出最后修改时间,当然,URL 还支持本地文件资源的读取,所以,它是很好的资源读取抽象对象, Tomcat 中对引入的资源的管理都使用URL作为操作对象 。
本节探讨了Jasper自动检测机制的实现, 自动检测机制给我们开发带来了很好的检验,我们不必修改了JSP 后自动执行编译操作,而由Tomcat 通过Jasper帮我们检测编译操作。
关于编译代码这一块,我不想说太多,代码我也不太懂,为了更方便理解编译这一块的原理,我们来看一个例子。
public class MyJDTCompiler { private static final String JDT_JAVA_9_VERSION; static { // The constant for Java 9 changed between 4.6 and 4.7 in a way that is // not backwards compatible. Need to figure out which version is in use // so the correct constant value is used. String jdtJava9Version = null; Class<?> clazz = CompilerOptions.class; for (Field field : clazz.getFields()) { if ("VERSION_9".equals(field.getName())) { // 4.7 onwards: CompilerOptions.VERSION_9 jdtJava9Version = "9"; break; } } if (jdtJava9Version == null) { // 4.6 and earlier: CompilerOptions.VERSION_1_9 jdtJava9Version = "1.9"; } JDT_JAVA_9_VERSION = jdtJava9Version; } private final Log log = LogFactory.getLog(MyJDTCompiler.class); // must not be static /** * Compile the servlet from .java file to .class file */ public void generateClass( final String sourceFile ,final String outputDir, String packageName , String outclassName , final ClassLoader classLoader ) throws FileNotFoundException, JasperException, Exception { long t1 = 0; if (log.isDebugEnabled()) { t1 = System.currentTimeMillis(); } final String targetClassName = ((packageName.length() != 0) ? (packageName + ".") : "") + outclassName; String[] fileNames = new String[] {sourceFile}; String[] classNames = new String[] {targetClassName}; final ArrayList<JavacErrorDetail> problemList = new ArrayList<JavacErrorDetail>(); class CompilationUnit implements ICompilationUnit { private final String className; private final String sourceFile; CompilationUnit(String sourceFile, String className) { this.className = className; this.sourceFile = sourceFile; } @Override public char[] getFileName() { return sourceFile.toCharArray(); } @Override public char[] getContents() { char[] result = null; FileInputStream is = null; InputStreamReader isr = null; Reader reader = null; try { is = new FileInputStream(sourceFile); isr = new InputStreamReader(is, "UTF-8"); reader = new BufferedReader(isr); char[] chars = new char[8192]; StringBuilder buf = new StringBuilder(); int count; while ((count = reader.read(chars, 0, chars.length)) > 0) { buf.append(chars, 0, count); } result = new char[buf.length()]; buf.getChars(0, result.length, result, 0); } catch (IOException e) { log.error("Compilation error", e); } finally { if (reader != null) { try { reader.close(); } catch (IOException ioe) {/*Ignore*/} } if (isr != null) { try { isr.close(); } catch (IOException ioe) {/*Ignore*/} } if (is != null) { try { is.close(); } catch (IOException exc) {/*Ignore*/} } } return result; } @Override public char[] getMainTypeName() { int dot = className.lastIndexOf('.'); if (dot > 0) { return className.substring(dot + 1).toCharArray(); } return className.toCharArray(); } @Override public char[][] getPackageName() { StringTokenizer izer = new StringTokenizer(className, "."); char[][] result = new char[izer.countTokens()-1][]; for (int i = 0; i < result.length; i++) { String tok = izer.nextToken(); result[i] = tok.toCharArray(); } return result; } @Override public boolean ignoreOptionalProblems() { return false; } } final INameEnvironment env = new INameEnvironment() { @Override public NameEnvironmentAnswer findType(char[][] compoundTypeName) { StringBuilder result = new StringBuilder(); String sep = ""; for (int i = 0; i < compoundTypeName.length; i++) { result.append(sep); result.append(compoundTypeName[i]); sep = "."; } return findType(result.toString()); } @Override public NameEnvironmentAnswer findType(char[] typeName, char[][] packageName) { StringBuilder result = new StringBuilder(); String sep = ""; for (int i = 0; i < packageName.length; i++) { result.append(sep); result.append(packageName[i]); sep = "."; } result.append(sep); result.append(typeName); return findType(result.toString()); } private NameEnvironmentAnswer findType(String className) { InputStream is = null; try { if (className.equals(targetClassName)) { ICompilationUnit compilationUnit = new CompilationUnit(sourceFile, className); return new NameEnvironmentAnswer(compilationUnit, null); } String resourceName = className.replace('.', '/') + ".class"; is = classLoader.getResourceAsStream(resourceName); if (is != null) { byte[] classBytes; byte[] buf = new byte[8192]; ByteArrayOutputStream baos = new ByteArrayOutputStream(buf.length); int count; while ((count = is.read(buf, 0, buf.length)) > 0) { baos.write(buf, 0, count); } baos.flush(); classBytes = baos.toByteArray(); char[] fileName = className.toCharArray(); ClassFileReader classFileReader = new ClassFileReader(classBytes, fileName, true); return new NameEnvironmentAnswer(classFileReader, null); } } catch (IOException exc) { log.error("Compilation error", exc); } catch (org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException exc) { log.error("Compilation error", exc); } finally { if (is != null) { try { is.close(); } catch (IOException exc) { // Ignore } } } return null; } private boolean isPackage(String result) { if (result.equals(targetClassName)) { return false; } String resourceName = result.replace('.', '/') + ".class"; InputStream is = null; try { is = classLoader.getResourceAsStream(resourceName); return is == null; } finally { if (is != null) { try { is.close(); } catch (IOException e) { } } } } @Override public boolean isPackage(char[][] parentPackageName, char[] packageName) { StringBuilder result = new StringBuilder(); String sep = ""; if (parentPackageName != null) { for (int i = 0; i < parentPackageName.length; i++) { result.append(sep); result.append(parentPackageName[i]); sep = "."; } } if (Character.isUpperCase(packageName[0])) { if (!isPackage(result.toString())) { return false; } } result.append(sep); result.append(packageName); return isPackage(result.toString()); } @Override public void cleanup() { } }; final IErrorHandlingPolicy policy = DefaultErrorHandlingPolicies.proceedWithAllProblems(); final Map<String,String> settings = new HashMap<String,String>(); settings.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.GENERATE); settings.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.GENERATE); settings.put(CompilerOptions.OPTION_ReportDeprecation, CompilerOptions.IGNORE); settings.put(CompilerOptions.OPTION_Encoding, "UTF-8"); settings.put(CompilerOptions.OPTION_LocalVariableAttribute, CompilerOptions.GENERATE); settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_8); settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_1_8); settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_1_8); final IProblemFactory problemFactory = new DefaultProblemFactory(Locale.getDefault()); final ICompilerRequestor requestor = new ICompilerRequestor() { @Override public void acceptResult(CompilationResult result) { try { if (result.hasProblems()) { IProblem[] problems = result.getProblems(); for (int i = 0; i < problems.length; i++) { IProblem problem = problems[i]; if (problem.isError()) { String name = new String(problems[i].getOriginatingFileName()); try { problemList.add(null); } catch (Exception e) { log.error("Error visiting node", e); } } } } if (problemList.isEmpty()) { ClassFile[] classFiles = result.getClassFiles(); for (int i = 0; i < classFiles.length; i++) { ClassFile classFile = classFiles[i]; char[][] compoundName = classFile.getCompoundName(); StringBuilder classFileName = new StringBuilder(outputDir).append('/'); for (int j = 0; j < compoundName.length; j++) { if(j > 0) { classFileName.append('/'); } classFileName.append(compoundName[j]); } byte[] bytes = classFile.getBytes(); classFileName.append(".class"); FileOutputStream fout = null; BufferedOutputStream bos = null; try { fout = new FileOutputStream(classFileName.toString()); bos = new BufferedOutputStream(fout); bos.write(bytes); } finally { if (bos != null) { try { bos.close(); } catch (IOException e) { } } } } } } catch (IOException exc) { log.error("Compilation error", exc); } } }; ICompilationUnit[] compilationUnits = new ICompilationUnit[classNames.length]; for (int i = 0; i < compilationUnits.length; i++) { String className = classNames[i]; compilationUnits[i] = new CompilationUnit(fileNames[i], className); } CompilerOptions cOptions = new CompilerOptions(settings); cOptions.parseLiteralExpressionsAsConstants = true; Compiler compiler = new Compiler(env, policy, cOptions, requestor, problemFactory); compiler.compile(compilationUnits); } }
上面加粗代码的调用,就能将.java文件编译成.class文件。 而修改了generateClass()方法传入了4个参数 ,public void generateClass( final String sourceFile ,final String outputDir, String packageName
, String outclassName , final ClassLoader classLoader ),这4个参数分别是
- sourceFile :源.java文件名称及路径
- outputDir :生成的class文件目录
- packageName : .java文件包名
- ReflictInvoke :源文件名称
- classLoader :类加载器
接下来看一个例子。
public class MyCompilerTest { public static void main(String[] args) throws Exception { String content = "package com.luban.compilerx;\n" + "\n" + "\n" + "\n" + "\n" + "public class ReflictInvoke extends BaseInvoke {\n" + "\n" + " public void invoke(){\n" + " System.out.println(\"ReflictInvoke调用11111111\");\n" + " }\n" + "}\n"; // 如果文件不存在 ,则创建文件 File file = new File("/Users/quyixiao/gitlab/tomcat/output/production/tomcat/com/luban/compilerx"); if (!file.exists()) { file.mkdirs(); } String sorceFile = "/Users/quyixiao/gitlab/tomcat/output/production/tomcat/com/luban/compilerx/ReflictInvoke.java"; FileOutputStream fos = new FileOutputStream(sorceFile); Writer out = new OutputStreamWriter(fos, "UTF-8"); out.write(content); out.close(); fos.close(); String outputDir = "/Users/quyixiao/gitlab/tomcat/output/production/tomcat"; String packageName = "com.luban.compilerx"; String targetClassName = "ReflictInvoke"; MyJDTCompiler myJDTCompiler = new MyJDTCompiler(); ClassLoader classLoader = MyCompilerTest.class.getClassLoader(); myJDTCompiler.generateClass(sorceFile, outputDir, packageName, targetClassName, classLoader); Class<?> clazz = classLoader.loadClass("com.luban.compilerx.ReflictInvoke"); BaseInvoke c = (BaseInvoke) clazz.newInstance(); c.invoke(); } }
这个例子的原理很简单,就是想动态的创建一个类ReflictInvoke
package com.luban.compilerx; public class ReflictInvoke extends BaseInvoke { public void invoke(){ System.out.println("ReflictInvoke调用11111111"); } }
调用compiler.compile(compilationUnits);将java文件编译为.class文件,再通过Class<?> clazz = classLoader.loadClass(“com.luban.compilerx.ReflictInvoke”); 加载到虚拟机,再实例化它,最终调用其invoke()方法,这门技术,我觉得很牛逼,这样,我们可以在运行时创建一个类,并调用它,很多的框架都用到了这项技术,如Spring的CGLIB 代理,不就是运行时动态的修改原来类的代码结构,再编译它,最终再加载到虚拟机,替换掉原来的类,话不多说,看一下执行效果 。
看到没有,简单的一个类,就实现了java文件动态生成,编译,及使用。
之前已经分析到jsp文件的编译了, 接下来,看jsp如何获取实例对象 。
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); } }
生成的_jsp.java 又是如何调用及执行的呢?重点就是上面两行加粗代码 ,先来看servlet = getServlet(); 这一行代码 。
public Servlet getServlet() throws ServletException { /* * DCL on 'reload' requires that 'reload' be volatile * (this also forces a read memory barrier, ensuring the new servlet * object is read consistently). * * When running in non development mode with a checkInterval it is * possible (see BZ 62603) for a race condition to cause failures * if a Servlet or tag is reloaded while a compile check is running */ if (getReloadInternal() || theServlet == null) { synchronized (this) { // Synchronizing on jsw enables simultaneous loading // of different pages, but not the same page. if (getReloadInternal() || theServlet == null) { // This is to maintain the original protocol. destroy(); final Servlet servlet; try { InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(config); // 实例化对象 servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader()); } catch (Exception e) { Throwable t = ExceptionUtils .unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(t); throw new JasperException(t); } servlet.init(config); if (theServlet != null) { ctxt.getRuntimeContext().incrementJspReloadCount(); } theServlet = servlet; reload = false; // Volatile 'reload' forces in order write of 'theServlet' and new servlet object } } } return theServlet; } public Object newInstance(final String className, final ClassLoader classLoader) throws IllegalAccessException, NamingException, InvocationTargetException, InstantiationException, ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException { // loadClass()方法加载类 Class<?> clazz = classLoader.loadClass(className); return newInstance(clazz.getDeclaredConstructor().newInstance(), clazz); }
上面代码原理还是很简单就,先加载我们生成的index_jsp.class类,先调用其init()方法。
public final void init(ServletConfig config) throws ServletException { super.init(config); jspInit(); _jspInit(); }
看到没有,调用_jspInit()方法,不就是生成index_jsp.java的_jspInit() 方法不?
同理,来看 servlet.service(request, response);方法调用,最终也是调用了_jspService(request,response);
不就对应index_jsp.java 的 _jspService()方法不?如下图所示 。
总结:
文章分享到这里也告一段落了, 因为篇幅过长,所以分为Tomcat 源码解析一JSP编译器Jasper-佛怒火莲(上) , Tomcat 源码解析一JSP编译器Jasper-佛怒火莲(下)两部分,前部分主要讲jsp 解析相关内容,后部分主要讲tomcat如何修改字节码,编译,及获取生成类实例。 可以涉及到的内容及范围也比较广,当然,你觉得关于jsp相关源码就完了吗?肯定没有,如果你想了解EL表达式如何解析,表达式替换,还需要看Tomcat 源码解析一EL表达式源码解析 这篇博客 ,内部细节实现也是很复杂的,当然,我也尽量的将所见到的源码分析清楚,希望这对大家有所帮助。由于我比较喜欢斗破苍穹,因此后面的博客中,每一篇博客可能以斗破苍穹中的一种斗技作为后缀, 话不多说,就到这里了,下一篇博客见。
本文相关的源码地址
https://github.com/quyixiao/tomcat.git
https://github.com/quyixiao/JspReader.git