java加载jar包下的资源文件过程及原理分析

最近遇到一个这样的问题,项目是一个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.
 *
 * <p> The name of a resource is a '<tt>/</tt>'-separated path name that
 * identifies the resource.
 *
 * <p> This method will first search the parent class loader for the
 * resource; if the parent is <tt>null</tt> 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.  </p>
 *
 * @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 <tt>URL</tt> object for reading the resource, or
 *          <tt>null</tt> 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;
}

jdk的开发者还为我们留下了注释,注意这段注释:


* <p> This method will first search the parent class loader for the
 * resource; if the parent is <tt>null</tt> 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.  </p>

通过代码和注释我们可以得知此代码会先去父节点的loader去加载资源文件,如果找不到,则会去BootstrapLoader中去找,如果还是找不到,才调用当前类的classLoader去找。这也就是我们有时说的所谓的双亲委派模型。

(双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载)


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:<url>!/{entry} )。所以,如果jar包中的类源代码用File f=new File(相对路径);的形式,是不可能定位到文件资源的

为此我专门写了个测试代码:


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);
            Iterator<String> keys = 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.<init>(FileInputStream.java:138)
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去加载。

猜你喜欢

转载自my.oschina.net/spinachgit/blog/1813941