写完上一篇文章《Maven打包SpringBoot项目,排除第三方依赖包》之后,我在测试项目中已经完全测试通过了。今天,在运行到实际的项目过程中,还是遇到了意外的情况。
一开始,我本来打算把打好的war包部署到Linux服务器上运行的。但是,考虑到连接数据库,必须通过VPN。我又搜索了一下怎么在Linux系统上安装使用VPN。安装了一下,过程中就报错了。我考虑不能在这上面浪费掉太多的时间。
所以,迅速调整了策略。打算还是把war包部署到Windows机器上。我把war包和第三方的依赖包都拷贝到了相应的位置。但是,意外又来临了。启动Tomcat,总是闪退。这个问题,我曾经是遇到过的。就是系统环境变量配置的问题。一番配置之后,依然不能解决问题。我又怀疑,是不是我的war包的问题。重复打包几次之后,问题依然如故。我突然想到,应该尝试去看一下Tomcat的运行日志。一看,原来是Tomcat的配置文件的问题。原来,这个Tomcat一直被我集成到IDE中来使用,难免会被IDE改动一些配置。所以,从此之后,就要长记性。单独用Tomcat中测试项目,就要用一个完全独立的Tomcat,而不要用集成到IDE中的Tomcat。
解决这个问题,浪费了不少的时间。解决完这个问题之后,Tomcat终于可以正常运行了。本来想想到此为止。但是,考虑到验证项目正常访问,还是很有必要的。所以,我尝试访问了项目的首页。这一下,又出现了一个意外。首页登陆跳转到了一个未知的路径。
事到如今,真可谓是“一波三折”,我也到了崩溃的边缘。原本想,有了上一篇文章所做的基础,今天可以说是手到擒来。然而,意外,意外,一个接着一个的意外,让人猝不及防。看看时间,也在公司耗费了一个下午。还能怎么办呢?吃个饭,继续解决问题。
前面只是开胃小点心,下面才开始正餐。
接着来说上面的问题。既然登陆跳转到一个未知的路径。根据分析后端的业务逻辑,权限登陆成功之后,要读取配置的路径,然后,浏览器跳转到这个路径。那么,现在的问题就变成了:项目没有读取到我的配置信息。
项目是调用了权限jar包中的代码来读取配置信息的。代码是:Auto.class.getClassLoader().getResourceAsStream("a.txt");
,代码逻辑是这样的,通过当前类的类加载器ClassLoader来查找根目录下的配置文件。此时,类加载器没有找到我的配置文件,但是,找到了自身jar包内的配置文件。果然,跳转的那个未知路径,正是这个文件中的配置信息。
我这里要说句题外话,这个问题也是以前开发的过程中遇到的,只是一直没有去思考和解决。那就是,jar 包内类加载器在查找文件的时候,这个文件如果路径和名称都完全相同,那么jar包外和jar包内的文件,哪个优先级更高呢?实际运行结果是,jar包外的优先级更高,但是,我还没有找到一个合理的解释。
在servlet-4_0_FINAL规范的10.5 Directory Structure中,If an index.html is present both in the root context and in the META-INF/resources directory of a JAR file in the WEB-INF/lib directory of the application, then the file that is available in the root context MUST be used.
,意思就是index.html同时在项目根路径下、在WEB-INF/lib目录下的jar包内的META-INF/resources的目录下,那么必须使用根目录下的index.html。由此规定可以推知,在WEB-INF/lib中的jar包和根路径下同时拥有相同的文件,那么,就使用根路径下的文件。
接着上面的问题,既然类加载器没有读取到我的配置信息,那说明一个问题,类加载器发生了变化。产生这样一个变化的原因就是,我把第三方依赖包和项目包分离部署了。然后,我进行了如下的配置:
在tomcat/lib目录下建一个ext目录,来将项目要用到的第三方依赖包放在此处。修改tomcat配置文件${catalina.home}/conf/catalina.properties中的 common.loader值,加上${catalina.home}/lib/ext/*.jar,完成此步骤后,项目启动便可以使用到lib/ext里面的jar包了。
我在做这个配置的时候,本身就持有怀疑态度。我自己的原则是,分离第三方依赖,减小项目包大小。部署项目的时候,我希望尽量和以前一样,项目启动正常找到第三方依赖包即可。但是,上面的配置明显将我这个想法扩大化了。common.loader
配置以后,将我本项目的第三方依赖全部共享出去了,让其他项目甚至服务器都能共享,这不是我想要的。即使这些依赖包是可以共享的,我也不想这样做。因为,这显然与我的初衷背道而驰了,我只是想做依赖包分离,好家伙,后续的影响一步步扩大了。开始的时候,网上都是这样的配置,我内心虽然反感,但是,也没有其他好的办法,只能将就着用。
现在问题暴露出来了,就不能还这样配置了。我的直觉就是,既然项目默认是从/WEB-INF/lib/
目录下查找的第三方依赖包。那么能不能修改一下这个默认路径,让项目去我指定的目录下寻找,这就可以轻而易举的解决问题了。
如何修改这个默认路径?我也是大费了一番周章。虽然,还没有找到答案,但是,我还是先整理了一个解题思路图:
分析一下上面的思路图:
- 第三步是部署到tomcat中运行,如果走到了这一步,就已经回天乏术了,我们就没有修改的时机了。
- 第二步是spring-boot-maven-plugin插件repackage之后,生成了一个符合spring boot项目的war包。这个war包和以前标准结构的war包是不一样的。在spring boot war 包中,
/META-INF/MANIFEST.MF
文件中有一行配置Spring-Boot-Lib: WEB-INF/lib/
。我就考虑,能不能把这里的配置值修改一下。经过一番艰苦卓绝的斗争之后,又发现了我的很多知识盲区。我本来以为,只有经过spring-boot-maven-plugin插件repackage之后,war包才能够在Tomcat容器中正常运行,否则,因为spring boot项目中没有web.xml,也没有一个程序入口,项目就不能够正常运行。但是,我看了spring-boot-maven-plugin插件的官方文档,repackage之后,打包形成的war包或者是jar包,是为了能够java -jar
命令来运行。没有经过repackage的war包,一样是可以再Tomcat中运行的。这里的原理是:在Servlet3.0规范推出,可以简化掉web.xml的配置。其中最关键的是定义了javax.servlet.ServletContainerInitializer这个接口,这个接口的实现类会在web容器启动阶段被回调,在onStartUp方法里对servlet、filter和listener进行一些注册的操作来代替web.xml。Tamcat通过Java的SPI(Service Provider Interface)机制,找到spring-web包下面的META-INF\services文件夹,里面有个名字javax.servlet.ServletContainerInitializer的文件,打开文件里面只有一行,那就是spring对这个接口的实现类的全限定名,找到SpringServletContainerInitializer这个类。SpringServletContainerInitializer作为ServletContainerInitializer的实现类,通过SPI机制,在web容器加载的时候会自动的被调用。而这个类的方法又会扫描找到WebApplicationInitializer的实现类,调用它的onStartup方法,从而起到启动web.xml相同的作用。第二步也就这样被报废了。 - 第一步是生成标准格式的war包。查了一波maven-war-plugin插件,也没有发现可以修改
/WEB-INF/lib/
默认路径的配置项。看样子这又是一条死胡同。
经过上面的一波分析,修改一下这个/WEB-INF/lib/
默认路径,让项目去我指定的目录下寻找,这条路算是又被堵死了。[撞墙ing]
那该怎么办呢?看来还是要回到原点。通过配置Tomcat,来让项目找到第三方依赖包,但是,要想办法克服这个配置带来的副作用(类加载器发生了变化)。我在搜索Tomcat如何解析war包的时候,在官网:Application Developer's Guide - Deployment中,有这样一段话,看似无关,却是非常有关系。
翻译一下,大概意思就是:Tomcat可以把第三方依赖放在
$CATALINA_HOME/lib
目录下,可以让Tomcat内部和项目都可以共享这些依赖。比如说,jsp和servlet的jar包,就是通过这种方式实现项目共享的。你看,从这段译文,你能琢磨出什么味儿来?我看这从侧面也说明了
/WEB-INF/lib/
这个默认目录是没办法更改的。所以,Tomcat才会想出这样一个变通的办法来解决查找依赖的问题。另外,他还提到了Tomcat的类加载的机制。是不是思路就像一个圆环,又回到了我们的问题:类加载器发生了变化。真是踏破铁鞋无觅处,得来全不费工夫。找了一大圈,我们现在是回到了真正的起点,为什么Tomcat配置之后,类加载器发生了变化?我们现在就来研究一下其中的原理。官网介绍的页面在:Class Loader How-To。主要说了Tomcat的类加载机制。
看英文有些吃力,先搜搜中文的文章看看。这篇《图解Tomcat类加载机制
》讲得不错。刚好文章里提到我前面插进去的题外话,jar包和项目中加载文件的优先级问题。感觉又是一个回环。
知识点一旦打通,就形成了一张网,从每一个知识点都可以通向网络中任意其他的知识点。
把官方文档都看完了,说白了,因为我对Tomcat的配置,导致了项目的加载和第三方依赖包的加载使用了不同的类加载器。而第三方依赖包的类加载器并不能在类路径中找到我项目的配置信息。
为啥第三方依赖包的类加载器就不能读取我的项目配置信息呢?《类加载器与 Class.getResourceAsStream 问题解决》这篇文章里从底层Java类加载机制开始,到Tomcat类加载机制,最后落脚到getResourceAsStream()方法的源码分析,说明了其中的原因。是相关类加载器在自己的classpath中没有找到配置文件。
绕了一圈,终于把开始的问题解释清楚了。这就像一个死循环,这也像一张大网。我在缝制一张大网,把我所有的知识盲点都补足了,这张知识网络也就缝制成功了。
到了这里,思路扯的太远,我都不知道自己是从哪里出发的了。只能回头去看自己的思路。
修改/WEB-INF/lib/
默认路径,这条路被堵死了。然后,通过配置Tomcat,同时克服由此带来的类加载器变化,这条路也是没有希望了。
接连两条思路碰壁,只剩一条路可走了,那就是把权限jar包保留在/WEB-INF/lib/
,其他第三方依赖包在打war包时排除掉。其实,这是一个简单直接的方案,我之前在验证自己的想法的时候也使用过。只是这个使用这个方法有点违背了我分离第三方依赖包的初衷,我感觉分离的不够彻底。但是,现在看来也是没有办法的办法。说到这里,也很能说明我自己的问题,就是太过于钻牛角尖,凭直觉的方案就可以解决问题,但是,由于自己的完美倾向,就不去认真尝试,非要等到花费大量时间,处处碰壁之后,才回头服软。这么做,固然能够有利于求知,但是,对于完成项目任务来说,就是灾难性的了。
打包时,排除所有第三方依赖包,除了权限jar包。我们需要在pom.xml中进行配置如下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<packagingExcludes>
%regex[WEB-INF/lib/log4j-(?!over-slf4j).*.jar]
</packagingExcludes>
</configuration>
</plugin>
关键点在于其中的正则表达式。关于正则表达式足够开一个系列文章来说了。这里就只研究一下这个业务场景下的用法吧。准确来说就:匹配除了auto.jar以外,其他所有的jar包。
WEB-INF/lib/(?!auto).*.jar
这里用到正则里的零宽度负预测先行断言,语法格式(?!exp)
,意思是:匹配后面跟的不是exp的位置。.*
连在一起就意味着任意数量的不包含换行的字符。
这样写的意思就是:匹配WEB-INF/lib/目录下面不是auto字符串的所有jar包。如果是要保留多个jar的话,就可以这样些:WEB-INF/lib/(?!spring|auto).*.jar
。这样就保留了spring开头的,auto开头的jar包,还可以添加更多的jar包。
这里regexper.com网站能够实时看到正则表达式的执行效果。很不错的。
总结
这次真的应该来做一个总结了。从开始解决问题,遇到了许多的坑,这些也都是我的知识盲点:Java类加载机制、Tomcat类加载机制、Spring boot插件作用和原理、正则表达式。说道这里,本文核心点就是搞明白了Tomcat类加载机制,虽然写文章的初衷是因为Maven打包SpringBoot项目,排除第三方依赖包所引发的问题。所以文章名称,也就由此得来啦。
另外,画了一个解决问题过程的复盘图:
image.png
网友评论