基于ClasspathResource路径问题的解决

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,还不是很清楚它的工作机制。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Java加载资源文件时的路径问题的解决办法

    加载资源文件比较常用的有两种: 一.用ClassLoader,说到这里就不得不提一下ClassLoader的分类,java内置的ClassLoader主要有三种, 第一种是根类加载器(bootstrap class loader),用C++来编写,负责将一些关键的Java类,如java.lang.Object和其他一些运行时代码先加载进内存中. 所负责加载的包:BootStrp------>JRE/lib/rt.jar 第二种是扩展类加载器(ExtClassLoader),由java类编写,负责

  • 解决SpringBoot ClassPathResource的大坑(FileNotFoundException)

    FileNotFoundException SpringBoot 项目将项目打包成jar包,使用ClassPathResource时使用的是绝对路径,直接调用getFile()方法会报 FileNotFoundException 直接上代码: 通过赋值文件为临时文件的方式解决 val resource = ClassPathResource("my.keystore") val temp = Files.createTempFile("my.keystore", &

  • spring boot加载资源路径配置和classpath问题解决

    1.spring boot默认加载文件的路径: /META-INF/resources/ /resources/ /static/ /public/ 我们也可以从spring boot源码也可以看到: private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classp

  • 浅谈web项目读取classpath路径下面的文件

    本文主要研究的是web项目下读取classpath路径下的文件的问题,具体如下. 首先分两大类按web容器分类 一种是普通的web项目,像用Tomcat容器,特点是压缩包随着容器的启动会解压缩成一个文件夹,项目访问的时候,实际是去访问文件夹,而不是jar或者war包. 这种的无论你是用获取路径的方法this.getClass().getResource("/")+fileName 获取流的方法this.getClass().getResourceAsStream(failName);

  • 解决异常FileNotFoundException:class path resource找不到资源文件的问题

    近期很多小伙伴问我,为何启动项目的时候Spring  或 Spring MVC资源文件找不到    ,这里我总结几个问题出现的原因和解决方案 一.问题的原因 为何会出现这种FIleNotFoundException呢?  原因无非两点: 1.路径配置错误 2.编译有问题,文件不存在 二.解决办法: 对于第一个原因,这个我们不多说,大家自己看看JavaWeb项目中classpath路径详解,改好配置目录路径,第一个原因排除,给大家一个案例参考 至于第二个原因,编译不成功,导致文件不存在,拿Ecli

  • 基于ClasspathResource路径问题的解决

    ClasspathResource路径问题 前言 在项目中工程以springboot jar形式发布,跟之前容器比少了一个解压目录,这个过程中出现了ClasspathResource的文件获取问题.具体如下: 故障情况 本地springboot工程打成jar包发布,在以下代码r.getFile()获取类目录下模板Excel文件报错: cannot be resolved to absolute file path because it does not reside in the file sy

  • 基于python 处理中文路径的终极解决方法

    1 .据说python3就没有这个问题了 2 .u'字符串' 代表是unicode格式的数据,路径最好写成这个格式,别直接跟字符串'字符串'这类数据相加,相加之后type就是str,这样就会存在解码失误的问题. 别直接跟字符串'字符串'这类数据相加 别直接跟字符串'字符串'这类数据相加 别直接跟字符串'字符串'这类数据相加 unicode类型别直接跟字符串'字符串'这类数据相加 说四遍 3 .有些读取的方式偏偏是要读取str类型的路径,不是unicode类型的路径,那么我们把这个str.enco

  • ASP.NET 路径问题的解决方法

    但是一些静态的内容,以及一些脚本文件的路径如果在自定义控件中引用,而后这个自定义控件又要在不同的母板页,或者不同的目录下引用.那就不可避免的会出现路径问题了. 解决方法也很简单: 复制代码 代码如下: script language="javascript" type="text/javascript" src='<%=ResolveUrl("~/js/media.js") %>'></script> <%=R

  • vue-cli webpack模板项目搭建及打包时路径问题的解决方法

    这里建议刚学vue的同学第一个小案例不要使用vue-cli进行操作,待对基本的api使用的比较顺手了之后再进行vue-cli的体验比较好.本人是一名后端开发人员,接触前端时间不长,这里有说的不好的地方,还请大家评论建议下. 1. 安装必要的环境准备 首先我们要能够暗转node.js,这个环境.百度搜索node,进入官网根据自己的操作系统进行下载即可.现在的版本都是自带npm的了.所以安装后,环境变量正常情况下会自动配置,开启一个命令行终端,输入node,npm,就可以看到相应的信息.那么说明安装

  • 基于vue 添加axios组件,解决post传参数为null的问题

    好,下面上货. 1.安装axios npm install axios --save 2.添加axios组件 import axios from 'axios' axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; axios.defaults.baseURL = 'http://localhost:7878/zkview'; Vue.prototype.$ajax = axios;

  • 基于Intellij Idea乱码的解决方法

    使用Intellij Idea经常遇到的三种乱码问题: 1.工程代码乱码 2.main方法运行,控制台乱码 3.tomcat运行,控制台乱码 解决方案: 1.工程代码乱码 Settings > Editor > File Encodings > Global Encodings & Project Encodings 设置为:UTF-8 2.main方法运行,控制台乱码 Settings > Build, Execution, Deployment > Compile

  • webpack配置打包后图片路径出错的解决

    问题 项目在开发环境下工作正常,当打包后图片不见了,检查元素后发现路径出错了. 图片路径是这样:background: url(/static/img/bg_camera_tip.bd37151.png),但该路径下文件并不存在. 打包后文件目录如下: 可以看到背景图片的路径应该是../../static而实际却是/static,找到原因后就好解决了 方法一 查看build目录下webpack.base.conf.js的配置,图片文件会经过url-loader处理. module: { rule

  • 基于pytorch padding=SAME的解决方式

    tensorflow中的conv2有padding='SAME'这个参数.吴恩达讲课中说到当padding=(f-1)/2(f为卷积核大小)时则是SAME策略.但是这个没有考虑到空洞卷积的情况,也没有考虑到strides的情况. 查阅资料后发现网上方法比较麻烦. 手算,实验了一个早上,终于初步解决了问题. 分为两步: 填充多少 中文文档中有计算公式: 输入: 输出: 因为卷积后图片大小同卷积前,所以这里W_out=W_in, H_out=H_in.解一元一次方程即可.结果取ceil. 怎么填充

  • 基于python 凸包问题的解决

    最近在看python的算法书,之前在年前买的书,一直在工作间隙的时候,学习充电,终于看到这本书,但是确实又有点难,感觉作者写的代码太炫技 了,有时候注释也不怎么能看懂,终于想到一个方法,就是里面说的算法问题,我就百度python解决他,觉得这个挺好. 下面是凸包问题的一个代码. # -*- coding: utf-8 -*- import turtle import random import time f=open('point.txt','w') for i in range(100): x

  • 基于django2.2连oracle11g解决版本冲突的问题

    上次用django2.2和oracle11g,在migrate的时候发生了版本冲突,最终将Oracle升级到了12c才解决问题 那么到底能不能用别的方法来解决这个冲突呢?想了个解决思路,实践一下: 用django2.2连Oracle12c环境下做migrate,创建基础表 将基础表导出,再导入到Oracle11g数据库中 用django2.2连Oracle11g 实施步骤 1.用django2.2连Oracle12c环境下做migrate,创建基础表 在前文中已经完成,连接到数据库,可以看到有1

随机推荐