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

Posted 凉茶方便面

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 的源码,但是如果仅阅读源码或者仅从官方文档很难去系统地学习,因此希望参考现成的文档,按照文章的脉络逐步学习。


欢迎关注我的公众号:我的搬砖日记,我会定时分享自己的学习历程。

以上是关于Mybatis 源码学习(12)-资源加载的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis 源码学习(13)-DataSource

Mybatis 源码学习(13)-DataSource

MyBatis 源码篇-资源加载

MyBatis源码学习--配置文件的加载

Mybatis学习笔记-增删改的操作 -对SqlSession的优化封装-优化代码

Spring源码解析-核心类之XmlBeanDefinitionReader