9.7 加载资源

类加载器除了可以加载Java类之外,还可以加载与Java类相关的资源文件,如文本文件、图片文件和音频文件等。Java程序在运行过程中需要访问这些资源文件的内容。每个资源文件通过名称来标识。资源名称可以由多个部分组成,每个部分之间用“/”分隔,如一个图片文件的资源名称可以是“resources/images/logo.gif”。资源名称的表示形式是平台无关的,由具体的实现负责把平台无关的抽象资源名称映射到实际的资源保存方式上。如果资源是保存在文件系统上的,通常是映射到资源文件的路径,而资源名称中每个用“/”分隔的部分代表一个目录。这些资源文件通常与class文件保存在同一个目录下,或者同一个jar包中。

使用类加载器来加载资源的好处是可以解决资源文件存放时路径不固定的问题。比如,一个Java类在运行时需要读取保存配置信息的属性文件的内容,如果使用Java的文件操作API来读取,就要知道属性文件的绝对路径。Java类的class文件与属性文件的相对位置虽然固定,但是它们所在的绝对路径是不确定的。通过文件操作API无法保证总是能正确地找到文件,通过类加载器提供的加载资源的方法则可以正确地加载到相关的资源。

ClassLoader类中负责加载资源的方法是getResource和getResourceAsStream。根据资源名称,前者得到表示资源路径的java.net.URL类的对象,后者得到用来读取资源内容的java.io.InputStream类的对象。从实现上来说,getResourceAsStream方法在内部先调用getResource方法得到URL类的对象,再调用该对象的openStream方法获取InputStream类的对象。一般对资源进行读取操作时,getResourceAsStream方法的使用频率较高。在进行查找时,getResource方法会先检查当前类加载器的双亲类加载器是否为null。如果不为null,则调用双亲类加载器的getResource方法来进行查找;如果为null,则通过启动类加载器来查找。如果找不到对应的资源,则调用ClassLoader类的findResource方法进行查找。这种实现方式类似于ClassLoader类在加载Java类时默认的双亲优先的代理模式。在ClassLoader类中声明为protected的findResource方法的作用类似于findClass方法。如果类加载器有自定义的资源查找机制,那么需要覆写此方法。

除了getResource方法之外,ClassLoader类中还有一个相关的getResources方法。这个方法会根据资源名称返回所有具有该名称的资源文件。该方法的返回值是一个可以遍历所有查找结果的java.util.Enumeration类的对象。在实现上,getResources方法的机制与getResource方法是一样的,都是先通过双亲类加载器进行查找,只不过getResources方法会完成整个查找过程来搜索所有满足条件的资源,而不是像getResource方法一样查找到第一个满足条件的结果就返回。ClassLoader类中也有与findResource方法作用相似的findResources方法,用来与getResources方法配合使用。在某些情况下,可能会需要查找在不同位置上的所有同名资源,此时可以使用getResources方法。

当需要使用系统类加载器来加载资源时,可以直接使用ClassLoader类中的静态方法getSystemResource、getSystemResourceAsStream和getSystemResources。这3个方法在实现上是先得到系统类加载器,再调用系统类加载器的对应方法来进行加载。如果当前系统类加载器为null,则通过启动类加载器来进行加载。代码清单9-15给出了使用类加载器来加载属性文件的示例。

代码清单9-15 使用类加载器来加载属性文件的示例


public class LoadResource{

public Properties loadConfig()throws IOException{

ClassLoader loader=this.getClass().getClassLoader();

InputStream input=loader.getResourceAsStream("com/java7book/chapter9/config.properties");

if(input==null){

throw new IOException("找不到配置文件。");

}

Properties props=new Properties();

props.load(input);

return props;

}

}


在使用类加载器的getResource和getResourceAsStream方法时,需要注意的是使用正确的资源名称。代码清单9-15给出了资源名称的通常表示形式,即根据资源文件所在的Java包名来确定。配置文件config.properties在com.java7book.chapter9包中,把包名中的“.”变成“/”之后再加上文件的文件名就得到了资源名称。资源名称需要根据资源文件所在的包名进行调整。

除了使用ClassLoader类中的方法来加载资源之外,Class类中也有相关的方法来加载资源。Class类中的方法与ClassLoader类中的方法在名称上是相同的,分别是getResource和getResourceAsStream。Class类中的这两个方法的实现是这样的,先通过getClassLoader方法得到加载当前类的ClassLoader类的对象,再通过ClassLoader类的对象的对应方法来进行加载。如果getClassLoader方法的返回值为null,则使用ClassLoader类中的getSystemResource和getSystemResourceAsStream方法来进行加载,相当于使用系统类加载器来进行加载。不过Class类中的方法在调用ClassLoader类的对应方法之前,会进行资源名称的转换。如果资源名称以“/”开头,则会去掉开头的“/”;否则自动在资源名称前加上Class类的对象所在的包的名称。对于代码清单9-15的示例,如果使用代码“this.getClass().getResourceAsStream”来进行加载,那么可以直接使用资源名称“config.properties”,而不需要加上前面的包名,包名的添加工作由Class类来完成。使用Class类中的加载资源的方法比使用ClassLoader类中的相关方法要实用一些。在重构的过程中,可能对包含资源文件的包名进行了修改。如果使用ClassLoader类中的方法,则需要手动修改加载时使用的资源名称,而使用Class类中的方法则不需要进行修改。