公司开拓了一款Web运用,开拓架构基于Spring Boot,通过jar包的办法发布到做事器并通过命令走运行在内置的Tomcat上。

上线将近一年,统统都是那么的风平浪静,然而统统的沉着被上周的一次现场算法回访冲破。

我们的数据剖析职员本意只是想查看一下历史数据来确认算法的表现符合预期,结果创造历史数据查询页面怎么点都没有反应,而其他页面都是正常的,做事重启后统统规复正常。

jsp报withrootcause一个Slash激发的ClassNotFound血案IT年夜神真实案例复盘请珍藏 Webpack

问题重现

虽然问题通过做事重启后成功办理,但是出错的缘故原由程序员没有定位到也就意味着再次出错的可能性依然存在。

剖析问题最直不雅观的办法便是从缺点出发,通过缺点信息来反向推导缺点发生的场景。
在这个案例中我们查看了浏览器掌握台和后台缺点日志,终极获取了准确的缺点信息:

2019-08-23 14:40:47,835 [http-nio-9090-exec-8] ERROR o.a.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [java.lang.ClassNotFoundException: org.apache.jsp.WEB_002dINF.views.report.report_005fmain_jsp] with root causejava.lang.ClassNotFoundException: org.apache.jsp.WEB_002dINF.views.report.report_005fmain_jsp at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at org.apache.jasper.servlet.JasperLoader.loadClass(JasperLoader.java:129) at org.apache.jasper.servlet.JasperLoader.loadClass(JasperLoader.java:60) at org.apache.catalina.core.DefaultInstanceManager.newInstance(DefaultInstanceManager.java:159) at org.apache.jasper.servlet.JspServletWrapper.getServlet(JspServletWrapper.java:171) at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:380) at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:386) at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:330) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:728) at org.apache.catalina.core.ApplicationDispatcher.proceequest(ApplicationDispatcher.java:470) at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:

这是一个ClassNotFoundException,通过缺点信息我们可以在搜索引擎上找到一堆解答,乃至在Spring Boot的Github上都有类似的情形。

https://github.com/spring-projects/spring-boot/issues/5009

结果总结下来便是:

Spring Boot内置的Tomcat会在系统根目录的/tmp下创建Tomcat开头的临时目录,tmp目录的定时清理会导致部分文件的class文件找不到,

办理办法是指定一个work目录不要利用默认的tmp目录。

听起来很有道理,官方都这么说了那照着做就行了呗。

然而作为一个好奇心爆棚的程序员,这样的阐明显得苍白而无力,但是这个阐明倒是给我们的重现供应了很好的便利,毕竟只有充分重现了这个问题才能更好的去探究深层次的缘故原由。

于是在官方阐明的辅导下,我们进行了多次重现的考试测验,终极将问题范围缩小如下:

在Tomcat启动后将tmp下的ROOT目录删除,访问的第一个页面会涌现无法访问的情形,后台涌现ClassNotFoundException,之后再访问其他页面都是正常。

这里补充一下背景知识:tmp目录在Centos6以及之前是通过TmpWatch的定时任务来定时清理,而Centos7之后直接修正为了systemd-tmpfiles-setup.service,配置文件在/usr/lib/tmpfiles.d/tmp.conf。

问题剖析

在进行问题剖析的时候,我们一样平常会利用三种办法

² 履历法

结合自身的履历来预测问题发生的可能缘故原由,然后通过验证来定位问题详细缘故原由

² 推导法

从问题的发生点开始倒推,沿着问题发生的路径逐步靠近问题的根源

² 剖析法

剖析全体流程中的每一个节点,找到和问题可能干系的节点逐个验证从而找到导致问题的节点

履历法每每是碰着问题时第一个利用的方法,由于面对问题时冲在前面的每每是我们的直觉。

在这个问题中我做了以下预测,并逐一验证

1, Class文件破坏

做出这个假设的依据是,在同一个目录下存在两个页面的Class文件,一个可以访问一个不可以访问。

验证方法也很大略,首先重启做事正常访问页面A获取到正常状态下的A.java和A.class文件;重启做事器后删除ROOT目录,再访问页面A触发缺点,将目录下的java和class文件更换成正常状态的问题;再次访问页面,依然报错。

至此我们推翻了我们关于Class文件破坏的假设。

2, Dev-tool导致ClassLoader不一致

做出这个假设的依据是我们之前碰着的一个dev-tool的问题,Spring Boot在引入了dev-tool后会进行热加载,这时候由于jar包加载和class加载利用了不同的ClassLoader会涌现ClassNotFoundException。

我们之前办理这个问题的方法是去掉dev-tool,同样在这里我们也可以去掉dev-tool再走一遍重现步骤,创造问题依然存在。

至此我们打消了Dev-tool导致ClassLoader不一致的假设。

3, Class文件韶光戳

在我们查看正常文件和非常文件差异的时候创造,正常文件的韶光戳和jar包中的jsp韶光戳同等,而非常文件的韶光戳是当前韶光,那会不会是由于韶光戳不一致导致的呢。

为了验证这个假设我们从两方面入手a) 调度正常文件的韶光戳到当前韶光,结果正常文件依然正常 b) 调度非常文件的韶光戳为jsp的韶光,结果非常文件依然无法访问。

于是我们也打消了Class文件韶光戳的假设。

推导法是比较直不雅观也是可以比较快速的创造问题的方法,但是在我们这个案例中我们创造缺点堆栈中的URLClassLoader并不是问题发生的第一现场,真正的第一现场在java自己的包中,对我们逐步跟踪问题造成了困难。

鉴于此我们选择剖析法作为我们办理问题的打破口。
当然还有一个主要条件支持我们采取剖析法办理问题,那便是在我们这个案例中我们存在OK和NOK两种情形,在每一个剖析的节点我们都可以引入两种情形进行比拟。

在开始之前,由于要每一步比较差异,我们须要配置Eclipse的远程调试。
传送门:

https://www.cnblogs.com/east7/p/10285955.html

首先我们梳理一下Tomcat解析JSP的流程,由于我们基于类来描述流程,以是先罗列一下涉及的类以及紧张的方法:

JspServlet类是主入口,吸收jsp要求;

JspRuntimeContext通过add和get方法来坚持一个ServletWrapper的缓存;

从JspServlet今后是加载的紧张类,而从Compiler今后的类是编译用到的类。

在大致理解了内部类构造后我们可以来看看Jsp加载的流程了,

从图中可以看出我们的报错点在获取Servlet的class这一步,那么我们从页面访问的步骤一步步比较OK和NOK表现的差异。

1, 获取ServletWrapper

这一步的浸染是为每一个Jsp页面构建一个代理并缓存在JspRuntimeContext中,这样每次访问页面直接获取代理即可。
从调试的结果看,构建wrapper的每个参数都是一样,而构建的wrapper结果也是同等的。

2, 编译Java文件

我们把稳到在Complier.class的generateJava这个方法中有一步是:

ctxt.checkOutputDir();

我们的重现正好是删除了ROOT目录,连续进去看代码

public void checkOutputDir() { if (outputDir != null) { if (!(new File(outputDir)).exists()) { makeOutputDir(); } } else { createOutputDir(); }}

由于一开始的outputDir为空会进入createOutputDir方法,

try { File base = options.getScratchDir(); baseUrl = base.toURI().toURL(); 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); }

这里对baseUrl进行赋值,遐想到之前看到一个关于UrlClassPath加载资源的解读,ucp类会根据baseUrl来加载不同的loader进行资源加载。

通过debug我们创造这个地方的baseUrl在OK和NOK两种情形下确实存在差异。

NOK:

baseURL = file:/tmp/tomcat.2612162063177545213.9090/work/Tomcat/localhost/ROOT

OK:

baseURL = file:/tmp/tomcat.2612162063177545213.9090/work/Tomcat/localhost/ROOT/

对照ucp的代码

private Loader getLoader(final URL url) throws IOException { try { return java.security.AccessController.doPrivileged( new java.security.PrivilegedExceptionAction<Loader>() { public Loader run() throws IOException { String file = url.getFile(); if (file != null && file.endsWith(\"大众/\公众)) { if (\"大众file\"大众.equals(url.getProtocol())) { return new FileLoader(url); } else { return new Loader(url); } } else { return new JarLoader(url, jarHandler, lmap, acc); } } }, acc); } catch (java.security.PrivilegedActionException pae) { throw (IOException)pae.getException(); }}

我们创造当涌现”/”的时候我们是通过fileLoader来加载资源,而没有”/”的情形我们默认到jarLoader,用jarLoader去加载一个文件路径当然会返回ClassNotFound了。

至此我们终于将这个问题的来龙去脉理清楚了,那这一个\"大众/\"大众的差异是怎么来的呢,回到那段代码片段:

baseUrl = base.toURI().toURL(); outputDir = base.getAbsolutePath() + File.separator + path + File.separator; if (!makeOutputDir()) {

OK和NOK的情形base是一样的,唯一的差异便是OK的情形文件目录都是存在的,而NOK的时候文件夹是没有的,是不是这种差异导致了一个”/”的差异呢,还是看代码吧:

base.toURI():

public URI toURI() { try { File f = getAbsoluteFile(); String sp = slashify(f.getPath(), f.isDirectory()); if (sp.startsWith(\"大众//\"大众)) sp = \公众//\"大众 + sp; return new URI(\公众file\"大众, null, sp, null); } catch (URISyntaxException x) { throw new Error(x); // Can't happen } }

Slashify():

private static String slashify(String path, boolean isDirectory) { String p = path; if (File.separatorChar != '/') p = p.replace(File.separatorChar, '/'); if (!p.startsWith(\"大众/\"大众)) p = \"大众/\"大众 + p; if (!p.endsWith(\"大众/\"大众) && isDirectory) p = p + \"大众/\"大众; return p;}

从上面的代码可以看出只有知足isDirectory的判断才会给URI加上\"大众/\"大众,在我们NOK的情形下由于文件夹不存在isDirectory返回false不会加上结尾的\"大众/”,导致了baseURI的差异,并终极导致了ClassNotFoundException的生产血案。

总结

在这个案例中我们紧张利用了履历法和剖析法来定位问题,查找本源。

在履历剖析的过程中我们碰着了阻碍,转而通过剖析法分解了Tomcat对付Jsp要求的处理流程。

在剖析Jsp编译过程时创造会对baseURI进行赋值,结合我们已有的对URLClassLoader的加载过程的理解,于是我们对付baseURI的处理进行了着重剖析。

终极创造由于baseURI赋值时系统环境的差异导致了天生的baseURI产生了一个”/”的差异,而这一个差异又导致资源加载的加载器选择差异,终极导致不得当的加载器加载不到资源的缺点。

在问题的办理上我们还是沿用官方的说法,指定一个tmp url用来存放tomcat的临时文件,避免系统做事定时删除。

-Djava.io.tmpdir=/xxx_web_root

©著作权归作者所有:来自51CTO博客作者chellman的原创作品,如需转载,请注明出处,否则将深究法律任务