授之于鱼不如授之于渔今天我就带着大家一起来探寻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)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
系统(System ClassLoader)类加载器:系统类加载器是由 Sun的 AppClassLoader(sun.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包之后再获取相应的资源要通过上面两种方法得到资源。