commons-io版本变动在windows环境下引发的NTFS ADS separator问题

起因

因业务需求,项目中引入了一个对方的业务jar包,但是发现代码却启动不起来了,报错:

ini 复制代码
org.springframework.beans.PropertyBatchUpdateException; nested PropertyAccessExceptions (1) are:
PropertyAccessException 1: org.springframework.beans.MethodInvocationException: Property 'locations' threw exception; nested exception is java.lang.IllegalArgumentException: NTFS ADS separator (':') in file name is forbidden.

尽管本地环境(windows)启动不起来,但是打包后放入Linux容器中是正常运行的。

随即进入debug分析,定位异常位置,发现是在使用disconf的jar中会调用到commons-ioFilenameUtilsgetExtension方法,来获取一个文件的后缀,具体调用如下:

java 复制代码
String extension = FilenameUtils.getExtension(fileName);

比如fileName为log4j2.xml,方法返回的就是xml

由于业务方引入的jar包中包含的commons-io的2.8.0的新包,IDEA启动时默认会使用该包,实际上原项目commons-io使用的是2.5版本。

分析

disconf使用的是org.apache.commons.io下的FilenameUtils,我们先看下2.5版本的实现:

java 复制代码
public static String getExtension(final String filename) {  
    if (filename == null) {  
        return null;  
    }  
    final int index = indexOfExtension(filename);  
    if (index == NOT_FOUND) {  
        return "";  
    } else {  
        return filename.substring(index + 1);  
    }  
}

关注下indexOfExtension的实现:

java 复制代码
public static int indexOfExtension(final String filename) {  
    if (filename == null) {  
        return NOT_FOUND;  
    }  
    final int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);  
    final int lastSeparator = indexOfLastSeparator(filename);  
    return lastSeparator > extensionPos ? NOT_FOUND : extensionPos;  
}

而2.8.0的commons-io的实现如下:

java 复制代码
public static String getExtension(final String fileName) throws IllegalArgumentException {  
    if (fileName == null) {  
        return null;  
    }  
    final int index = indexOfExtension(fileName);  
    if (index == NOT_FOUND) {  
        return EMPTY_STRING;  
    }  
    return fileName.substring(index + 1);  
}
java 复制代码
public static int indexOfExtension(final String fileName) throws IllegalArgumentException {  
    if (fileName == null) {  
        return NOT_FOUND;  
    }  
    if (isSystemWindows()) {  
        // Special handling for NTFS ADS: Don't accept colon in the fileName.  
        final int offset = fileName.indexOf(':', getAdsCriticalOffset(fileName));  
        if (offset != -1) {  
            throw new IllegalArgumentException("NTFS ADS separator (':') in file name is forbidden.");  
        }  
    }  
    final int extensionPos = fileName.lastIndexOf(EXTENSION_SEPARATOR);  
    final int lastSeparator = indexOfLastSeparator(fileName);  
    return lastSeparator > extensionPos ? NOT_FOUND : extensionPos;  
}

问题就出在这里,方法多了一个IllegalArgumentException异常,并且是特定在windows环境下才抛出NTFS ADS separator (':') in file name is forbidden.,这也就解释了为什么服务打包后在Linux环境下没有问题。

该方法的注释上也写了为什么会做这个改变:

Note: This method used to have a hidden problem for names like "foo.exe:bar.txt". In this case, the name wouldn't be the name of a file, but the identifier of an alternate data stream (bar.txt) on the file foo.exe. The method used to return ".txt" here, which would be misleading. Commons IO 2.7, and later versions, are throwing an {@link IllegalArgumentException} for names like this.

翻译过来就是,在windows上类似foo.exe:bar.txt的文件,bar.txt不是文件的名称,而是文件foo.exe上的备用数据流bar.txt的标识符。

NTFS中的备用数据流(Alternate Data Stream,ADS)允许将一些元数据嵌入文件或是目录,而不需要修改其原始功能或内容。

这个改动刚好是commons-io的2.7版本,命中了项目因为引入业务jar包导致的2.5->2.8.0的升级迭代。

解决

后续决定还是在业务jar中移除commons-io的2.8.0版本,沿用之前项目中的2.5版本。

总结

项目使用disconf来监听配置文件的变更,实际上传入的是类似classpath:log4j2.xml这样的fileName,导致的问题。

项目初步启动时,直接报错的堆栈中并没有关于commons-io的堆栈信息,只知道是disconf的bean无法启动,可以先从这个bean入手,逐步定位到是com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean成员变量locations的问题,进而从setLocations这个方法定位到使用了FilenameUtils中的getExtension,从而得知是哪里的异常。

进而直接使用Dependency Analyzer,看到业务jar包引入了更新版本的commons-io,并通过对比两个版本的FilenameUtilsgetExtension方法的区别,了解本次异常的原因。

这是一次较为常见的组件变动引起的启动异常,但是实际上问题的表征并不能直接定位原因,合理利用debug,在项目启动时,添加断点,逐步分析,是开发者需要掌握的技能。

这个问题直接搜索是不太能得到答案的,但是分析问题的过程,是初学者逐步向熟练者进阶的必备技能,特此记录,以作参考。

参考

  1. NTFS ADS(备用数据流)
相关推荐
卓怡学长41 分钟前
w272基于springboot便民医疗服务小程序
java·spring boot·spring·小程序·intellij-idea
霸道流氓气质44 分钟前
Harness Engineering 从理论到实战:基于 Spring AI Alibaba 的完整实现指南
数据库·人工智能·spring·harness
AI人工智能+电脑小能手20 小时前
【大白话说Java面试题 第155题】【06_Spring篇】第15题:Spring 如何解决循环依赖问题?
java·spring·循环依赖·三级缓存·aop代理
AI人工智能+电脑小能手1 天前
【大白话说Java面试题 第156题】【06_Spring篇】第16题:Spring 用到了哪些设计模式?
java·spring·设计模式·代理模式·工厂模式
辰海Coding1 天前
MiniSpring框架学习笔记-动态代理:如何在运行时插入逻辑?
java·笔记·学习·spring·动态代理
一路向北North1 天前
Spring Security OAuth2.0(16):密码模式
java·后端·spring
卓怡学长1 天前
w273基于springboot的智能笔记的开发与应用
java·spring boot·spring·intellij-idea
无心水2 天前
【全域智能营销实战】2、Spring AI 模块化架构深度解读:从 1.0 到 2.0 的演进与最佳实践
人工智能·spring·架构·harness·顶尖架构师·全域智能营销·harmess
livemetee2 天前
【关于Spring声明式事务】
java·后端·spring
Esaka_Forever2 天前
Python 完整内存管理机制详解
开发语言·python·spring