1

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
  • 使用 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 下面的配置文件都加载到类路径下。

<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>

使用 类.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等,代码大同小异,无需多讲。

收获

  • 了解加载配置文件的细节
  • 重载方法的优雅编写方式:入参少的方法调用入参多的方法,没有的参数使用默认值,最后那个参数最多的方法统一处理。

指尖改变世界
27 声望6 粉丝