前言
项目中经常会使用 properties 文件定义一些配置变量,相应的就需要写一个类来加载此配置。
常用的方式是使用 class 或者 classLoader 对象的getResourceAsStream 来加载properties文件。
eg:
GlobalConfig.class.getResourceAsStream("/properties/globalConfig.properties") GlobalConfig.class.getClassLoader().getResourceAsStream("properties/globalConfig.properties")
用Class或者ClassLoader 读取的区别是什么呢?相信眼尖的读者已经看出来了,就是指定的路径差了个 /。
路径是否有 “/” 到底有什么影响,能不能使用 GlobalConfig.class.getResourceAsStream(“properties/globalConfig.properties”)
或者 GlobalConfig.class.getClassLoader().getResourceAsStream("/properties/globalConfig.properties") 来读取配置呢?
带着种种疑问,下面就来探究一下这里面有什么猫腻。
测试环境:sping-boot项目(jar包)
项目结构:
深入源码探究:
Class类的 getResourceAsStream方法
查看Class类getResourceAsStream方法的源码,如下:
public InputStream getResourceAsStream(String name) { name = resolveName(name); ClassLoader cl = getClassLoader0(); //... return cl.getResourceAsStream(name); }
哦呵,看出来了吧,Class的getResourceAsStream方法调用的是ClassLoader的getResourceAsStream,Class这家伙真是够懒的。不过呢,调用之前也不是什么都不做,在调用前对name做了一些操作,即resolveName(name)。看来也不是真懒。
我们看看 resolveName 方法做了什么呢。
/** * Add a package name prefix if the name is not absolute * Remove leading "/" if name is absolute */ private String resolveName(String name) { //... if (!name.startsWith("/")) { // name不是以 "/" 开头 Class<?> c = this; while (c.isArray()) { c = c.getComponentType(); } String baseName = c.getName(); // 当前类的全限定名。eg:com.markix.config.GlobalConfig int index = baseName.lastIndexOf('.'); if (index != -1) { name = baseName.substring(0, index).replace('.', '/') +"/"+name; } } else { // name以 "/" 开头 name = name.substring(1); } return name; }
其实,resolveName方法上的注释已说明一切,硬翻译一波:**如果name不是绝对路径,则添加包路径;如果name是绝对路径,则删除最前面的"/"。**最终都会变成一个相对路径。
通过代码我们也能得出此结论,当name以‘/’开头,则执行 name = name.substring(1); 也就是截取掉开头的’/’。当name不是以“/”开头,则获取当前类class对象的name(即类的全路径名,包括包名),再截取包名替换成路径形式拼接到name的前缀。
举个栗子直观描述上面说的一坨东西:
绝对路径:GlobalConfig.class.getResourceAsStream("/properties/config.properties")
name 经过 resolveName 方法从 /properties/config.properties 变成 properties/config.properties
进而调用 ClassLoader类的getResourceAsStream(“properties/config.properties”)
相对路径:GlobalConfig类class.getResourceAsStream(“properties/config.properties”)
name 经过 resolveName 方法从原本的 properties/config.properties 变成 com/markix/config/properties/config.properties
进而调用 ClassLoader类的getResourceAsStream(“com/markix/config/properties/config.properties”)
小结
调用 类名.class.getResourceAsStream("/路径") 等价于调用 类名.class.getClassLoader().getResourceAsStream("路径")
调用 类名.class.getResourceAsStream("路径") 等价于调用 类名.class.getClassLoader().getResourceAsStream("类路径 + 路径")
过渡一句,class的getResourceAsStream本质就是调用classLoader的getResourceAsStream,下面探究下classLoader的getResourceAsStream。
ClassLoader类的getResourceAsStream方法
查看ClassLoader类getResourceAsStream方法的源码,呃,遇到难题了,有多个实现类重写了getResourceAsStream,到底是哪个类?
这里关乎Java类加载器的知识,不了解的请先自觉补姿势。博主直接抛结论啦,一般我们应用类运行都是使用 AppClassLoader 加载的,其继承自 URLClassLoader,所以我们查看URLClassLoader的getResourceAsStream方法,如下:
public InputStream getResourceAsStream(String name) { URL url = getResource(name); try { if (url == null) { return null; } URLConnection urlc = url.openConnection(); InputStream is = urlc.getInputStream(); //... return is; } catch (IOException e) { return null; } }
核心就是调用了 getResource 方法,接着获取流就返回了。继续看getResource方法,在ClassLoader类中:
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; }
和类加载类似,采用双亲委托。如果有parent,就先调用父加载器的方法。
我们的资源在我们项目中,其实最终调用的是findResource方法进行查找,代码如下:
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() { return ucp.findResource(name, true); } }, acc); return url != null ? ucp.checkURL(url) : null; }
点到为止哈哈,有兴趣自行debug哈(再深入编不下去了哈哈)。通过debug调试,总结一下结论。
小结
调用 类名.class.getClassLoader().getResourceAsStream("/路径") ,总是返回 null。’/’ 不可访问。
调用 类名.class.getClassLoader().getResourceAsStream("路径") ,会在运行时环境(ClassPath路径)下搜索指定的“路径”。(也会在依赖jar包中搜索)
举个栗子
我的项目存放在 E:\WorkSpace\IDEA\spring-boot-demo,
编译目录为 E:\WorkSpace\IDEA\spring-boot-demo\target\classes\。(这个即为ClassPath路径)
我是直接在IDEA运行的,运行时会加载编译目录的内容,当调用
GlobalConfig.class.getClassLoader().getResourceAsStream("properties/globalConfig.properties") 时,也就会在 E:\WorkSpace\IDEA\spring-boot-demo\target\classes\ 目录查找一下 properties\globalConfig.properties 文件是否存在,找不到则报错,找得到就返回了。
总结
Class.getResourceAsStream 本质就是调用 ClassLoader.getResourceAsStream。
调用 类名.class.getResourceAsStream("/路径") 等价于调用 类名.class.getClassLoader().getResourceAsStream("路径")
调用 类名.class.getResourceAsStream("路径") 等价于调用 类名.class.getClassLoader().getResourceAsStream("类路径 + 路径")
ClassLoader.getResourceAsStream 会在运行时环境(ClassPath路径)下搜索指定的“路径”。(该路径必须是相对路径,绝对路径总是返回 null。’/’ 不可访问。)
另外:tomcat的特殊处理
在tomcat容器环境中,调用类名.class.getClassLoader().getResourceAsStream("/路径") 并不是返回null。原因在于tomcat重写了ClassLoader机制,war项目运行时并不是使用AppClassLoader加载,而是使用tomcat自定义的WebappClassLoader类,其父类WebappClassLoaderBase重写了getResourceAsStream方法,对路径做了特殊处理,最终实现了调用 类名.class.getClassLoader().getResourceAsStream("/路径") 等价于调用 类名.class.getClassLoader().getResourceAsStream("路径") 。
详见 WebappClassLoaderBase的getResourceAsStream方法。
————————————————
版权声明:本文为CSDN博主「markix」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_31772441/article/details/106413808