前言

程序员看到异常就好像看到天敌一样,要么解决掉,要么把它隐藏掉(苦笑~),哪怕有些异常并不影响业务,或只是警告信息。

最近就遇到一个启动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方法当中。

image


当浏览完整个方法,你会发现核心的代码就在此处两行,第一个addAll方法就是将找到的URL路径添加到Deque\<URL>队列当中。然后processURLs对队列当中的URL再进一步的处理。

让项目启动起来,先看看获取到的URL都是什么。第一次循环时为空,第二次循环时便有值了。

image

我这里有160个jar文件,我们看jar文件的路径会发现就是JDK自身的jar包和pom文件中配置的依赖jar包。

也就是说,tomcat在启动时会把所需jar包进行扫描加载。那么单单就加载这160个jar吗?并不是,继续看看processURLs方法。

image

processURLs方法会将队列当中的URL遍历处理,处理的结果放到processedURLs集合(Set)当中。看名字就知道是处理过的URL集合。

同时,还会调用一个process方法,重点来了,注意听讲。先看process方法核心实现:

image

看到什么了?也就是说如果URL对应的资源是一个jar文件,或者以.jar结尾,那么就通过JarFactory创建一个Jar对象。其实前面抛异常便是创建对象时,对应的jar不存在导致的。

但此时是第一层循环,所以不存在JarFactory创建抛异常的问题,问题在processManifest方法内。

image

processManifest方法做了几个重要的操作:1、获取当前jar包的MANIFEST.MF文件内容;2、获取MANIFEST.MF文件中的Class-Path属性,并解析属性;3、获取前jar的路径;4、以当前jar包的路径拼接Class-Path中的jar包路径,并把结果放到前面提到的classPathUrlsToProcess队列中。

以项目中出现异常的jar包依赖为例:

image

可以看到对应的依赖jar包的MANIFEST.MF文件中配置了多个jar包。但回头看导入该jar包的pom文件中已经引入了对应的jar包依赖了啊:

image

为什么加载不到?注意上面我们说的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练练手吧。



如何解决一个全网都找不到答案的bug?插图7

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://choupangxia.com/2020/10/27/find-tomcat-bug/