事情的起因来源于一个群内消息
这勾起了我的兴趣
问题的简述就是在windows环境下,用jar包方式启动咱们的服务,会启动报错,但是linux下没问题
java
Caused by: org.apache.ibatis.builder.BuilderException: Error parsing Mapper XML. The XML
location is 'URL [jar:file:F:\...-3.0.jar!/BOOT-INF/classes/.../repository/GroupDao_MYSQL.xml]'.
Cause: java.lang.IllegalArgumentException:
org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:123)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.parse(XMLMapperBuilder.java:95)
at org.mybatis.spring.SqlSessionFactoryBean.buildSqlSessionFactory(SqlSessionFactoryBean.java:611)
... 150 common frames omitted
Caused by: java.lang.IllegalArgumentException: Result Maps collection already contains value org.apache.ibatis.builder.MapperBuilderAssistant.addResultMap(MapperBuilderAssistant.java:209)
at org.apache.ibatis.builder.ResultMapResolver.resolve(ResultMapResolver.java:47)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:289)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElement(XMLMapperBuilder.java:254)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.resultMapElements(XMLMapperBuilder.java:246)
at org.apache.ibatis.builder.xml.XMLMapperBuilder.configurationElement(XMLMapperBuilder.java:119)
... 152 common frames omitted
现在开始祭出调试大法,debug启动jar包,刚好分享下我排查问题的方式,目前知道堆栈处是XMLMapperBuilder.java:123,断点到这个位置
看看e里面的堆栈哪儿抛的
ok 去Configuration的1014行看看
断点到1014行,然后重置帧到上一个方法
然后F9就能到达刚才的1014行的断点
然后从后往前点击堆栈,看看这些参数都是怎么传递的 发现在 buildSqlSessionFactory()这个方法看到了解析xml的mapperLocations
看看里面的内容 果然有重复的xml ,size大于1的都是代表有重复的
随便找个重复的看一下
发现三种不同
- classes一个有感叹号,一个没有
- 一个前面部分路径为\, 一个为/
- 一个前面为jar:file:/F..., 一个为jar:file:F... 由于在linux上面没问题,隐约觉得可能跟第三种情况有关
那就现在看这个mapperLocations的数据来自哪儿 通过跟上面类似的方法,重置帧然后找到这些mapperLocations是来自 PathMatchingResourcePatternResolver.findPathMatchingResources(String locationPattern)方法,方法参数就是来自配置文件mybatis.mapper-locations属性,报错的配置为classpath*:/**/*Dao_MYSQL.xml 发现结果集是Set集合,那么应该重写了equals方法,找到这个Resource的真实类型为UrlResrource,进去看看
看来取决定性作用的是getCleanedUrl(),发现主要用的StringUtils.cleanPath(originalPath),发现里面会把\换成/,经过排查classes!也不影响,那么问题就是上面说的第三点,那个盘符前面的/的有无造成的.
仔细调试了下findPathMatchingResources这个方法,发现逻辑是先找到rootDirResources,然后找这些root的子资源,那么子资源肯定前缀跟rootDirResource一致,那么就找找这些rootDirResource中前缀不同,但是子资源又一致的是哪些, 最终发现两个很可疑
URL [jar:file:/F:/.../target/app.jar!/BOOT-INF/classes!/]
URL [jar:file:F:...\target\app.jar!/]
第一个路径是springboot的jar包classes路径,第二个是jar包本身,第二个找子资源也能重新找到classes路径下,这就解释了为啥会重复找xml,那么现在就要解决两个路径前缀不一致的问题,要么都用/,那么都不用。后来再经过排查,发现带/的资源前缀,也就是jar:file:/F...这种来自jdk自身的方法,咱也不敢说,咱也不敢问,总不能让jdk改吧,说不定改出其他问题,刚好不带/的资源前缀是来自spring的方法,来看看来自哪儿
java
protected void addClassPathManifestEntries(Set<Resource> result) {
try {
String javaClassPathProperty = System.getProperty("java.class.path");
for (String path : StringUtils.delimitedListToStringArray(
javaClassPathProperty, System.getProperty("path.separator"))) {
try {
String filePath = new File(path).getAbsolutePath();
int prefixIndex = filePath.indexOf(':');
if (prefixIndex == 1) {
// Possibly "c:" drive prefix on Windows, to be upper-cased for proper duplicate detection
filePath = StringUtils.capitalize(filePath);
}
// # can appear in directories/filenames, java.net.URL should not treat it as a fragment
filePath = StringUtils.replace(filePath, "#", "%23");
// Build URL that points to the root of the jar file
UrlResource jarResource = new UrlResource(ResourceUtils.JAR_URL_PREFIX +
ResourceUtils.FILE_URL_PREFIX + filePath + ResourceUtils.JAR_URL_SEPARATOR);
// Potentially overlapping with URLClassLoader.getURLs() result above!
if (!result.contains(jarResource) && !hasDuplicate(filePath, result) && jarResource.exists()) {
result.add(jarResource);
}
}
catch (MalformedURLException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Cannot search for matching files underneath [" + path +
"] because it cannot be converted to a valid 'jar:' URL: " + ex.getMessage());
}
}
}
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to evaluate 'java.class.path' manifest entries: " + ex);
}
}
}
来自上面的11行,也就是对应spring框架5.3.x的PathMatchingResourcePatternResolver类的433行,这行代码意思是如果发现资源路径来自windows,那么就转大写,最后拼上jar:file:,也就是一个f:/...的jar资源最终变成jar:file:F:/...,前面说了,既然不能都变成不带/的,那就变成都带/的,这行代码就加三个字符
java
filePath = "/"+StringUtils.capitalize(filePath);
经过测试,搞定,至于linux为啥不出问题,因为没有盘符,根目录就是/,不会出现多一级造成差别。
本来事已至此就没了,但是看我这标题说明不简单。本来我准备把这个bug作为经验分享,也就是现在发个帖子了事,但是我们群里有小伙伴说,你发帖子不如给spring提个pr,那样对自己打造个人ip更有帮助! 听得我那叫一个心动,我就问怎么弄,说fork下,修改代码,完善测试,然后提交合并请求。说干那就干,去github上fork了spring的源码,就在github上用自带编辑器加上了那个'/',然后提交pr,没想到老外很快回复,然后用我蹩脚的英文跟他们互动了下。然后昨天凌晨,就是14号凌晨把我的pr请求放到6.0.14里程碑,昨天晚上10点多正式合并到主线,并backreports到5.3.x。
想看看跟老外的对话可以去github.com/spring-proj...
首贴,后面有机会分享一个全网都没有解决方案的一个有趣的小需求。
最后-请叫我小驰哥,一个喜欢技术的java程序员。