jar中资源无法找到报 FileNotFoundException的深度(深入JDK)解析及其终极解决办法

授之于鱼不如授之于渔今天我就带着大家一起来探寻jar中资源无法找到报 FileNotFoundException 异常。

如果你不想看下面繁琐的调试那你就记住:

  我们在编写程序最好不要用***.calss.getResource(path)得到URL之后再去创建文件再去创建流来得到配置,这样在jar中会报错而且执行的步骤多。最好直接用***.calss.getResourceAsStream(path)获取流然后直接加载配置文件。

  也就是以后获取jar中的资源时用

 prop.load(你自己的类.class.getResourceAsStream("资源路径"));  或者
 prop.load(你自己的类.class.getClassLoader.getResourceAsStream("资源路径")); 
 不要用

URL url = 你自己的类.class.getResource("资源路径");
File file=new File(URLDecoder.decode(url.getPath(),"utf-8"));
prop.load(new FileInputStream(file));
  即在获取jar中的资源时不要尝试获取文件File,要直接获取流InputAsStream来加载配置

  但是提倡看一下下面的调试。

一.业务场景。

      现在我手里有一个项目Tank其中有个tank.properties文件存放着 initNum=30即我们坦克初始化的数目。模块目录如下(我们用IDEA因为IDEA调试功能比eclipse强大)

其中FileOprationTool,GetPropertiesValue两个类工具类

FileOprationTool的源码:

package com.hanghang.tool;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;

public class FileOprationTool {
	public static File getFileUnderPackpage(String name) {
		File file = null;
		URL url = FileOprationTool.class.getResource(name);
		try {
			file = new File(URLDecoder.decode(url.getPath(), "utf-8"));
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return file;
	}

	public static URL getURLUnderPackpage(String name) {
		URL url = FileOprationTool.class.getResource(name);
		try {
			url = new URL(URLDecoder.decode(url.toString(), "utf-8"));
		} catch (MalformedURLException | UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
 		return url;
	}
}

GetPropertiesValue类的源码:

package com.hanghang.tool;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;

public class GetPropertiesValue {
    private static Properties prop = new Properties();

    // 不让别人创建对象
    private GetPropertiesValue() {

    }

    public static String getValue(String key, String path) {
        try {
        1.prop.load(GetPropertiesValue.class.getResourceAsStream(path));
	2.prop.load(new FileInputStream(FileOprationTool.getFileUnderPackpage("/com/hanghang/file/tank.properties")));
        } catch (FileNotFoundException e) {

            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return prop.getProperty(key);
    }
}

打包后用GetPropertiesValue中的1得不到tank.properties文件,而GetPropertiesValue的2可以到得到tank.properties。打包前两者都可以用

二.原因和解决办法.

1. 打包前,下断点深入调试jdk源码


进入断点


可以看出java加载资源是通过类加载器加载的,其中Launcher$AppClassLoader@1385是系统类加载器 继续进入断点


这里说明:

当一个 JVM启动的时候,Java缺省(默认)开始使用如下三种类型类装入器:

根(Bootstrap ClassLoader)类加载器Bootstrap ClassLoader 被称为引导(原始或根)类加载器是用本地代码实现的类装入器,它负责将<Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

扩展(Extension ClassLoader)类加载器:扩展类加载器是由SunExtClassLoadersun.misc.Launcher$ExtClassLoader实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器

系统(System  ClassLoader)类加载器系统类加载器是由 Sun AppClassLoadersun.misc.Launcher$AppClassLoader对应上面调试的类)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器来加载类或者是资源

其中除了根加载器为他们都是ClassLoader子类的实例,开发者可以通过拓展ClassLoader的子类来实现自己的类加载机制。

加载机制是:

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器(这里的父子关系不是继承上的,这里的父子关系是类加载器实例之间的关系),依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载

这里听不懂没事接着进入断点


   这里我们要注意上面两张图的第一张图的URLClassLoader类的实例是AppClassLoader类的实例(在到了这个AppClassLoader调试之前 已经通过Bootstrap ClassLoader(这个找不到类是JVM实现的)和ExtClassLoader类的findResource查找资源了只是前两个没有查找的到,所以最后用“子类”AppClassLoader来查找,这个和我们的前面说的类加载机制是一样的),其中URLClassPath是URLClassLoader 的ucp属性。

  此时(URLClassLoader实例是AppClassLoader类的实例)我们看到对应ucp属性对应URLClassPath类中有一下几个属性:


其中path中有以下路径:


   其中file:/D:/%e6%9d%a8%e5%8f%ac%e6%88%90/IntelliJ%20IDEA%20WorkSpace/JavaSE/out/production/Tank/ 是我当前模块 的工作目录。这个目录下面有/com/hanghang/file/tank.properties文件。URLClassPath对象的findResource这个函数会从AppClassLoader加载器的加载路径(path)下一个个找,其中也会在jar中找。直到找到为止再返回URL,最后我们代码的URL如下图。


url为file:/D:/%e6%9d%a8%e5%8f%ac%e6%88%90/IntelliJ%20IDEA%20WorkSpace/JavaSE/out/production/Tank/com/hanghang/file/tank.properties

此时通过prop.load(new FileInputStream(FileOprationTool.getFileUnderPackpage("/com/hanghang/file/tank.properties")));可以得到配置,其中new FileInputStream传入的是文件FIle对象

2.现在将其打包成jar包再建立一个模块叫TankJarTest,这个模块来调用刚刚所打的包Tank.jar


调试与上面的一样此处略,得到的url为


url为jar:file:/D:/%e6%9d%a8%e5%8f%ac%e6%88%90/IntelliJ%20IDEA%20WorkSpace/JavaSE/out/artifacts/Tank_jar/Tank.jar!/com/hanghang/file/tank.properties

解码后是file:/D:/杨召成/IntelliJ IDEA WorkSpace/JavaSE/out/artifacts/Tank_jar/Tank.jar!/com/hanghang/file/tank.properties。之后再通过prop.load(new FileInputStream(FileOprationTool.getFileUnderPackpage("/com/hanghang/file/tank.properties")));就得不到配置了

原因:

首先jar是个文件,在文件里不能通过路径查找资源。再者通过文件路径查找也没有这个路径的文件所以会报错

3.解决办法

GetPropertiesValue.class.getResourceAsStream(path)
方法加载配置文件,修改之后重新打包重复第二次的调试


断点进入URLClassLoader.class的getResourceAsStream方法中


其中getResource(name)的与上面调试过程一模一样。关键是下面的代码

   URLConnection urlc = url.openConnection();
            InputStream is = urlc.getInputStream();
            if (urlc instanceof JarURLConnection) {
                JarURLConnection juc = (JarURLConnection)urlc;
                JarFile jar = juc.getJarFile();
                synchronized (closeables) {
                    if (!closeables.containsKey(jar)) {
                        closeables.put(jar, null);
                    }
                }
            } else if (urlc instanceof sun.net.www.protocol.file.FileURLConnection) {
                synchronized (closeables) {
                    closeables.put(is, null);
                }
            }
            return is;
        } catch (IOException e) {
            return null;
        }

通过urlc instanceof JarURLConnection判断出了,是在jar中查找资源还是用在file中查找资源。如果是JarURLConnection连接,则用juc.getJarFile()方法得到文件从而得到对应的流。如果你继续debug下去你会发现JDK是通过ZipFile这个类的getInputStream方法得到的。不再调试下去了,下面是ZipFile的一些算法太难了。

最后回到我们的代码中

prop.load(GetPropertiesValue.class.getResourceAsStream(path));

此时就可以得到配置了

总结:

现在我们知道哪里错了,我们在编写程序最好不要用***.calss.getResource(path)得到URL之后再去创建文件再去创建流来得到配置,这样在jar中会报错而且执行的步骤多。最好直接用***.calss.getResourceAsStream(path)获取流然后直接加载配置文件。

解决办法:

1:用prop.load(GetPropertiesValue.class.getResourceAsStream(path))直接获取流

2:通过URL url = ****.class.getResource(name);

    URLConnection urlc = url.openConnection();

    InputStream is = urlc.getInputStream();

    或直接用

    URL url = ****.class.getResource(name);

    InputStream is = url.openStream();

两者原理一样

得到的经验:

一般获取jar之外的资源可以通过得到url或path来建立File,但是如果要把模块打成 jar包之后再获取相应的资源要通过上面两种方法得到资源。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值