Java查找配置文件那些事

空无

Java里经常需要查找解析配置文件,简单情况下还好,直接在ClassPath下查找即可;那么在某些复杂情况下,比如jar包内/外都有同样的配置文件时,查找策略时怎么样的呢?又如何定向查找Jar内/Jar外的配置文件呢?

在介绍查找配置文件之前,先来说下class path以及java程序的启动方式

class path

The class path is the path that the Java Runtime Environment (JRE) searches for classes and other resource files.

这是Oracle 官方对class path的说明,意思是class path是JRE搜索类和其他资源文件的路径

Java程序启动方式(Oracle JDK 8及之前版本)

Java程序都是通过java命令来启动的,可以执行一个Class文件/多个Class文件(目录)/一个Jar包/多个Jar包/多个Jar+多个Class文件(目录)的混合方式

java命令的官方手册(以下是Oracle JDK7,JDK8下有些说明不全):

Name
       java - the Java application launcher

SYNOPSIS
       java [ options ] class [ argument ... ]
       java [ options ] -jar file.jar [ argument ... ]

          options
             Command-line options. See Options.

          class
             The name of the class to be called.

          file.jar
             The name of the JAR file to be called. Used only with the -jar command.

          argument
             The arguments passed to the main function.

OPTIONS(Standard Options)
       The launcher has a set of standard options that are supported in the current runtime environment.

       In addition, the current implementations of the virtual machines support a set of nonstandard options that are subject to change in future releases. See Nonstandard Options.

    -classpath classpath, -cp classpath
             Specifies a list of directories, JAR files, and ZIP archives to search for class files. Separate class path entries with colons (:). Specifying -classpath or -cp overrides any setting of the CLASSPATH environment variable.
             If -classpath and -cp are not used and CLASSPATH is not set, then the user class path consists of the current directory (.).
             As a special convenience, a class path element that contains a base name of * is considered equivalent to specifying a list of all the files in the directory with the extension .jar or .JAR.  A  Java  program  cannot  tell  the  difference
             between the two invocations.
             For  example, if directory mydir contains a.jar and b.JAR, then the class path element mydir/* is expanded to a A.jar:b.JAR, except that the order of jar files is unspecified. All jar files in the specified directory, even hidden ones, are
             included in the list. A class path entry consisting simply of * expands to a list of all the jar files in the current directory. The CLASSPATH environment variable, where defined, will be similarly expanded. Any class path wildcard  expan-
             sion occurs before the Java VM is started. No Java program will ever see wild cards that are not expanded except by querying the environment. For example, by calling System.getenv("CLASSPATH").
             
             这里可以指定目录,jar文件,zip压缩文件作为classpath
             
    -jar
             Executes a program encapsulated in a JAR file. The first argument is the name of a JAR file instead of a startup class name. For this option to work, the manifest of the JAR file must contain a line in the form Main-Class: classname. Here,
             classname identifies the class with the public static void main(String[] args) method that serves as your application’s starting point.
             When you use this option, the JAR file is the source of all user classes, and other user class path settings are ignored.
             JAR files that can be run with the java -jar option can have their execute permissions set so they can be run without using java -jar. See JAR File Overview at http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jarGuide.html

如果执行java命令前不指定classpath的话,那么classpath为当前执行命令的目录

执行一个Class

java mainclass(休要执行并定义了Main方法的类),例如

java HelloWorld

执行多个Class文件(目录)

执行多个Class,或者叫加载多个Class时,需要指定Class所在目录

java -cp "实际的classpath路径,多个则以分号分隔" mainclass,例如
java -cp "/path/to/classpath0;/path/to/classpath1;/path/to/classpath2" com.example.HelloWorld

执行单个Jar包

通过-jar执行单个jar包时,需要在jar包内的MANIFEST.MF中定义Main-Class:

META-INF/MANIFEST.MF

Manifest-Version: 1.0
Main-Class: HelloWorld1(全类名,包名+类名)

执行单个jar:

java -jar jarfile( 需要执行的jar包),例如
java -jar HelloWorld.jar 

多个Jar+多个Class文件(目录)

这里和执行多个Class文件/目录一样,通过-cp/-classpath/指定文件/目录

java -cp "class文件所在目录;jar文件路径",例如
java -cp "/path/to/file1.jar;/path/to/file2.jar;/path/to/file3.jar;/path/to/classpath"

这种方式也是通常的项目启动方式,classpath指定所有jar包的同时加载class文件的目录

当然在项目的启动脚本里,或者IDE中,会按照顺序扫描library下的所有jar包,将所有jar的绝对路径添加在classpath中

JDK API查找当前的ClassPath下文件

Java中加载ClassPath下配置文件,最常用的方式就是通过ClassLoader了:

URL file = ClassLoader.getSystemClassLoader().getResource("/path/to/file");

这里获取了系统默认的CLassLoader,然后使用SystemClassLoader来加载配置文件

当然,加载资源文件这一步操作也是parents delegate的(很多人翻译为双亲委派,不知道谁是第一个翻译的……parents怎么能翻译为双亲呢?而且ClassLoader都是“单亲”……就是优先使用父亲加载而已),优先使用parent classloader来加载配置文件

这里如果不是在自定义ClassLoader的环境下,会使用继承于URLClassLoader的ClassLoader

URLClassLoader:

public URL findResource(final String name) {
    /*
     * The same restriction to finding classes applies to resources
     */
    URL url = AccessController.doPrivileged(
        new PrivilegedAction<URL>() {
            public URL run() {
                //这里的ucp是URLClassPath
                return ucp.findResource(name, true);
            }
        }, acc);

    return url != null ? ucp.checkURL(url) : null;
}

在URLClassPath内部,维护了一个loaders列表(有序,根据classpath定义顺序),classpath下的每一个jar/目录和jdk下的jar都会在这个loaders中单独维护一个元素,

URLClassPath:

public URL findResource(String var1, boolean var2) {
    int[] var4 = this.getLookupCache(var1);

    URLClassPath.Loader var3;
    //这里遍历loaders来查找文件,找到第一个就退出
    for(int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
        URL var6 = var3.findResource(var1, var2);
        if (var6 != null) {
            return var6;
        }
    }

    return null;
}

上面的查找策略是找到第一个就退出,也就是说如果classpath下存在多个重复文件,会使用classpath中的第一个,这个策略在类加载上也是一样

如果想查找classpath下的所有文件(包括jar包内的文件),可以使用classLoader.getResources();这样返回的结果就是所有符合条件的文件,其实和上面的规则一样,通过所有的loader去查找,只是找到第一个之后不返回了而已

JDK API查找ClassPath下所有Jar内的ClassPath下文件

同上,需要注意同名(路径一致)文件只会查找第一个

Spring查找ClassPath下文件

Spring加载配置文件时,常用两种写法:

classpath:/path/to/path

这种是只查找当前classpath下的,即只加载当前classpath目录下的,classpath指定的jar包不会加载

classpath*:/path/to/path

这种是查找当前目录和classpath下所有资源文件的classpath

spring具体的做法是,根据classpath这个前缀的不同,使用不同的查找类。

*的,使用上面的ClassLoader查找方式,会通过每个jar的loader来查找资源文件

不带*的使用直接查找文件的方式,说白了就是绝对路径查找

spring加载配置文件的方式有很多,子类众多,上面只是说了最基本常用的方式

spring加载配置文件相关代码在spring-core: org.springframework.core.io(不同版本可能有一定区别)

查找SpringBoot(Executable Jar方式)下所有Jar内的ClassPath下文件

SpringBoot下的启动类是org.springframework.boot.loader.JarLauncher,在org.springframework.boot:spring-boot-loader中。除了JarLauncher和当前项目classpath相关的代码,其他所有jar都会被作为jar包,放在BOOT-INF/lib下,当前模块的代码会放在BOOT-INF/classes下

SpringBoot使用自定义的org.springframework.boot.loader.LaunchedURLClassLoader作为默认的ClassLoader,这个classloader会先加载SpringBoot相关的资源至urls内,然后后续所有的类/资源文件加载都会从urls内查找

所以就算在SpringBoot下,使用上述JDK/Spring中的查找方式时,策略也是一致的:

当前模块的文件使用classpath,jar内的文件使用classpath*

虽然在SpringBoot下获取的URL格式不同了,但并不影响使用,只需要通过URL对象就可以轻松的获取文件

但是对于一些不按套路出牌的查找方式,在SpringBoot下可能会出现找不到文件的情况,比如Mybatis的VFS

参考

阅读 1.5k

坚持原创,专注分享 JAVA、网络、IO、JVM、GC 等技术干货

2.7k 声望
3.5k 粉丝
0 条评论
你知道吗?

坚持原创,专注分享 JAVA、网络、IO、JVM、GC 等技术干货

2.7k 声望
3.5k 粉丝
宣传栏