基于ClasspathResource路径问题的解决
作者:夜晚在编程 发布时间:2022-03-29 21:23:22
ClasspathResource路径问题
前言
在项目中工程以springboot jar形式发布,跟之前容器比少了一个解压目录,这个过程中出现了ClasspathResource的文件获取问题。具体如下:
故障情况
本地springboot工程打成jar包发布,在以下代码r.getFile()获取类目录下模板Excel文件报错:
cannot be resolved to absolute file path because it does not reside in the file system: jar
解决方案
调整代码,直接获取对应的文件流,进行封装。
ClassPathResource详解
ClassPathReource resource=new ClassPathResource("spring_beans.xml");
1:public class ClassPathResource extends AbstractFileResolvingResource
在ClassPathResource中,含参数String path的构造函数:
public ClassPathResource(String path ) {
this (path , (ClassLoader) null);
}
2:上述构造函数指向了另外一个构造函数:
public ClassPathResource (String path , ClassLoader classLoader ) {
Assert. notNull(path, "Path must not be null");
String pathToUse = StringUtils.cleanPath(path);
if (pathToUse .startsWith("/")) {
pathToUse = pathToUse .substring(1);
}
this .path = pathToUse;
this .classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
能够看到path由StringUtils的cleanPath方法返回了pathToUse。由此,我们找到StringUtils的cleanPath方法
3:public abstract class StringUtils
public static String cleanPath (String path ) {
if (path == null) {
return null ;
}
String pathToUse = replace( path , WINDOWS_FOLDER_SEPARATOR , FOLDER_SEPARATOR);
int prefixIndex = pathToUse .indexOf(":" );
String prefix = "" ;
if (prefixIndex != -1) {
prefix = pathToUse .substring(0, prefixIndex + 1);
pathToUse = pathToUse .substring(prefixIndex + 1);
}
if (pathToUse .startsWith(FOLDER_SEPARATOR)) {
prefix = prefix + FOLDER_SEPARATOR;
pathToUse = pathToUse .substring(1);
}
String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR );
List<String> pathElements = new LinkedList<String>();
int tops = 0;
for (int i = pathArray. length - 1; i >= 0; i --) {
String element = pathArray [i ];
if (CURRENT_PATH .equals(element)) {
// Points to current directory - drop it.
}
else if (TOP_PATH.equals(element)) {
// Registering top path found.
tops ++;
}
else {
if (tops > 0) {
// Merging path element with element corresponding to top path.
tops --;
}
else {
// Normal path element found.
pathElements .add(0, element );
}
}
}
// Remaining top paths need to be retained.
for (int i = 0; i < tops; i++) {
pathElements .add(0, TOP_PATH);
}
return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR );
}
4:StringUtils类中 replace方法
public static String replace (String inString , String oldPattern , String newPattern ) {
if (!hasLength( inString ) || !hasLength(oldPattern) || newPattern == null ) {
return inString ;
}
StringBuilder sb = new StringBuilder();
int pos = 0; // our position in the old string
int index = inString .indexOf(oldPattern );
// the index of an occurrence we've found, or -1
int patLen = oldPattern.length();
while (index >= 0) {
sb.append( inString .substring(pos , index ));
sb.append( newPattern );
pos = index + patLen;
index = inString .indexOf(oldPattern, pos );
}
sb.append( inString .substring(pos ));
// remember to append any characters to the right of a match
return sb .toString();
}
5:StringUtils类类中的hasLength方法
由此可以看出,同样的方法名,不同的方法签名,然后在其中一个方法中引用另外一个方法。好多类都是这么用的,就像开始的时候的构造函数那样,虽然不知道好处 是什么,但先记下来。
public static boolean hasLength (String str ) {
return hasLength((CharSequence) str);
}
public static boolean hasLength (CharSequence str) {
return (str != null && str.length() > 0);
}
跟踪到这里,可以知道hasLength方法的目的就是str不为空且str的长度大于0。突然发现CharSequence这个类没接触过,来看一下它的源码
6:public interface CharSequence{}
额,源码没看懂 就不粘贴过来了。
7:回到StringUtils的replace方法
首先判断传入的三个参数,如果为空后者长度小于0,直接返回inString;那么我看一下这三个参数都是什么:
inString:path 这个就是我们传入的文件名
oldPattern:private static final String WINDOWS_FOLDER_SEPARATOR = "\\";
newPattern:private static final String FOLDER_SEPARATOR = "/" ;这两个是文件分隔符
然后给局部变量index赋值,通过查阅API:
public int indexOf(int ch)
返回指定字符在此字符串中第一次出现处的索引。
意思就是在path中查找"\\",例如我写文件的绝对路径是D:\\文件\\API\\JDK_API_1_6_zh_CN.CHM,我就需要循环的读取“\\”,接下来while循环中出现了substring方法,继续查阅API:
public String substring(int beginIndex)
返回一个新的字符串,它是此字符串的一个子字符串。该子字符串从指定索引处的字符开始,直到此字符串末尾。
public String substring(int beginIndex,
int endIndex)
返回一个新字符串,它是此字符串的一个子字符串。该子字符串从指定的 beginIndex 处开始,直到索引 endIndex - 1 处的字符。因此,该子字符串的长度为 endIndex-beginIndex
故此 ,第一次循环会把path路径中从0索引开始,直到第一个"\\"之间的内容添加到StringBuffer中,然后再在StringBuffer中添加“/”,接下来pos和index都需要改变,要往后挪。因为循环需要往后走,我们要找到第二个“\\”,觉得这个有点算法的意思。返回sb.toString()。
总结一下replace方法,本意是根据传入的路径path,如果是D:\\文件\\API\\JDK_API_1_6_zh_CN.CHM这种格式的,给转换成D:/文件/API/JDK_API_1_6_zh_CN.CHM这种格式。
8:StringUtils的cleanPath方法:
通过replace的返回值,我们得到了可以用的路径pathToUse,然后我们要把这个路径下“:”给找出来,正如代码
int prefixIndex = pathToUse.indexOf(":" );
那样,需要知道,indexOf方法只要没找到相应的字符,就会返回-1,所以在下面的判断中才会以perfixIndex是否为-1来进行判断。如果路 径中有“:”,接着以D:/文件/API/JDK_API_1_6_zh_CN.CHM举例,prefix="D:" pathToUse="/文件/API/JDK_API_1_6_zh_CN.CHM ",这个很有意思,因为程序不知道我们输入的是绝对路径 带D:的这种 ,还是/开头的这种,或者说相对路径,程序直接全给你判断了。
接下来会判断pathToUse是否以“/"开头,是的话prefix会加上“/”,现在的prefix有两种情况,可能是"D:/"这种,也可能是"/"这种,而pathToUse肯定是“文件/API/JDK_API_1_6_zh_CN.CHM ”这种了。
String[] pathArray = delimitedListToStringArray( pathToUse, FOLDER_SEPARATOR );
看到这句代码,我估计是把pathToUse给拆成字符串数组里,就像是这样,文件 API ***的这种。接下来看看具体的代码是不是这样:
9:StringUtils的delimitedListToStringArray方法
public static String[] delimitedListToStringArray(String str, String delimiter) {
return delimitedListToStringArray( str, delimiter, null );
}
public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete ) {
if (str == null) {
return new String[0];
}
if (delimiter == null) {
return new String[] {str};
}
List<String> result = new ArrayList<String>();
if ("" .equals(delimiter)) {
for (int i = 0; i < str.length(); i++) {
result.add(deleteAny( str.substring(i , i + 1), charsToDelete));
}
}
else {
int pos = 0;
int delPos ;
while ((delPos = str.indexOf(delimiter , pos )) != -1) {
result.add(deleteAny( str.substring(pos , delPos), charsToDelete ));
pos = delPos + delimiter.length();
}
if (str .length() > 0 && pos <= str.length()) {
// Add rest of String, but not in case of empty input.
result.add(deleteAny( str.substring(pos ), charsToDelete));
}
}
return toStringArray( result);
}
先看看传入的参数:
str:pathToUse,就是文件/API/JDK_API_1_6_zh_CN.CHM
delimiter:"/"
charsToDelete:null
public static String deleteAny(String inString, String charsToDelete ) {
if (!hasLength( inString) || !hasLength(charsToDelete)) {
return inString ;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < inString.length(); i++) {
char c = inString.charAt( i);
if (charsToDelete .indexOf(c) == -1) {
sb.append( c);
}
}
return sb .toString();
}
如果说我们传入的"/"等于""的话,显然是不可能,我们所假如的话,会把pathTOUse倒着循环,每个字符都摘出来,然后当成字符串用,传入deleteAny中,然后又是循环,对每个字符而言,如果charsToDelete中没有这个字符,就在StringBuilder中添加这个字符。返回值是String。当然了,这个还没用到。我们用到的是那个很复杂的else
我们遇到了一个循环,对pathToUse而言,从索引0开始,如果pathToUse中有"/",就像文件/API/JDK_API_1_6_zh_CN.CHM 我们会得到“文件,然后还会进入deleteAny这个方法,参数inString就是"文件",charsToDelete是null,突然发现charsToDelete的值为Null的话会直接返回InString,也就是“文件”。
返回到delimitedListToStringArray方法之后,接着往后循环,最终的结果就是实现了把pathToUse给切割成若干个String的形式。
10:回到StringUtils的cleanPath方法
我们遇到了一个倒着的循环,如果说我们这个是特别正常的路径,就相当于复制了,如果是以.或者..结尾的这些内容,我们就把它给忽略了。
我得承认上面这些过程真的好复杂,其实就是做了一件事,对输入的路径进行了处理,只不过考虑的情况多了一点。所以我决定还是用debug来走一遍看看。
这时我用的是绝对路径
终于熬到了这个方法的结束。
11:回到ClassPathResource的构造函数
this .classLoader = ( classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
如果传入的classLoaser有值,就返回这个值,如果没有,就获取一个。
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex ) {
// Cannot access thread context ClassLoader - falling back...
}
if (cl == null) {
// No thread context class loader -> use class loader of this class.
cl = ClassUtils.class .getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex ) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
return cl ;
}
写到这,我们的ClassPathResouce resouce实例就有了path 和 classLoader这两个关键属性。
如果我们想获取输入流
@Override
public InputStream getInputStream() throws IOException {
InputStream is;
if (this .clazz != null) {
is = this.clazz .getResourceAsStream(this. path);
}
else if (this.classLoader != null) {
is = this.classLoader .getResourceAsStream(this. path);
}
else {
is = ClassLoader.getSystemResourceAsStream( this.path );
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is ;
}
会判断clazz 有没有值,classLoader有没有值,然后再获取输入流。前两天整理了java.lang.Class这个类的意思,现在就能用一点了,先看看定义的一些属性
private final String path ;
private ClassLoader classLoader;
private Class<?> clazz;
ClassPathResource有好几个构造函数,有的构造函数会传入classLoader,有的会传入clazz,这个clazz就是相应的类在JVM上的实例,显然上面的例子中并没有这个东西,而classLoader是有的,所以通过classLoader获取输入流。
我觉得对ClassPathResource理解的更透彻了,虽然大部分时间都是在对path进行处理。
近期还要看看ClassLoader,还不是很清楚它的工作机制。
来源:https://blog.csdn.net/weixin_42005602/article/details/106661322
猜你喜欢
- 1. Spring ProfileSpring可使用Profile绝对程序在不同环境下执行情况,包含配置、加载Bean、依赖等。 Sprin
- 今天去某在线教育面试面试官让做的一道题,题目描述如下:给定一个不重复的无序数组arr和一个定值num查找arr中是否有两个数的和等于num有
- 简介springmvc对json的前后台传输做了很好封装,避免了重复编码的过程,下面来看看常用的@ResponseBody和@Request
- Timer 详解Timer 和 TimerTask 用于在后台线程中调度任务的 java.
- 动态方法就是一个Action对应多个请求,减少Action的数量1、指定method属性<action name="addA
- 问题描述 idea启动tomcat后乱码了,并且,idea的各种编码都是设置的为UTF-8,但是中文就是乱码了。解决方法 进入idea的安装
- 线程可以有六种状态:1.New(新创建)2.Runnable(可运行)(运行)3.Blocked(被阻塞)4.Waiting(等待)5.Ti
- 引言♀ 小AD:明哥,昨天气死我了,明哥要帮我出气。♂ 明世隐:咋了,有谁惹到你了。♀ 小AD:昨天辅助喷我小鲁班菜,我反手就对喷,然后竟然
- 如何实现封装可以分为两步:第一步:将类的变量声明为private。第二步:提供公共set和get方法来修改和获取变量的值。代码展示publi
- java 设计模式之单例模式前言:在软件开发过程中常会有一些对象我们只需要一个,如:线程池(threadpool)、缓存(cac
- Java环境部署下载所需软件和依赖安装 jdk路径可选别的盘。但是指向时得注意我自己时装在 F 盘的,在f盘里新建文件夹 Java,Java
- 概述日常工作中,我们经常会有发送 HTTP 网络请求的需求,概括下我们常见的发送 HTTP 请求的需求内容:可以发送基本的 GET/POST
- 前文传送门:Netty启动流程服务端channel初始化注册多路复用回到上一小节的代码:final ChannelFuture initAn
- Android之文件数据存储一、文件保存数据介绍Activity提供了openFileOutput()方法可以用于把数据输出到文件中,具体的
- 项目需要对接外部接口,将图片文件流发送到外部接口,下面代码就是HttpsURLConnection如何上传文件流:/** *
- 注意事项阿里云的dataworks提供了OpenApi, 需要是企业版或旗舰版才能够调用,也就是付费项目。这里测试主要是调用拉取datawo
- 快速排序过程没有既不浪费空间又可以快一点的排序算法呢?那就是“快速排序”!光听这个名字是不是就觉得很高端呢。假设我们现在对“52 39 67
- 本文实例讲述了Java使用Preference类保存上一次记录的方法。分享给大家供大家参考。具体分析如下:在使用java中JFileChoo
- 以下总结是2016/3/23在做一个网站时遇到的一个功能模块,现在将总结从为知笔记上搬家到CSDN,与大家共享,欢迎指正。0.准备工作 0.
- 本文实例讲述了C#实现Base64处理的加密解密,编码解码。分享给大家供大家参考,具体如下:using System;using Syste