在 mybatis源码分析-环境搭建 一文中,我们的测试代码如下:
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class);
List deptList = deptMapper.getAllDept();
System.out.println(deptList);
} finally {
sqlSession.close();
}
}
mybatis首先需要加载配置文件,包括全局配置文件和映射文件,代码中我们使用了 Resources 类的 getResourceAsStream 方法来获取 InputStream 对象,以此来进行后面的操作,因此我们先来研究 Resources 类。
本节的重点就是下面的代码:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
加载配置文件
在研究Resources之前,我们考虑一下如何获取个输入流,也就是读取一个文件?
- 使用 FileInputStream
- 使用 ClassLoader 的 getResourceAsStream 方法
- 使用 类.class 的 getResourceAsStream 方法
使用 FileInputStream
局限很大,因为一般不使用绝对路径,下面代码中第二种方式打包执行 jar 包会出现找不到文件的错误。
public static void main(String[] args) throws IOException {
//要么是全路径
File file1 = new File("D:\\my-code\\mybatis\\src\\main\\resources\\mybatis-config.xml");
InputStream input1 = new FileInputStream(file1);
System.out.println(input1.available());
//要么是去在全路径基础上去掉项目名
File file2 = new File("src\\main\\resources\\mybatis-config.xml");
InputStream input2 = new FileInputStream(file2);
System.out.println(input2.available());
}
使用 ClassLoader的 getResourceAsStream 方法
public static void main(String[] args) throws IOException {
InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream("mappers/DeptMapper.xml");
System.out.println(inputStream.available());
}
此方法可以获取 resources 下面的配置文件,但是如果配置文件存在与和resources同级别的java目录下面,是否可以获取到呢?我们将配置文件移到java代码的目录下面,使用下面代码获取文件:
public static void main(String[] args) throws IOException {
InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream("com/yefengyu/mybatis/mappers/DeptMapper.xml");
System.out.println(inputStream.available());
}
结果是空指针异常,找不到相关文件。
Exception in thread "main" java.lang.NullPointerException
at com.yefengyu.mybatis.Main.main(Main.java:22)
这里特别注意:使用 ClassLoader 获取文件,都是从类路径下面获取,也就是maven install 之后生成的 target 目录下面的classes目录,而不是源码路径。使用idea开发工具,在代码目录下面的配置文件,是不会install 到classes下面的。那如果有些人喜欢把 mapper 配置文件写入到 java 目录下面,而不是 resources 目录下面,那么如何解决呢?
pom.xml文件新加下面的配置即可:也就是将 java 和 resources 下面的配置文件都加载到类路径下。
strategy-service
src/main/java
**/*.xml
src/main/resources
**/*.xml
使用 类.class 的 getResourceAsStream 方法
public static void main(String[] args) throws IOException {
InputStream resource=Main.class.getResourceAsStream("mybatis-config.xml");
System.out.println(resource.available());
}
注意此时 mybatis-config.xml 是在 resources 的根目录,为啥获取不到呢?
Exception in thread "main" java.lang.NullPointerException
at com.yefengyu.mybatis.Main.main(Main.java:22)
查看下源码:
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);
}
发现此种方式也是使用 classLoader 获取文件的,那么为什么没有加载到文件呢?主要是这句:
name = resolveName(name);
这里重新解析了文件名称:
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class> c = this;
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/')
+"/"+name;
}
} else {
name = name.substring(1);
}
return name;
}
这里主要是先获取调用类的相对路径,再加上我们所传入的文件路径,那么也就是从 java 目录下面查找了,自然找不到。
如果我们把 mybatis-config.xml 配置文件放入到 main 函数所在类的同级目录,重新 install 之后就可以看到可以加载文件了。
mybatis如何实现?
我们主要看下 Resources 类,关于获取InputStream有两个方法:
public static InputStream getResourceAsStream(String resource) throws IOException {
return getResourceAsStream((ClassLoader)null, resource);
}
public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
if (in == null) {
throw new IOException("Could not find resource " + resource);
} else {
return in;
}
}
第一个方法调用第二个方法,只传递一个参数,就是文件名称。然后调用第二个重载的方法,传入空的ClassLoader 对象。第二个方法也使用 classLoaderWrapper 来获取 InputStream 对象。
classLoaderWrapper 是对 ClassLoader 的一个封装。主要是尝试多种classLoader对文件进行加载。相关代码:
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
return getResourceAsStream(resource, getClassLoaders(classLoader));
}
这段代码就是 Resources 类中,classLoaderWrapper 所调用的方法。它也调用的是它的重载方法,在看重载方法之前,我们看下 getClassLoaders 方法,此方法是尽可能多的获取 ClassLoader.
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
return new ClassLoader[]{
classLoader,
defaultClassLoader,
Thread.currentThread().getContextClassLoader(),
getClass().getClassLoader(),
systemClassLoader};
}
这里返回了5种 ClassLoader,下面看下getResourceAsStream方法:
InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
for (ClassLoader cl : classLoader) {
if (null != cl) {
InputStream returnValue = cl.getResourceAsStream(resource);
if (null == returnValue) {
returnValue = cl.getResourceAsStream("/" + resource);
}
if (null != returnValue) {
return returnValue;
}
}
}
return null;
}
这里还是使用 ClassLoader获取 InputStream 对象,一个ClassLoader没有获取,下一个ClassLoader继续,直到所有的都尝试加载文件,没有什么太难的。
对于 Resources 类,除了获取 InputStream,还有 URL、Properties、Reader、File等,代码大同小异,无需多讲。
收获
- 了解加载配置文件的细节
- 重载方法的优雅编写方式:入参少的方法调用入参多的方法,没有的参数使用默认值,最后那个参数最多的方法统一处理。