程序笔记   发布时间:2022-07-19  发布网站:大佬教程  code.js-code.com
大佬教程收集整理的这篇文章主要介绍了jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。

工作中常常会用到文件加载,然后又经常忘记,印象不深,没有系统性研究过,从最初的war包项目到现在的springboot项目,从加载外部文件到加载自身jar包内文件,也发生了许多变化,这里开一贴,作为自己的备忘录,也希望能给广大 java coder 带来帮助。

一、目标

通过此文,能熟知普通war包项目目录内、jar包自身内文件的加载方式。

二、文件定位

2.1 WAR 包项目

为什么先说war包项目,war包项目部署到Web容器里后 ,会被解压,所以文件读取方式,和在ide里面读取是类似的。

读取文件,首先要定位文件,定位到文件之后才能读取。

定位文件,java常用的有两种,分别是

这里的参数 name ,就是咱们认为的路径,官方对这个参数名的描述是:

name of the desired resource

渴望得到的资源的名字

URL 则是资源的定位,可以得到资源所在路径。

URL 可以是不同的资源,通过其字段 protocol 来区分是哪种类型资源,取值有:

  • ftp
  • nntp
  • http
  • file
  • jar

感兴趣的同学可以自行了解 URL 的定义

2.1.1 Class.getresource(String @R_696_8313@

通过class实例获得资源的定位,传入参数有如下查找方式:

  • / 开头,则从 classPath 即运行的 class 文件所在的项目的 ***/classes/ 目录下找起
  • 非以 / 开头的,则从当前class所在路径下找起

验证:

先上项目结构图

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

验证代码

public class Classresource {
    public static void main(String[] args) {
        Classresource classresource = new Classresource();
        classresource.resWithInstance("");
        classresource.resWithInstance("/");
        classresource.resWithInstance("Classresource.class");
        classresource.resWithInstance("/Classresource.class");
        classresource.resWithInstance("/1.txt");
    }

    public void resWithInstance(String path) {
        URL resource = this.getClass().getresource(path);
        print(resource, path);
    }

    private static void print(URL resource, String path) {
        try {
            System.out.println("Classresource 根据目录[" + String.format("%-20s", path) + "] 获取路径为 " + resourcE);
        } catch (Exception E) {
            System.out.println("Classresource 根据目录[" + path + "] 获取路径出错,错误原因:" + e.getmessage());
        }
    }

}

我们传入了5个参数,分别是

  • 空字符串
  • /
  • 当前类文件名
  • / + 当前类文件名
  • / + 项目 resources 目录下的 1.txt 文件

运行结果如下:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

结果分析:

  1. 空字符串

    定位为当前类路径

  2. /

    定位为 classPath 路径

  3. 当前类文件名

    定位为当前类文件所在路径,成功定位到文件

  4. / + 当前类文件名

    定位不到文件

  5. / + 项目 resources 目录下的 1.txt 文件

    定位为 resoures/1.txt,因为编译后 resources 目录里的文件都移动到了 classPath 路径下,所以也成功定位

4错误原因很明显,因为 classPath 路径下没有名叫 Classresource.class 的文件,所以定位不到

总结:

使用 class 查找文件,以 / 开头的文件名,是从 classPath 目录下找,否则从当前类文件目录下找

2.1.2 ClassLoader.getresource(String @R_696_8313@

通过 classLoader 实例获得资源的定位,传入参数仅有如下查找方式:

  • classPath 路径下找起

验证:

验证代码

public class ClassLoaderresource {

    public static void main(String[] args) {
        ClassLoaderresource classLoaderresource = new ClassLoaderresource();
        classLoaderresource.resWithInstance("");
        classLoaderresource.resWithInstance("/");
        classLoaderresource.resWithInstance("ClassLoaderresource.class");
        classLoaderresource.resWithInstance("/ClassLoaderresource.class");
        classLoaderresource.resWithInstance("1.txt");
        classLoaderresource.resWithInstance("/1.txt");
    }

    public void resWithInstance(String path) {
        URL resource = this.getClass().getClassLoader().getresource(path);
        print(resource, path);
    }

    private static void print(URL resource, String path) {
        try {
            System.out.println("ClassLoaderresource 根据目录[" + String.format("%-26s", path) + "] 获取路径为" + resourcE);
        } catch (Exception E) {
            System.out.println("ClassLoaderresource 根据目录[" + path + "]获取路径出错,错误原因:" + e.getmessage());
        }
    }
}

我们传入了6个参数,分别是

  • 空字符串
  • /
  • 当前类文件名
  • / + 当前类文件名
  • 1.txt
  • /1.txt

运行结果如下:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

结果分析:

  1. 空字符串

    定位为 classPath 路径

  2. /

    定位不到

  3. 当前类文件名

    定位不到

  4. / + 当前类文件名

    定位不到

  5. resources目录下的 1.txt 文件名

    定位为 resoures/1.txt,因为编译后 resources 目录里的文件都移动到了 classPath 路径下,成功定位

  6. / + resources目录下的 1.txt 文件名

    定位不到

3错误原因很明显,因为 classPath 路径下没有名叫 Classresource.class 的文件,所以定位不到

2、4、6错误原因是因为以 / 开头,这里先记着:

/ 开头的都会定位不到,但是参数中可以带有 / 来表示下一级路径

如:查找 @H_773_18@main.class 的参数此处应写为 com/yx/jtest/Main.class

总结:

使用 classLoader 查找文件,总是从 classPath 目录下找起,且不能以 / 开头

2.1.3 Class.getresource 与 ClassLoader.getresource 的异同原因

对于相同的开头字符 /空字符串 为什么两种方式的执行结果不一样呢

来分析下 class.getresource 源码

public class Class {
    public java.net.URL getresource(String @R_696_8313@ {
        name = resolvename(@R_696_8313@; // ①
        ClassLoader cl = getClassLoader0();
        if (cl == null) {
            // A system class.
            return ClassLoader.getSystemresource(@R_696_8313@;
        }
        return cl.getresource(@R_696_8313@;
    }
}

可以看到在进行 ① 转换资源名称后,内部还是调用了 classLoader.getresource 方法。

那么异同的奥秘就都在这个第一行里的 resolvename(@R_696_8313@ 方法里了

来看 resolvename(@R_696_8313@

public class Class {
    /**
     * Add a package name prefix if the name is not absolute Remove leading "/"
     * if name is absolute
     */
    private String resolvename(String @R_696_8313@ {
        if (name == null) {
            return name;
        }
        if (!name.startsWith("/")) {
            Class<?> c = this;
            while (c.isArray()) {
                c = c.getComponentType();
            }

            // 这里的basename类似 com.foo.bar 之类的形式
            String basename = c.getName();
            int index = basename.lasTindexOf('.');
            if (index != -1) {

                // 拼name,把包名称拼上,如 "com/foo/" + "/" + "Bar1"
                // 就是获取当前类目录下的路径名
                name = basename.subString(0, indeX).replace('.', '/')
                        + "/" + name;
            }
        } else {
            name = name.subString(1);
        }
        return name;
    }
}

可以看到:

  1. 如果不以 / 开头,就返回 当前类所在目录 + 资源名
  2. 否则返回 / 后面的字符串
  3. 总结就是该方法把相对路径转换为了基于 classPath 的绝对路径

在经过资源名称处理后,就跟 classLoader.getresource 的规则一样了。

这里处理 / 符号也间接说明了 classLoader.getresource 不再接受 / 开头的资源名称,因为它把 / 当成了路径分隔符,下面是官方的参数说明

The name of a resource is a '/'-separated path name that identifies the resource.

资源的名称是一个“/”分隔的路径名,用于标识资源。

所以两者的异同点在于:

class.getresource 先进行了 / 符号开头的路径的预处理,使之转换为了基于 classPath 的绝对路径,再调用 classLoader.getresource 的方法

classLoader.getresource 只接受基于 classPath 的绝对路径,并不再接受以 /开头的路径,此时 "" 空字符串则代表 classPath 路径,而非 class.getresource/

2.2 JAR包项目

当项目为jar项目时,加载的方式变了,主要有

  1. classPath 路径由 file 目录变成了 jar文件,这影响到资源的定位方式,而且不再支持获取当前 classPath 路径
  2. URLClassPath 加载资源时候由 FileLoader 变成了 JarLoader,这影响到资源对特殊符号的处理方式
  3. 定位内部文件的URL协议由 file 变成了 jar,这影响到资源文件的读取方式

先来看打包成jar后的运行情况,这次使用另外一个类去写测试,该类直接调用上面的演示方法

代码:

public class Main {
    public static void main(String[] args) {
        Classresource classresource = new Classresource();
        classresource.resWithInstance("");
        classresource.resWithInstance("/");
        classresource.resWithInstance("Classresource.class");
        classresource.resWithInstance("/Classresource.class");
        classresource.resWithInstance("1.txt");
        classresource.resWithInstance("/1.txt");

        ClassLoaderresource classLoaderresource = new ClassLoaderresource();
        classLoaderresource.resWithInstance("");
        classLoaderresource.resWithInstance("/");
        classLoaderresource.resWithInstance("Classresource.class");
        classLoaderresource.resWithInstance("/Classresource.class");
        classLoaderresource.resWithInstance("1.txt");
        classLoaderresource.resWithInstance("/1.txt");
    }
}

运行结果:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

分析:

class.getresource()

  1. 空字符串仍代表当前类路径,以非 / 开头的资源名称都会从当前类路径下找起。记得没有打包时候的规则吗,没错,这里的空格又被转换成了当前类路径,然后调用的 classLoader.getresource() 方法
  2. / 仍代表应用 classPath 路径,以 / 开头的资源名称都会从应用 classPath 路径下找起,找资源名为 / 后面的字符的资源。但是如果只传 / 则定位不到,不能输出 classPath 路径

classLoader.getresource()

  1. 还是不接受 / 开头的资源名称,所有 / 开头的资源名称都会返回为null,定位不到
  2. 空字符串原本代表 classPath 路径,这里不再支持
  3. / 开头的资源从应用 classPath 路径下找起

其他

  1. classPath 由文件夹变成了jar文件
  2. URL协议由 file: 变成了 jar:file:

三、文件加载

上一章节,我们已经知道了 classclassLoader 定位资源的异同,和在打成 jar 包之后的变化。

现在定位文件已经做到了,这里不再区分究竟是 class 定位的文件还是classLoader 定位的文件,本章节就使用 class 去定位文件如何加载我们定位到的文件呢?

3.1 WAR 包项目

因为 WAR 项目会被解压成为具体的文件(tomcat),所以这里我们用传统的 File 描述一个对象,并读取即可。

public class Classresource {
    public static void main(String[] args) {
        readFile("/1.txt");
    }

    public static void readFile(String path) {
        //1.定位资源
        URL resource = Classresource.class.getresource(path);
        System.out.println("[getresource        ] 读取文件:" + resourcE);
        if (null == resourcE) {
            System.out.println("找不到资源文件");
            return;
        }

        //2.映射资源
        File file = new File(resource.getPath());
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream(filE);
            //3.读取资源
            read(inputStream);
        } catch (IOException E) {
            e.printStackTrace();
        } finally {
            if (null != inputStream) {
                try {
                    inputStream.close();
                } catch (IOException E) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void read(InputStream resourcE) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i;
        while ((i = resource.read()) != -1) {
            baos.write(i);
        }
        System.out.println(new String(baos.toByteArray(), StandardCharsetS.UTF_8));
    }
}

执行结果:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

可以看到能正常读取,这里不再叙述。

3.2 JAR 包项目

我们首先将上述方法打到 jar 包里面去运行,看一下效果

import com.yx.jtest.loadfile.ClassLoaderresource;
import com.yx.jtest.loadfile.Classresource;

public class Main {

    public static void main(String[] args) {

        System.out.println("#############打包后Classresource开始读取文件############");
        Classresource.readFile("/1.txt");
    }
}

执行结果:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

可以看到,读取失败了:FileNotFoundException ,到这里,大家可以思下,为什么文件读取不到了?

3.2.1 为什么路径是 jar: 开头

注意看红框部分输出的文件 URL,这个 URL 不再是以 file: 开头的了。这里先标记下,我们来跟踪下 classLoader.getresource() 的方法,来找到为什么是 jar: 开头。不感兴趣的同学可以跳过这部分

public class ClassLoader {
    //...

    public URL getresource(String @R_696_8313@ {
        URL url;
        if (parent != null) {
            url = parent.getresource(@R_696_8313@;
        } else {
            url = getBootstrapresource(@R_696_8313@;
        }
        if (url == null) {
            url = findresource(@R_696_8313@;
        }
        return url;
    }
}

熟悉的双亲委任模型,这里不多说,介绍下 ClassLoader 这个类和 Java 的类加载器

从Java虚拟机的角度来讲,只存在两种不同的类加载器:

一种是启动类加载器 (Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;

另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且 全都继承自抽象类java.lang.ClassLoader。

摘自:《深入理解Java虚拟机-JVM高级特性与最佳实践》

其中启动类加载器和其他类加载器的关系,如下图所示:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

到这里,我们能知道上述代码的 ClassLoader 实例的 parent 变量都是谁了,这里揭示下:

jar 启动调用的类加载器为 AppClassLoader,其 parentExtClassLoader,而 ExtClassLoader 的父加载器就是启动类加载器了

其中:

  • 启动类加载器 默认加载 <JAVA_HOME>/lib 目录下的能被虚拟机正确识别的类库
  • 扩展加载器 默认加载 <JAVA_HOME>/lib/ext 目录下的类库 可以看到,这两个都不是用来加载我们指定的文件的,加载 1.txt 只能是 AppClassLoader 的工作了。

因为父类加载器得到的 url 均为null,所以方法执行到 findresource(@R_696_8313@ 这一行 AppClassLoader 本身没有这个方法的实现类,这里追踪到其父类 URLClassLoader 的实现

public class URLClassLoader {
    //...

    public URL findresource(final String @R_696_8313@ {
        /*
         * 忽略这个方法,可以看到是交个成员变量 ucp 去找资源了
         */
        URL url = AccessController.doprivileged(
                new PrivilegedAction<URL>() {
                    public URL run() {
                        //交给 ucp 寻找
                        return ucp.findresource(name, truE);
                    }
                }, acc);

        return url != null ? ucp.checkURL(url) : null;
    }
}

这里的 ucp 变量,是个 URLClassPath 实例,继续往下追

public class URLClassPath {
    //...

    public URL findresource(String var1, Boolean var2) {
        int[] var4 = this.getLookupCache(var1);

        URLClassPath.Loader var3; //找到对应的Loader
        for (int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
            //让Loader去找资源
            URL var6 = var3.findresource(var1, var2);
            if (var6 != null) {
                //找到资源并返回
                return var6;
            }
        }

        return null;
    }
}

通过注释可以看到最终是通过 URLClassPath 的内部类 Loader 去定位的资源

这里介绍下 Loader 的两个实现类

  • JarLoader
  • FileLoader

到这里就不再往下追踪了,需要知道的是,打成Jar包后,文件的定位靠 JarLoader 来了

 private static class Loader implements Closeable {

    private final URL base;

    Loader(URL var1) {
        this.base = var1;
    }
}

static class JarLoader extends Loader {

    private final URL csu;

    JarLoader(URL var1, URLStreamHandler var2, HashMap<String, URLClassPath.Loader> var3, AccessControlContext var4) throws IOException {
        //这里设置 base url 的协议为 jar:
        super(new URL("jar", "", -1, var1 + "!/", var2));
        //..
    }

    //1
    URL findresource(String var1, Boolean var2) {
        //先获取resource, 找到 resource 获得其资源定位符 URL
        resource var3 = this.getresource(var1, var2);

        //返回 文件 url 给我们写的代码
        //返回 文件 url 给我们写的代码
        //返回 文件 url 给我们写的代码
        return var3 != null ? var3.getURL() : null;
    }

    //2
    resource getresource(String var1, Boolean var2) {
        //省略部分代码
        //其他不看,看这里,checkresource后会返回resource
        return this.checkresource(var1, var2, var3);

    }

    //3
    resource checkresource(final String var1, Boolean var2, final JarEntry var3) {
        final URL var4;

        //..

        //获取初始化时候设置的 base url ,其协议为 jar,并重新封装目标 url,然后赋值给下面的 resource 实例
        var4 = new URL(this.getBaseURL(), ParseUtil.encodePath(var1, falsE));

        //..

        //返回资源
        return new resource() {

            public URL getURL() {
                //上述的封装的目标 url
                return var4;
            }
            // ..
        };
    }
}

所以我们获取到的资源定位就是以 jar: 开头的了

3.2.2 打 jar 包后,jar 包内资源为什么不能读取了

显而易见,对于 File 类来说,单个的 jar 文件,既是一个 File, 那么,再通过一个 File 去描述一个文件内部的 File 是不太合适的。

这有点像压缩文件一样:你不能直接操作压缩包内的文件。

那么,该如何快速方便地读取 jar 包内我们想要操作的文件(证书、固定配置)呢?

3.2.3 打 jar 包后,jar 包内资源该怎么读取

答案是,用流的形式,只要稍微改写就可以了,请看如下demo

public class Classresource{

    //以流的形式读取文件
    public static void readFileByStream(String path) {
        System.out.println("[getresourceAsStream] 读取文件:" + path);
        InputStream inputStream = null;
        try {
            //获得jar包内的文件的流
            inputStream = Classresource.class.getresourceAsStream(path);
            read(inputStream);
        } catch (IOException E) {
            e.printStackTrace();
        }
    }

    //输出文件内容
    private static void read(InputStream resourcE) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i;
        while ((i = resource.read()) != -1) {
            baos.write(i);
        }
        System.out.println(new String(baos.toByteArray(), StandardCharsetS.UTF_8));
    }

}

//jar 启动类
public class Main {

    public static void main(String[] args) {
        System.out.println("#############打包后Classresource开始读取文件############");
        //注意这里的文件名,因为仍然是使用 Class.getresourceXxxx(),所以文件名解析路径方式仍然不变
        //跟上述章节保持一致
        Classresource.readFileByStream("/1.txt");
    }
}

输出结果:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

可以看到,是能够正常读取 jar 内部文件的内容的

3.2.4 jar 包内资源的其他读取方法

也可以使用 JarFile 的形式去读取 jar 包内的资源,这种适合读取别的 jar 包内的资源,这里就不再介绍,感兴趣的同学可以自行百度。

3.3 SpringBoot JAR 包的文件加载方式

Spring boot 项目打包后不同于普通的 jar 包目录结构

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

执行原有jar读取方式代码

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
		readFileByStream("/1.txt");
	}
 //以流的形式读取文件
    public static void readFileByStream(String path) {
        System.out.println("[getresourceAsStream] 读取文件:" + path);
        InputStream inputStream = null;
        try {
            //获得jar包内的文件的流
            inputStream = Classresource.class.getresourceAsStream(path);
            read(inputStream);
        } catch (IOException E) {
            e.printStackTrace();
        }
    }

    //输出文件内容
    private static void read(InputStream resourcE) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i;
        while ((i = resource.read()) != -1) {
            baos.write(i);
        }
        System.out.println(new String(baos.toByteArray(), StandardCharsetS.UTF_8));
    }
}

输出结果:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

可以看到,即使目录结构变了,Springboot jar 包也能正常读取到文件内容,这是因为,Spring boot 把如下两个目录添加到了 classPath 当中

  • BOOT-INF/classes
  • BOOT-INF/lib

Spring boot 额外提供了一种新的 jar 包内部的资源读取方式,即 ClassPathresource

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
        //使用SpringBoot的方式读取资源文件,这里不再以 ‘/’ 开头,类似ClassLoader加载资源的name写法
		ClassPathresource classPathresource = new ClassPathresource("1.txt");
		try {
			read(classPathresource.geTinputStream());
		} catch (IOException E) {
			e.printStackTrace();
		}
	}

	private static void read(InputStream resourcE) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		int i;
		while ((i = resource.read()) != -1) {
			baos.write(i);
		}
		System.out.println(new String(baos.toByteArray(), StandardCharsetS.UTF_8));
	}
}

执行结果:

jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了

四、总结

现今微服务大行其道,读取项目内的资源文件也常常在 SpringBoot jar中出现问题,这里使用 ClassPathresourceclass.getresourceAsStream()均可。

但是在企业提供高质量服务的目标下,应当把这些额外读取资源的需求,迁移到可配置化的环境当中,这样就能避免因改动配置引起的服务启停和中断。

本人才疏学浅,人微技轻,如有不妥之处,请留下宝贵批评指正。

大佬总结

以上是大佬教程为你收集整理的jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了全部内容,希望文章能够帮你解决jar、war 和 SpringBoot 加载包内外资源的方式总结,你再也不会出现FileNotFoundException了所遇到的程序开发问题。

如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。