Mybatis 源码学习(12)-资源加载

历史文章:
Mybatis 源码学习(11)-日志模块


Mybatis 中的资源加载依赖于 JVM 的类加载机制,在 JVM 中的类加载机制使用的双亲委派模式。默认情况下,JVM 中存在三种加载器:Bootstrap ClassLoader、Extension ClassLoader、System ClassLoader,我们也可以拓展实现用户自定义的类加载器 UserDefine ClassLoader。

ClassLoaderWrapper

Mybatis 中的资源加载功能均在 org.apache.ibatis.io 中实现,ClassLoaderWrapper 是 ClassLoader 的包装类,其内部有 ClassLoader 变量。ClassLoaderWrapper 内部会维护一个 ClassLoader 的优先级,按照特定的次序返回内部包装的 ClassLoader。

ClassLoaderWrapper 内部定义了默认 ClassLoader 和 systemClassLoader:

ClassLoader defaultClassLoader; // 由应用指定的默认类加载器
ClassLoader systemClassLoader; // SystemClassLoader

ClassLoaderWrapper() {
  try {
    // 初始化 SystemClassLoader
    systemClassLoader = ClassLoader.getSystemClassLoader();
  } catch (SecurityException ignored) {
    // AccessControlException on Google App Engine   
  }
}

ClassLoaderWrapper 的核心功能分为三类,分别是:classForName、getResourceAsStream、getResourceAsURL,这三种方法分别有多个重载,但是最终都调用了对应的 String, ClassLoader[] 参数的方法,它们的逻辑类似,因此仅以常见的 getResourceAsStream 为例。
在这里插入图片描述

// 不指定 ClassLoader 加载资源
public InputStream getResourceAsStream(String resource) {
  return getResourceAsStream(resource, getClassLoaders(null));
}

// 指定 ClassLoader 加载资源
public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
  return getResourceAsStream(resource, getClassLoaders(classLoader));
}

// ClassLoaderWrapper.getClassLoaders 方法返回 ClassLoader 数组
// 该方法指定了类加载器的优先级(指定的 ClassLoader 优先级最高)
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
  return new ClassLoader[]{
      classLoader, // 参数指定的类加载器
      defaultClassLoader, // 系统默认的类加载器
      Thread.currentThread().getContextClassLoader(), // 当前线程的类加载器(应用上下文类加载器)
      getClass().getClassLoader(), // 加载当前类使用的类加载器
      systemClassLoader}; // SystemClassLoader
}

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

Resources

Resources 提供了多个加载和使用资源的工具类,其中的方法均通过内部的 classLoaderWrapper 实现,这里仅以常用的 getResourceAsStream 为例。

// 初始化内部 classLoaderWrapper
private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();

public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
  // 通过 classLoaderWrapper 读取资源(加载过程已经在之前解析过)
  InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
  if (in == null) { // 未找到资源则抛异常
    throw new IOException("Could not find resource " + resource);
  }
  return in;
}

ResolverUtil

ResolverUtil 提供了一种使用命令的方式过滤并加载指定包内的类,它的内部 Test 接口提供了过滤功能。Test 接口有两个实现:IsA 和 AnnotatedWith 提供了两个基本的实现,分别提供按照类型过滤和按照注解过滤的功能。另外,和 ClassLoaderWrapper 一样,它也允许外部直接传入自定义的 ClassLoader,但是它默认使用应用上下文的 ClassLoader:Thread.currentThread().getContextClassLoader()。

在这里插入图片描述
Test 接口提供了 matches 方法用于检查是否符合特定的条件:

public interface Test {
  // 参数 type 是待检测的类型,在检测时,对于所有待测试的类,都需要调用该方法
  // 如果检测符合条件需要返回 true
  boolean matches(Class<?> type);
}

IsA 和 AnnotatedWith 的实现如下:

// IsA 是用来判断一个类是否是指定类的子类
public static class IsA implements Test {
  private Class<?> parent;

  // 创建对象时指定原始的类型
  public IsA(Class<?> parentType) {
    this.parent = parentType;
  }

  // 通过反射判断子类是否可以向父类赋值
  public boolean matches(Class<?> type) {
    return type != null && parent.isAssignableFrom(type);
  }

  // toString 方法
}

// AnnotatedWith 是用来判断一个类是否使用了指定注解
public static class AnnotatedWith implements Test {
  private Class<? extends Annotation> annotation;

  // 通过构造器指定需要判定的注解类型
  public AnnotatedWith(Class<? extends Annotation> annotation) {
    this.annotation = annotation;
  }

  // 通过反射判断待检测的类是否配置了指定注解
  public boolean matches(Class<?> type) {
    return type != null && type.isAnnotationPresent(annotation);
  }

  // toString 方法
}

使用 ResolverUtil 时仅需指定特定的包,以及需要判定的类,即可获取符合条件的子类集合。

ResolverUtil<org.apache.ibatis.logging.Log> resolverUtil = new ResolverUtil<>();
// 在指定的包内查找 Log 接口的子类
resolverUtil.find(new ResolverUtil.IsA(org.apache.ibatis.logging.Log.class), "org.apache.ibatis.logging");
// 获取查找的结果
log.info("{}", resolverUtil.getClasses());

除了 find 方法外,ResolverUtil 还提供了 findImplementations 和 findAnnotated,分别用于扫描包内指定类的子类和包内指定注解注释的类,它们都依赖于 find 方法的实现,这里仅以 findImplementations 为例。

// 查找多个包内实现指定类的子类
public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) {
  if (packageNames == null) { // 未指定包名,则直接返回
    return this;
  }
  // 创建 Test 的子类 IsA
  Test test = new IsA(parent);
  // 遍历使用 find 方法检测
  for (String pkg : packageNames) {
    find(test, pkg);
  }
  return this;
}

// 扫描单个包内,符合条件的实现
public ResolverUtil<T> find(Test test, String packageName) {
  String path = getPackagePath(packageName); // 获取包名,及其对应路径

  try {
    // 通过 VFS.getInstance().list 查找路径下的所有资源
    List<String> children = VFS.getInstance().list(path);
    for (String child : children) {
      if (child.endsWith(".class")) {
        addIfMatching(test, child); // 检测类是否符合条件
      }
    }
  } catch (IOException ioe) {
    log.error("Could not read package: " + packageName, ioe);
  }
  return this;
}

// fqn 表示类的全限定名称
protected void addIfMatching(Test test, String fqn) {
  try {
    // 去除 “.class” 后缀,返回全限定名称
    String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
    ClassLoader loader = getClassLoader();
    // 加载指定的类
    Class<?> type = loader.loadClass(externalName); 
    if (test.matches(type)) { // 判断是否满足条件
      matches.add((Class<T>) type); // 符合条件,则记录到 matchs 集合中
    }
  } catch (Throwable t) {
    log.warn(“…”);
  }
}

VFS

VFS 的全称是 Visual File System,即虚拟文件系统,它主要负责加载指定路径下的资源。VFS 是一个抽象类,Mybatis 默认提供了 DefaultVFS 和 JBoss6VFS,当然用户也可以在初始化时指定自定义的 VFS。
在这里插入图片描述
VFS 的核心字段有两个,同时提供了内部类 VFSHolder,用于实现 Lazy 的单例模式(延迟加载占位符单例模式)。

// 记录内建的两个 VFS 的实现类
public static final Class<?>[] IMPLEMENTATIONS = { JBoss6VFS.class, DefaultVFS.class };

// 记录用户自定义的 VFS 实现,VFS.addImplClass 会把对应的实现类添加到 USER_IMPLEMENTATIONS 中
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<Class<? extends VFS>>();

// 使用 VFSHolder 实现单例,仅 VFSHolder.class 被加载时,自动调用 createVFS,实现一次单例的加载
private static class VFSHolder {
  static final VFS INSTANCE = createVFS();

  static VFS createVFS() {
    // 优先使用用户自定义的 VFS 实现,如果没有自定义实现,则使用 Mybatis 的默认实现
    List<Class<? extends VFS>> impls = new ArrayList<Class<? extends VFS>>();
    impls.addAll(USER_IMPLEMENTATIONS);
    impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));

    // 依次检测 impls 中所有的 VFS 实现,直到找到第一个有效的 VFS 实现,并结束循环
    VFS vfs = null;
    for (int i = 0; vfs == null || !vfs.isValid(); i++) {
      Class<? extends VFS> impl = impls.get(i);
      try {
        vfs = impl.newInstance();
        if (vfs == null || !vfs.isValid()) { // vfs.isValid 由子类实现
          // … 日志
        }
      } catch (InstantiationException e) {
        log.error("Failed to instantiate " + impl, e);
        return null;
      } catch (IllegalAccessException e) {
        log.error("Failed to instantiate " + impl, e);
        return null;
      }
    }
    // …日志
    return vfs;
  }
}

VFS 抽象类中提供了两个抽象方法:isValid 和 list,isValid 负责检测在当前环境下 VFS 实现是否有效,list 方法负责查找指定资源名称列表。VFS 默认提供两个 list 方法:list(URL url, String forPath)list(String path),而 list(String path) 可以找找指定 path 下的所有资源,对单个资源它实际上是调用了 list(URL url, String forPath),接下来以 DefaultVFS.list(URL url, String path) 为例解释其中的逻辑。

public List<String> list(URL url, String path) throws IOException {
  InputStream is = null;
  try {
    List<String> resources = new ArrayList<String>();

    // 如果 url 指向了一个 jar包,则获取 jar 包对应的 url,否则返回 null
    URL jarUrl = findJarForResource(url);
    if (jarUrl != null) {
      is = jarUrl.openStream();
      // 遍历 jar 包中的资源,返回以指定 path 开头的资源
      resources = listResources(new JarInputStream(is), path);
    } else {
      List<String> children = new ArrayList<String>();
      try {
        if (isJar(url)) { // 读取文件头,判断是否是 jar 包
          // 兼容部分 JBoss 虚拟文件系统对非 jar 文件返回 jar 文件格式
          is = url.openStream();
          JarInputStream jarInput = new JarInputStream(is);
          for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
            // 加载所有的资源
            children.add(entry.getName());
          }
          jarInput.close();
        } else {
          // 某些 servlet 容器允许从文本文件中指定目录,每行目录代表一系列资源
          // 为了兼容这种情况,仅先读取第一行,如果第一行表示的是资源路径,
          // 则表示该文件表示的都是资源路径
          is = url.openStream();
          BufferedReader reader = new BufferedReader(new InputStreamReader(is));
          List<String> lines = new ArrayList<String>();
          for (String line; (line = reader.readLine()) != null;) {
            lines.add(line);
            if (getResources(path + "/" + line).isEmpty()) {
              lines.clear();
              break;
            }
          }

          if (!lines.isEmpty()) {
            children.addAll(lines);
          }
        }
      } catch (FileNotFoundException e) {
        // 对于文件夹类型的 url,无法直接在 servlet 容器中读取,需要转化成文件列表读取
        if ("file".equals(url.getProtocol())) {
          File file = new File(url.getFile());
            children = Arrays.asList(file.list());
          }
        }
        else {
          throw e;
        }
      }
      // 记录下来 url 的前缀,用于递归查找资源
      String prefix = url.toExternalForm();
      if (!prefix.endsWith("/")) {
        prefix = prefix + "/";
      }
      // 遍历 children 集合,递归查找符合条件的资源
      for (String child : children) {
        String resourcePath = path + "/" + child;
        resources.add(resourcePath);
        URL childUrl = new URL(prefix + child);
        resources.addAll(list(childUrl, resourcePath));
      }
    }

    return resources;
  } finally {
    if (is != null) {
      try {
        is.close();
      } catch (Exception e) {
      }
    }
  }
}

// 列出 jar 包内的资源列表
protected List<String> listResources(JarInputStream jar, String path) throws IOException {
  // 如果 path 不是以 / 开头和结束,则拼接上 /
  if (!path.startsWith("/")) {
    path = "/" + path;
  }
  if (!path.endsWith("/")) {
    path = path + "/";
  }

  // Iterate over the entries and collect those that begin with the requested path
  // 遍历整个 jar 包内的资源,并把以 path 开头的资源记录到 resources 中
  List<String> resources = new ArrayList<String>();
  for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
    if (!entry.isDirectory()) {
       // 如果 name 不是以 / 开头和结束,则拼接上 /
      String name = entry.getName();
      if (!name.startsWith("/")) {
        name = "/" + name;
      }

      // 检测 name 是否以 path 开头
      if (name.startsWith(path)) {
        // 记录资源名称
        resources.add(name.substring(1));
      }
    }
  }
  return resources;
}

总结

本次内容较多,但是都是 org.apache.ibatis.io 包内的内容,该包提供了资源加载的功能,Mybatis 的资源加载依赖于 ClassLoader。

ClassLoaderWrapper 提供了 ClassLoader 的封装,便于在运行时自动选择 ClassLoader。

Resources 封装了 ClassLoaderWrapper,通过内部字段实现对外的资源加载逻辑。ResolverUtil 提供了根据条件筛选指定 package 内的资源。

VFS 提供了虚拟文件系统,其实就是按照包路径加载资源的抽象,它不仅可以解析 class 文件,还可以解析jar 包内的资源。比较特殊的一点是,VFS 内部使用了延迟资源占位符的方式实现了单例模式。


参考文档:《Mybatis 技术内幕》

本文的基本脉络参考自《Mybatis 技术内幕》,编写文章的原因是希望能够系统地学习 Mybatis 的源码,但是如果仅阅读源码或者仅从官方文档很难去系统地学习,因此希望参考现成的文档,按照文章的脉络逐步学习。


欢迎关注我的公众号:我的搬砖日记,我会定时分享自己的学习历程。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值