本文介绍
本文是一篇小记,多图,切勿当作教程看待!
描述
一个SpringMVC
项目,出现了在Tomcat7
下能正常运行,但是使用Tomcat9/8
部署时,会有某个配置文件的属性值读取错误的问题(该属性值是${redis.password}
)。项目的配置结构如下:
配置 | 值 |
---|---|
Spring版本 | 4.0.4.RELEASE |
项目架构 | 多模块项目,domain、service、dao、web分成四个模块,使用mvn install和pom.xml引用依赖的方式进行关联 |
Tomcat版本 | 7.0.94 (能正常运行的Tomcat版本) |
分析
起因
起初项目一直使用Tomcat7
部署,有一次本地调试时用的是Tomcat9
,结果运行时发现配置文件的属性${redis.password}
的值读取错误了,不是项目的配置文件的值。该项目的redis
的配置属性共有3个,包括ip
,port
和password
,但结果只有password
属性的值读取错误。
可能的原因
在检查了项目配置后,发现最重要的一个配置,是Spring
的xml
配置文件中指明了classpath*
,这个表示将扫描classpath
其他jar
包内置的properites
配置文件。
<context:property-placeholder location="classpath*:*.properties" ignore-unresolvable="true"/>
在了解了项目启用加载jar
包内其他配置文件的配置后,接下来可能就是如下几个方向了:
- 项目引用的第三方
jar
包里有配置文件,并且可能是跟我项目的${redis.password}
重名了 - 为什么
Tomcat9
会出现配置值读取错误,而Tomcat7
不会,跟Tomcat
版本有关系吗?
其中第一点马上就得到验证了,是正确的方向,项目引用了一个smart-sso
的单点登录jar
包,里面的确有个service.properties
的配置文件,并且也有个${redis.password}
的属性值,且该属性值就是Tomcat9
部署下读取到的那个值。那现在就只能是分析第2点了。
针对配置文件加载顺序,可以先补充一点,配置文件的加载顺序,在
SpringMVC
的XML
配置里面,是可以通过order
属性来指定加载顺序的,order
值越小,加载优先级别越高
如何用idea调试Tomcat源码
在调试项目代码过程中,发现到一旦运行到Tomcat
部分的代码时,idea
总是突然间就不动了,光标一直在某行中停着,直到step out
几步之后才开始跳到Spring
源码的其他地方。还好idea
给出了提示,的确是运行到Tomcat
的源码部分。(下图红线部分表示idea
运行到了org.apache.catalina.loader
的代码)
用idea
怎么调试Tomcat
源码,其实也挺简单,把相关Tomcat
版本的依赖添加到pom.xml
就好了,如下所示
<!--仅调试使用-->
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
<!-- <dependency>-->
<!-- <groupId>org.apache.tomcat</groupId>-->
<!-- <artifactId>tomcat-catalina</artifactId>-->
<!-- <version>9.0.22</version>-->
<!-- <scope>provided</scope>-->
<!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
<!-- <dependency>-->
<!-- <groupId>org.apache.tomcat</groupId>-->
<!-- <artifactId>tomcat-catalina</artifactId>-->
<!-- <version>8.5.43</version>-->
<!-- <scope>provided</scope>-->
<!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>7.0.94</version>
<scope>provided</scope>
</dependency>
添加后,之后idea
调试到tomcat
源码部分,就可以正常进入了
跟Tomcat版本有什么关系?
首先我建了一个测试项目testProgram
,该项目testProgram
依赖了我的一个jar包,包内有个配置文件app.properties
,里面仅有属性redis.password=newcih
,然后测试项目testProgram
有三个配置文件,分别是jdbc.properties
、jdbd.properties
、z.properties
之所以这样命名,是为了测试配置文件加载顺序是否跟文件名的字典顺序有关,实验证明配置文件的加载顺序跟文件名的字典顺序无关。
为了真实还原现场,调试的代码可能有真实项目中的,也有本测试项目testProgram
中的,同时由于调试的代码包括了Spring
框架和Tomcat
,所以一旦出现源码部分,我会明说是Spring
的或者是Tomcat
的源码。为了缩短篇幅,对于正常的部分,不进行调试说明,如存在多配置文件时,Spring
会逐一加载配置文件,存在重复键值时,则后加载覆盖先加载。
测试操作
移除真实项目本身的配置文件后,发现Tomcat7
启动会报错,显示${redis.password}
无法解析出值,而Tomcat9
则可以正常启动并读取到值。
提出假设
这边假设是Tomcat7
和9
在加载配置文件的实现不同。
查看启动日志
如图所示,通过启动日志可以发现,负责加载配置文件的类是Spring
的PathMatchingResourcePatternResolver
,具体位置是org.springframework.core.io.support
,位于spring-core
包。
下图即类PathMatchingResoucePatternResolver
的关键方法的源码
通过调试Tomcat7
和9
下的运行情况,可以发现,在该方法内,Tomcat7
加载到的资源明显比Tomcat9
要少非常多,可以看的出来,少的那些就是classpath*
模式下应该要加载出来的那些jar
包。如下图所示。
上图是Tomcat7
的运行情况,下图是Tomcat9
的运行情况。
该方法是Spring
框架的源码,所以不应该是Spring
的原因造成Tomcat7
和9
加载资源不同。可以注意到,方法里指明资源是getResources
方法加载出来的,调试进入该方法,还是Spring
的实现,方法实现如下。当资源加载模式是classpath*
时,进入下划线的方法中。下划线的方法实现如第二张图。
下图即为上图下划线方法的具体实现
直到这个Spring
的方法实现出来,我才开始看到Tomcat
相关的东西了。即方法中的getClassLoader()
和cl.getResouces(path)
,这里获取到的类加载器是Tomcat
自实现的WebappClassLoader
(双亲委托机制什么的,就不balabala了。。),这里的类加载器实际上是个抽象类WebappClassLoaderBase
,有两个实现类WebappClassLoader
和ParallelWebappClassLoader
,(调试过程中发现Tomcat7
采用WebappClassLoader
类实现,而Tomcat8/9
使用ParallelWebappClassLoader
实现,不过这个不是影响的因素,因为加载资源的方法是写在WebappClassLoaderBase
里面的)
上代码
下面分别列出Tomcat7
和9
在关键部分的源码,其上下部分的调试过程略过,基本可以定位到如下位置的实现导致Tomcat7
和9
在资源加载时的不同,其实Tomcat8
的这个方法实现和9
是一样的。该类的具体位置是org.apache.catalina.loader.WebappClassLoaderBase
,方法是findResources(String name)
。
我是分割区间,上Tomcat7,下Tomcat9
上第一图是Tomcat7
实现,由于代码过长,截取一段,记住那个红色框起来的代码部分。上第二图是Tomcat8
和9
的实现。Tomcat
对于资源的加载,虽然实现的方法以及不同了,但是Tomcat7
采用的jarFiles
数组的方式时,jarFiles
数组的大小其实跟Tomcat9
的webResources
数组大小的值只差1 (Tomcat7
加载到133个资源,Tomcat9
加载到134个资源),即少了WEB-INF/classes
目录,因为jarFiles
不包含目录。如下图。
上图是Tomcat7
加载的资源,下图是Tomcat9
加载的资源
到这一步的时候,Tomcat7
和9
加载的资源其实是一样的,也包括了第三方jar
包,但是jar
内properties
文件为什么最后是Tomcat7
少了很多呢?问题就是刚才红线框起来的代码部分,它把所有jarfiles
都跳过了,没有把路径添加到result
值里面。这时候方法的返回值,也就是result
,就变成只有一个值,就是WEB-INF/classes
。而Tomcat9
的返回值,也就是result
的集合封装,大小是134,是全部返回的。 (这里其实省略了一部分代码,如Tomcat7
的返回值result
是1个对象,但是外部资源却显示大小为2,实际上外部有一层父类资源加载,那一层多了个资源对象,就是/Users/newcih/Downloads/apache-tomcat-7.0.94/lib/
,所以最后返回值大小为2)。 所以实际上Tomcat7
的findResources
方法并不会去加载第三方jar包内部的properties
文件。
我个人其实对技术研究不是那么热爱,技术更新迭代那么频繁的时代下,有些东西我都只作非常简单的了解,不想深究。比如为什么
Tomcat7
要加一层jarEntry
不为null
时的判断才允许添加该资源,明明你赋值JarFile
对象时就不给manEntry
属性赋值,让它成null
的。等等之类的东西,这篇文章其实也不是教程,只是一个小记,怕以后还遇到这种问题不知所措。对于Tomcat7
和8
、9
的源码分析可能也有误,仅供参考。
不仅仅是Tomcat
就在我以为,这个事情可以告一段落了,是Tomcat7
不支持的问题时。转念一想,不对劲啊。这可是Tomcat7
啊!!世界上大把项目在用着,难道就没人提出来吗?
我这时候才想起来,之前一直是真实项目中发生的情况,所以一直是在调试真实项目的代码,这时候才想起新建一个项目来测试才能验证是否正确。于是新建了如上一开始所说的testProgram
测试项目,结果测试发现。Tomcat7
和Tomcat9
部署下,均可以正常获取得依赖包里面的properties
文件的属性值。(我是直接注释testProgram
项目的配置文件的相关属性了,只留下依赖包的配置文件的属性存在,但是Tomcat7
部署下是可以正常获取的)。
遇到这种看起来奇葩的事情时,不要着急,物理老师告诉过我们要用控制变量法
。想想这个测试项目跟真实项目不同的地方在哪里?
我仔细看了看,是Spring
版本的不同,真实项目是 4.0.4.RELEASE
,测试项目是 4.2.6.RELEASE
。都是4.x
版本啊?能差到哪里去?
不管,不慌,按照一开始Spring
部分的源码开始调试进去看看。果然在Spring
这一层面的时候,源码就不一样了,跟Tomcat
加载无关了,上图有截图到Spring
一个findAllClassPathResources
方法的实现,还记得吗?
你能记得才有鬼?我调试过那么多遍都要回去看看呢!
下图直接给出Spring
这个方法在4.0.x
版本和4.1.x
版本的区别。
可以看出来,在Spring
的4.1.x
版本时,加载资源已经多了一个方法了,就是下面这个
if ("".equals(path)) {
// The above result is likely to be incomplete, i.e. only containing file system references.
// We need to have pointers to each of the jar files on the classpath as well...
addAllClassLoaderJarRoots(cl, result);
}
实际调试证明,Tomcat7
进入这个方法前,result
还是大小为2,但是经过这个方法之后,result
的大小增到171。。。如下图
所以很明显的一点是,Tomcat7
的一个不足之处 (或者说是一段我不了解为什么这么写的代码),好像被Spring
给顺带修复了?而且人家Spring
的注释好像也说了,上面那部分加载出来的不是全部的jar
资源,需要下面那个方法去加载classpath
的每一个jar
文件呢!但是再仔细想想,Spring
是不是只是刚好在这个版本加了这个功能而已,以后可能就没有了!
于是赶紧把Spring
仓库的分支切换到5.1.x
,可能不是最新,但是也挺新的了,看看这个方法的具体实现更新成什么样了,如下图所示:
额,,一模一样,看来这个方法的实现是不会大修大改了。
写得有点乱,总结一下,其实就是一个SpringMVC
项目,在Spring
加载配置文件的配置中,使用了classpath*
,导致在Tomcat7
下部署是正常运行的,但是用Tomcat8/9
部署时,发生配置属性被覆盖的事情。原因是,Tomcat7
本身不去加载jar
包内配置文件,所以不会有配置属性覆盖的情况,而Tomcat8/9
会加载,所以会覆盖。那Tomcat7
本身不去加载jar
包内配置文件的行为不是很好,所以Spring 4.1
的版本已经有顺带处理了,附带下图在Spring
的Github
上看到的issue
,不知道是否与此有关
因个人能力问题,本文仅作为小记,用于提醒以后遇到同样的情况,但不作为一篇讲解和说明分析。
解析方案
- 改属性键名,这样做最不会影响项目,只要不是依赖的
jar
包内配置文件有的属性键值就行了 - 如果不必要,可以不要在
Spring
配置里的加载配置使用classpath*
,用classpath
就好了