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

相关推荐
AskHarries2 小时前
Spring Cloud OpenFeign快速入门demo
spring boot·后端
isolusion3 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp3 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder3 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚4 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
机器之心5 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴6 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲6 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
机器之心6 小时前
AAAI 2025|时间序列演进也是种扩散过程?基于移动自回归的时序扩散预测模型
人工智能·后端
hanglove_lucky7 小时前
本地摄像头视频流在html中打开
前端·后端·html