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程序员。

相关推荐
刘鹏3787 分钟前
深入浅出Java中的CAS:原理、源码与实战应用
后端
Lx35212 分钟前
《从头开始学java,一天一个知识点》之:循环结构:for与while循环的使用场景
java·后端
fliter12 分钟前
RKE1、K3S、RKE2 三大 Kubernetes 发行版的比较
后端
aloha_13 分钟前
mysql 某个客户端主机在短时间内发起了大量失败的连接请求时
后端
程序员爱钓鱼15 分钟前
Go 语言高效连接 SQL Server(MSSQL)数据库实战指南
后端·go·sql server
xjz184215 分钟前
Java AQS(AbstractQueuedSynchronizer)实现原理详解
后端
Victor35615 分钟前
Zookeeper(97)如何在Zookeeper中实现分布式协调?
后端
至暗时刻darkest15 分钟前
go mod文件 项目版本管理
开发语言·后端·golang
程序员爱钓鱼16 分钟前
Go 语言高效连接 MySQL 数据库:从入门到实战
后端·mysql·go
陈随易16 分钟前
前端之虎:现代前端开发必备依赖(第3期)
前端·后端·程序员