最近遇到一个这样的问题,项目是一个spring cloud的项目,一个主模块(记为mainMoudle)依赖了另一个子模块(记为subMoudle)。在开发过程中,在idea开发工具环境下是能正常运行的,但由于测试时,需要将模块打包,就将subMoudle工程打成了一个jar放在mainMoudle下,跑jar包时就发现不能运行了,控制台抛出了fileNotFoundException的异常信息。
通过查看subMoudle下的代码排查问题时,我发现是由于subMoudle在初始化时,需要加载mainMoudle中的配置文件。加载的代码是通过File类直接加载的,在开发环境时,运行时是直接将工程的资源文件编译到target的classes目录下的, 所以在开发环境下是可以正常运行的。而当项目打成了一个jar包时运行时,jar包中的资源文件不会再自动解压释放到目录中的,因为它已经编译好了,java也已经它成了class字节码文件了。所以再通过原来的File直接读取jar下的一个文件时是读取不到的,故问题就出现在这里。那么我们该如何去解决这个问题呢,当时为了方便我直接用的apache的commons-configuration包来解决的,将subMoudle的中对于读取配置文件的代码进行了替换,对于要读取配置文件的代码全改成了configuration的代码来读取,问题就解决了。
他们都说知道如何解决一个问题是一个初级程序员的该干的事,作为一个中高级程序员就必须得要了解其原理了,我觉得很有道理。于是我通过在idea下的断点进行了分析,找到了关键代码
static URL locateFromClasspath(String resourceName) { URL url = null; ClassLoader loader = Thread.currentThread().getContextClassLoader(); if (loader != null) { url = loader.getResource(resourceName); if (url != null) { LOG.debug("Loading configuration from the context classpath (" + resourceName + ")"); } } if (url == null) { url = ClassLoader.getSystemResource(resourceName); if (url != null) { LOG.debug("Loading configuration from the system classpath (" + resourceName + ")"); } } return url; }
首先它通过获取了当前线程的一个类加载器,通过加载器的getResouce方法去类加载器找到resourceName这个文件
loader.getResouce的代码属于JDK的代码,其getResouce这个方法代码为:
// -- Resource -- /** * Finds the resource with the given name. A resource is some data * (images, audio, text, etc) that can be accessed by class code in a way * that is independent of the location of the code. * *jdk的开发者还为我们留下了注释,注意这段注释:The name of a resource is a '/'-separated path name that * identifies the resource. * *
This method will first search the parent class loader for the * resource; if the parent is null the path of the class loader * built-in to the virtual machine is searched. That failing, this method * will invoke {@link #findResource(String)} to find the resource. * * @apiNote When overriding this method it is recommended that an * implementation ensures that any delegation is consistent with the {@link * #getResources(java.lang.String) getResources(String)} method. * * @param name * The resource name * * @return A URL object for reading the resource, or * null if the resource could not be found or the invoker * doesn't have adequate privileges to get the resource. * * @since 1.1 */ public URL getResource(String name) { URL url; if (parent != null) { url = parent.getResource(name); } else { url = getBootstrapResource(name); } if (url == null) { url = findResource(name); } return url; }
*通过代码和注释我们可以得知此代码会先去父节点的loader去加载资源文件,如果找不到,则会去BootstrapLoader中去找,如果还是找不到,才调用当前类的classLoader去找。这也就是我们有时说的所谓的 双亲委派模型。This method will first search the parent class loader for the * resource; if the parent is null the path of the class loader * built-in to the virtual machine is searched. That failing, this method * will invoke {@link #findResource(String)} to find the resource.
(双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载)
public InputStream getInputStream(URL url) throws ConfigurationException { File file = ConfigurationUtils.fileFromURL(url); if (file != null && file.isDirectory()) { throw new ConfigurationException("Cannot load a configuration from a directory"); } else { try { return url.openStream(); } catch (Exception var4) { throw new ConfigurationException("Unable to load the configuration from the URL " + url, var4); } } }
当资源被找到后,通过调用url的openStream()方法去获得此文件的输入流
因此,单纯地用File去去读取jar包的文件是不能的,因为!并不是文件资源定位符的格式 (jar中资源有其专门的URL形式: jar:
为此我专门写了个测试代码:
import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import java.io.*; import java.net.URL; import java.util.Iterator; public class ResourceReader { private static final String subMoudlePropertiesFile = "sys.properties";//jar下的配置文件 private static final String innerPropertiesFile = "own.properties";//内部配置文件 public static void main(String[] args) throws InterruptedException { loadJarFileByConfiguration(); Thread.sleep(1000); loadLocalFile(); Thread.sleep(1000); loadJarFileByResource(); Thread.sleep(1000); loadJarFileByFile(); } /** * 通过File类去加载jar包的资源文件 */ private static void loadJarFileByFile() { System.out.println("----------loadJarFileByFile---- begin------------"); URL resource = ResourceReader.class.getClassLoader().getResource(subMoudlePropertiesFile); String path = resource.toString(); System.out.println(path); try { File file = new File(path); FileInputStream fileInputStream = new FileInputStream(file); BufferedReader br = new BufferedReader(new InputStreamReader(fileInputStream)); String s = ""; while ((s = br.readLine()) != null) System.out.println(s); } catch (Exception e) { e.printStackTrace(); } System.out.println("----------loadJarFileByFile---- end------------\n\n"); } /** * 通过apache configuration包读取配置文件 */ private static void loadJarFileByConfiguration() { System.out.println("----------loadJarFileByConfiguration---- begin------------"); try { Configuration configuration = new PropertiesConfiguration(subMoudlePropertiesFile); Iteratorkeys = configuration.getKeys(); while (keys.hasNext()) { String next = keys.next(); System.out.println("key:" + next + "\tvalue:" + configuration.getString(next)); } } catch (ConfigurationException e) { e.printStackTrace(); } System.out.println("----------loadJarFileByConfiguration---- end------------\n\n"); } /** * 通过类加载器去的getResource方法去读取 */ private static void loadJarFileByResource() { System.out.println("----------loadJarFileByResource---- begin------------"); URL resource = ResourceReader.class.getClassLoader().getResource(subMoudlePropertiesFile); String path = resource.toString(); System.out.println(path); try { InputStream is = resource.openStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String s = ""; while ((s = br.readLine()) != null) System.out.println(s); } catch (Exception e) { e.printStackTrace(); } System.out.println("----------loadJarFileByResource---- end------------\n\n"); } /** * 读取当前工程中的配置文件 */ private static void loadLocalFile() { System.out.println("----------loadLocalFile---- begin------------"); String path = ResourceReader.class.getClassLoader().getResource(innerPropertiesFile).getPath(); System.out.println(path); try { FileReader fileReader = new FileReader(path); BufferedReader bufferedReader = new BufferedReader(fileReader); String strLine; while ((strLine = bufferedReader.readLine()) != null) { System.out.println("strLine:" + strLine); } } catch (Exception e) { e.printStackTrace(); } System.out.println("----------loadLocalFile---- begin------------\n\n"); } }
子模块结构为:
sys.properties位于subMoudle的jar中
以上代码运行结果为:
----------loadJarFileByConfiguration---- begin------------
log4j:WARN No appenders could be found for logger (org.apache.commons.configuration.PropertiesConfiguration).
log4j:WARN Please initialize the log4j system properly.
key:username value:haiyangge
key:password value:haiyangge666
----------loadJarFileByConfiguration---- end------------
----------loadLocalFile---- begin------------
/E:/idea_space/spring_hello/target/classes/own.properties
strLine:db.username=9527
strLine:db.password=0839
----------loadLocalFile---- begin------------
----------loadJarFileByResource---- begin------------
jar:file:/E:/idea_space/spring_hello/libs/subMoudle-1.0-SNAPSHOT.jar!/sys.properties
username=haiyangge
password=haiyangge666
----------loadJarFileByResource---- end------------
----------loadJarFileByFile---- begin------------
java.io.FileNotFoundException: jar:file:\E:\idea_space\spring_hello\libs\subMoudle-1.0-SNAPSHOT.jar!\sys.properties (文件名、目录名或卷标语法不正确。)
jar:file:/E:/idea_space/spring_hello/libs/subMoudle-1.0-SNAPSHOT.jar!/sys.properties
----------loadJarFileByFile---- end------------
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.
at javafile.read.ResourceReader.loadJarFileByFile(ResourceReader.java:35)
at javafile.read.ResourceReader.main(ResourceReader.java:22)
从运行结果可以看出,通过file类去加载本项目中的资源文件是可以成功的,但加载jar下的资源文件是不可以的,因为jar!sys.properties不是文件资源定位符的格式,而是jar中的.
故加载jar包内的资源文件时,应该用classLoader的getResource方法去加载,获取到URL后,用openStream()方法打开流,不应该原生的file去加载。