再学习MyBatis的时候学习到的一个小知识点。使用MyBatis需要先加载全局配置文件。然后在程序中将该文件的资源路径传递过去。由于习惯的原因,刚开始的时候写的路径如下:
String resource = "/SqlMapConfig.xml";
InputStream inStream = Resources.getResourceAsStream(resource);
运行程序发现报错:资源没找到。
点进去源码是这样定义的:
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
if (null != cl) {
// try to find the resource as passed(尝试着获取资源)
InputStream returnValue = cl.getResourceAsStream(resource);
// now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource
//现在有些类加载器需要前导 /
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
可以看到MyBatis使用的是类加载器获取流对象。不过之前在编程中我一直使用的是字节码对象的getResourceAsStream方法。
现在验证一下两者的区别(src目录下创建一个xml文件):
现在通过两种方式获取该资源流:
public static void main(String[] args) {
InputStream inStream = Test02.class.getResourceAsStream("/User.xml");
System.out.println(inStream); //获取到流对象
inStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("/User.xml");
System.out.println(inStream); //输出null
}
对比两个方法的源码:
类加载器:
public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
它是直接通过传递的路径信息获取资源的URL,如果URL不为null,则打开流。再对比字节码对象中的源码:
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}
发现它底层也是调用的类加载器的getResourceAsStream方法,只不过在这之前有一个resolveName方法,对我们传递的路径做了前置处理。再看一下该方法的源码:
//name为路径信息
private String resolveName(String name) {
if (name == null) {
return name;
}
//如果name不以/开头的情况
if (!name.startsWith("/")) {
Class<?> c = this;
while (c.isArray()) { //不是Class<?>[]不进循环
c = c.getComponentType();
}
String baseName = c.getName(); //得到baseName == com.mec.mybatis.test.Test
int index = baseName.lastIndexOf('.');
if (index != -1) { //条件成立
name = baseName.substring(0, index).replace('.', '/') //进入这里
+"/"+name; //得到com/mec/mybatis/test/Test
}
} else {
//name如果以/开头 --------->他就会把/ 给你去掉
name = name.substring(1);
}
return name;
}
上面就可以看出来,两种情况处理:
- 路径信息不以 / 开头 他会获取该类的class文件所在的路径,然后返回给类加载器,然后类加载器在该资源路径下加载资源
- 以 / 开头。进入else他会直接把 / 去掉。交给类加载器。注意:类加载器加载资源是从的根路径开始加载。加/会报错的。
我们可以在测试一下,类加载器认为的根路径是什么:
public static void main(String[] args) {
try {
//不传路径信息看它从什么地方加载资源
Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources("");
while(urls.hasMoreElements()) {
System.out.println(urls.nextElement());
}
} catch (IOException e) {
e.printStackTrace();
}
}
输出结果如下:
所以将我们最开始的路径信息连起来,路径应该是.....bin/User.xml
所以如果带上/ 那就变成了 .....bin//User.xml ----- 报错bin目录下没有“/User.xml”这个配置文件
通过输出可以看到类加载器自己认为的工程根是bin/ 这个和classpath有关,其实它会去classpath中寻找自己应该从哪里开始加载资源,也即classpath告诉他根在哪里。
打开classpath显示如下:
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="lib" path="src/lib/mybatis-3.3.0.jar" sourcepath="F:/文件夹/mybatis-3-mybatis-3.3.0/mybatis-3-mybatis-3.3.0.zip"/>
<classpathentry kind="lib" path="src/lib/asm-4.2.jar"/>
<classpathentry kind="lib" path="src/lib/cglib-3.1.jar"/>
<classpathentry kind="lib" path="src/lib/commons-logging-1.2.jar"/>
<classpathentry kind="lib" path="src/lib/javassist-3.17.1-GA.jar"/>
<classpathentry kind="lib" path="src/lib/log4j-1.2.17.jar"/>
<classpathentry kind="lib" path="src/lib/log4j-api-2.2.jar"/>
<classpathentry kind="lib" path="src/lib/log4j-core-2.2.jar" sourcepath="F:/文件夹/mybatis-3-mybatis-3.3.0/mybatis-3-mybatis-3.3.0.zip"/>
<classpathentry kind="lib" path="src/lib/slf4j-api-1.7.12.jar"/>
<classpathentry kind="lib" path="src/lib/slf4j-log4j12-1.7.12.jar"/>
<classpathentry kind="lib" path="src/lib/mysql-connector-java-5.1.46-bin.jar"/>
<classpathentry kind="lib" path="src/lib/MecUtils.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
其中都是指定的资源路径,不过最后一个 output 的 path=“bin”。这个就是指定将上面所有标签中指定的路径中的资源全部输出到bin下,所以我们在src目录下的内容才会在bin目录下产生一个副本。这里也就相当于告诉类加载器,他应该去bin目录下加载资源。
我们可以将classpath的输出路径改一下,如下:
<classpathentry kind="output" path="yhj"/>
在创建一个空的目录(yhj)如下:
运行原来的程序看结果:
它直接连主类都加载不到了。因为他是去yhj目录下去找我们的资源,但是现在该目录为空,所以他没找到就加载不到。
综上所述:
使用字节码对象获取资源:
- 不带 / 会从本字节码文件的路径下加载资源
- 带 / 他会从classpath中指定的根中去加载资源
使用类加载器: 直接从classpath中指定的根去加载资源