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去加载。

  • 21
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
Java的ServiceLoader是一个用于加载服务提供者的工具类。服务提供者是指实现了某个特定服务的类,它们通常会被打包成jar文件,并在应用程序运行时动态加载。 ServiceLoader的实现原理是基于Java SPI机制,它通过查找META-INF/services目录下的配置文件加载服务提供者,配置文件的格式为: ``` com.example.MyService ``` 其中,com.example.MyService是服务提供者的全限定类名。 接下来,我们来分析一下ServiceLoader的源码实现: 1. ServiceLoader的构造函数 ``` private ServiceLoader(Class<S> service, ClassLoader loader) { this.service = Objects.requireNonNull(service, "Service interface cannot be null"); this.loader = (loader == null) ? ClassLoader.getSystemClassLoader() : loader; acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; reload(); } ``` 构造函数接收服务提供者的接口类和类加载器,然后进行初始化,并调用reload()方法加载服务提供者。 2. ServiceLoader的reload()方法 ``` private void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); } ``` reload()方法会清空已加载的服务提供者,并创建一个新的LazyIterator对象。 3. ServiceLoader的iterator()方法 ``` public Iterator<S> iterator() { return new Iterator<S>() { private final LazyIterator<S> knownProviders = lookupIterator; private Iterator<S> serviceProviderIterator = Collections.emptyIterator(); public boolean hasNext() { if (serviceProviderIterator.hasNext()) { return true; } if (knownProviders.hasNext()) { serviceProviderIterator = knownProviders.next().iterator(); return hasNext(); } return false; } public S next() { if (!hasNext()) { throw new NoSuchElementException(); } return serviceProviderIterator.next(); } public void remove() { serviceProviderIterator.remove(); } }; } ``` iterator()方法会返回一个迭代器对象,用于遍历已加载的服务提供者。该迭代器内部维护了一个knownProviders对象和一个serviceProviderIterator对象。在遍历时,先判断serviceProviderIterator是否还有下一个元素,如果有则直接返回,否则就从knownProviders中加载下一个服务提供者,并将其迭代器赋值给serviceProviderIterator。 4. ServiceLoader的LazyIterator类 ``` private static class LazyIterator<T> implements Iterator<T> { Class<T> service; ClassLoader loader; Enumeration<URL> configs = null; Iterator<String> pending = null; String nextName = null; private LazyIterator(Class<T> service, ClassLoader loader) { this.service = service; this.loader = loader; } public boolean hasNext() { if (nextName != null) { return true; } if (configs == null) { try { String fullName = "META-INF/services/" + service.getName(); if (loader == null) { configs = ClassLoader.getSystemResources(fullName); } else { configs = loader.getResources(fullName); } } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while (pending == null || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; } public T next() { if (!hasNext()) { throw new NoSuchElementException(); } String cn = nextName; nextName = null; try { return service.cast(Class.forName(cn, true, loader).newInstance()); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } catch (Exception x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); } public void remove() { throw new UnsupportedOperationException(); } } ``` LazyIterator类实现了Iterator接口,它负责从配置文件中解析出服务提供者的类名,并进行实例化。在hasNext()方法中,首先判断nextName是否已经被赋值,如果是则直接返回true,否则就从配置文件中解析出下一个类名。在next()方法中,将nextName赋值给cn变量,然后通过反射加载并实例化服务提供者类,最后返回实例化对象。 以上就是ServiceLoader的源码实现分析。通过分析,我们可以了解到ServiceLoader的实现原理,以及如何通过配置文件加载服务提供者。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水中加点糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值