Spring 框架bug修复经历分享

事情的起因来源于一个群内消息

这勾起了我的兴趣

问题的简述就是在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的都是代表有重复的

随便找个重复的看一下

发现三种不同

  1. classes一个有感叹号,一个没有
  2. 一个前面部分路径为\, 一个为/
  3. 一个前面为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程序员。

相关推荐
paopaokaka_luck2 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风3 小时前
详解K8S--声明式API
后端
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml44 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~4 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616884 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
睡觉谁叫~~~5 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
2401_865854887 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
AskHarries7 小时前
Spring Boot集成Access DB实现数据导入和解析
java·spring boot·后端
2401_857622668 小时前
SpringBoot健身房管理:敏捷与自动化
spring boot·后端·自动化