在 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<Dept> 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局限很大,因为一般不使用绝对路径,下面代码中第二种方式打包执行 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()); }
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 下面的配置文件都加载到类路径下。
<build> <finalName>strategy-service</finalName> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>
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 之后就可以看到可以加载文件了。
我们主要看下 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等,代码大同小异,无需多讲。