前言
程序员看到异常就好像看到天敌一样,要么解决掉,要么把它隐藏掉(苦笑~),哪怕有些异常并不影响业务,或只是警告信息。
最近就遇到一个启动Spring Boot项目时,抛出一堆包含Exception的警告信息,于是每天心心念念的事就是干掉它。
今天终于有时间来解决了,但发现几乎搜遍全网只找到类似的问题,却没有找到对应的解决方案。于是拿起debug利器,分析了一波源码,还真找到原因了。
或许你不会在项目中遇到类似的问题,但该问题的解决思路一定会对你有所启发,不信你读完该篇文章试试。
异常现状
启动Spring Boot项目时会抛出如下异常:
2020-10-27 19:20:33.706 WARN 13099 --- [ main] o.a.tomcat.util.scan.StandardJarScanner : Failed to scan [file:/Users/zzs/.m2/repository/com/sun/xml/bind/jaxb-impl/2.1/jaxb-api.jar] from classloader hierarchy java.io.FileNotFoundException: /Users/zzs/.m2/repository/com/sun/xml/bind/jaxb-impl/2.1/jaxb-api.jar (No such file or directory) // ... 2020-10-27 19:20:33.708 WARN 13099 --- [ main] o.a.tomcat.util.scan.StandardJarScanner : Failed to scan [file:/Users/zzs/.m2/repository/com/sun/xml/bind/jaxb-impl/2.1/activation.jar] from classloader hierarchy java.io.FileNotFoundException: /Users/zzs/.m2/repository/com/sun/xml/bind/jaxb-impl/2.1/activation.jar (No such file or directory)
总之是一堆堆的异常信息,虽然日志级别为WARN,实在看不下去。
在网上也有大量的类似问题,比如:
[WARNING] [path] bad path element "/home/fprochazka/.m2/repository/org/glassfish/jaxb/jaxb-runtime/2.3.2/jakarta.xml.bind-api-2.3.2.jar": no such file or directory
再比如:
Caused by: java.io.FileNotFoundException: /usr/local/tomcat/webapps/u-plan/WEB-INF/lib/activation-1.1.1.jar (No such file or directory)
等等,各类相关的问题。但统一的结果都是没准确的答案,或治标不治本,或临时解决但不知道最终原因的答案。
那么,我们就从根本上来分析一下导致问题的原因。
原因追踪
首先,从打印的堆栈(片段)信息来看:
at java.util.jar.JarFile.<init>(JarFile.java:130) at org.apache.tomcat.util.compat.JreCompat.jarFileNewInstance(JreCompat.java:164) at org.apache.tomcat.util.scan.JarFileUrlJar.<init>(JarFileUrlJar.java:65) at org.apache.tomcat.util.scan.JarFactory.newInstance(JarFactory.java:49) at org.apache.tomcat.util.scan.StandardJarScanner.process(StandardJarScanner.java:374) at org.apache.tomcat.util.scan.StandardJarScanner.processURLs(StandardJarScanner.java:309) at org.apache.tomcat.util.scan.StandardJarScanner.doScanClassPath(StandardJarScanner.java:266) at org.apache.tomcat.util.scan.StandardJarScanner.scan(StandardJarScanner.java:229)
从上面我们可以看出,其实出问题就出在tomcat进行jar文件加载时找不到对应的文件目录。
下面亮出神器——debug。先把断点打在StandardJarScanner#doScanClassPath方法当中。当浏览完整个方法,你会发现核心的代码就在此处两行,第一个addAll方法就是将找到的URL路径添加到Deque<URL>队列当中。然后processURLs对队列当中的URL再进一步的处理。
让项目启动起来,先看看获取到的URL都是什么。第一次循环时为空,第二次循环时便有值了。我这里有160个jar文件,我们看jar文件的路径会发现就是JDK自身的jar包和pom文件中配置的依赖jar包。
也就是说,tomcat在启动时会把所需jar包进行扫描加载。那么单单就加载这160个jar吗?并不是,继续看看processURLs方法。processURLs方法会将队列当中的URL遍历处理,处理的结果放到processedURLs集合(Set)当中。看名字就知道是处理过的URL集合。
同时,还会调用一个process方法,重点来了,注意听讲。先看process方法核心实现:看到什么了?也就是说如果URL对应的资源是一个jar文件,或者以.jar结尾,那么就通过JarFactory创建一个Jar对象。其实前面抛异常便是创建对象时,对应的jar不存在导致的。
但此时是第一层循环,所以不存在JarFactory创建抛异常的问题,问题在processManifest方法内。processManifest方法做了几个重要的操作:1、获取当前jar包的MANIFEST.MF文件内容;2、获取MANIFEST.MF文件中的Class-Path属性,并解析属性;3、获取前jar的路径;4、以当前jar包的路径拼接Class-Path中的jar包路径,并把结果放到前面提到的classPathUrlsToProcess队列中。
以项目中出现异常的jar包依赖为例:为什么加载不到?注意上面我们说的processManifest方法的第3、4条,以当前jar包的路径来拼接Class-Path的jar包路径。
当前jar包路径为:com/sun/xml/bind/jaxb-impl,然后添加上jaxb-api.jar,路径变为com/sun/xml/bind/jaxb-impl/xx/jaxb-api.jar。其中中间的xx为版本号。
那么再看依赖文件中引入的jaxb-api.jar的路径应该是什么:/javax/xml/bind/xx/jaxb-api.jar。
此时当然找不到对应的jar包了,路径完全错了嘛。
原因汇总
其实整个异常的问题就出在tomcat加载classpath中的jar包时,把jar包中Manifest文件中配置的Class-Path的jar包也给加载了。
加载倒是没问题,但它加载时还把路径给拼错了,坑不坑?
由于此处的拼接,会出现多种错误:要么路径直接错了;要么路径上没有版本号;要么jar包上没有版本号(即文件名错误)……
解决方案
既然找到问题了,那么就是思考如何解决了。
第一种方案:既然MANIFEST.MF文件中指定的jar包加载错误并不影响程序运行,那就把它删掉吧。但遇到像本例中依赖的是aliyun的jar包,那就有点坑了。除非自己改它的代码,并上传的一个私服上。并不太可行,而且还会出现不知情的其他错误。
第二种方案:既然知道它要找那个路径了,那就在对应的路径上放置对应的jar包不就可以了?其实也有问题,因为其他依赖已经引入了jar包,而再通过其他路径引入一份,双方jar包存在,不仅增加的了发布文件的大小,还会引起jar包冲突的问题。
第三种方案:此异常是由于tomcat高版本导致的,使用8.5.0 或以下版本,也可以规避此问题。
第四种方案:指定不扫描Manifest文件。以Spring Boot为例,在启动类中配置如下配置:
@Bean public TomcatServletWebServerFactory tomcatFactory() { return new TomcatServletWebServerFactory() { @Override protected void postProcessContext(Context context) { ((StandardJarScanner) context.getJarScanner()).setScanManifest(false); } }; }
等于是重新构建了一个TomcatServletWebServerFactory,在构建的过程中指定不扫描Manifest文件。但此种方案,也可能会有隐患,就是如果Manifest文件中的路径万一正确了呢!
哈哈,其实别痴心妄想了~看上面的拼接方式正确的概率不会太大。
其实呢,还有第五种方案,只要你能忍受,那么就看着WARN警告吧,就当什么都看不到……
小结一下
通过一路debug,一路狂分析,终于找到问题原因。那么解决问题,其实就很简单了。
读到这里最起码你再遇到该问题时,已经知道基本的解决思路了。
但你get到如何解决一个全网都很难找到答案的bug了吗?赶紧找个bug练练手吧。