原创

MyBatis源码解析 - 资源加载模块

MyBatis源码解析 - 资源加载模块

简介

Java虚拟机中的类加载器(ClassLoader) 负责加载来自文件系统、网络或其他来源的类文件。Java 虚拟机中的类加载器默认使用的是双亲委派模式,如图所示,其中有三种默认使用的类加载器,分别是Bootstrap ClassL oader. Extension ClassLoaderSystem ClassL oader (也被称为Application ClassLoader),每种类加载器都已经确定从哪个位置加载类文件。加载器详细的介绍可以看下之前我写的一篇文章。Java - ClassLoader双亲委派机制

Mybatis源码中对应org.apache.ibatis.io包。

ClassLoaderWrapper

上面上面我们已经了解了类加载器常用的使用方式。在MyBatis IO包中封装了ClassLoader以及读取资源文件相关的API

IO包中提供的ClassLoaderWrapper是一个ClassLoader的包装器,其中包含了多个ClassLoader对象。通过调整多个类加载器的使用顺序,ClassLoaderWrapper可以确保返回给系统使用的是正确的类加载器。使用ClassLoaderWrapper就如同使用一个ClassLoader对象,ClassLoaderWrapper会按照指定的顺序依次检测其中封装的ClassLoader对象,并从中选取第一个可用的ClassLoader完成相关功能。

ClassLoaderWrapper中定义了两个字段,分别记录了系统指定的默认ClassLoaderSystem ClassLoader,定义如下:

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

//System ClassLoader
ClassLoader systemClassLoader;

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

ClassLoaderWrapper的主要功能可以分为三类,分别是getResourceAsURL()方 法、classForName()方法、getResourceAsStream()方法, 这三个方法都有多个重载,这三类方法最终都会调用参数为StringClassLoader[]的重载。这里以getResourceAsURL()方法为例进行介绍,其他两类方法的实现与该实现类似,ClassLoaderWrapper.getResourceAsURL()方法的实现如下:

public URL getResourceAsURL(String resource, ClassLoader classLoader) {
  return getResourceAsURL(resource, getClassLoaders(classLoader));
}
ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,  //参数指定类加载器
        defaultClassLoader, //系统指定的默认的类加载器
        Thread.currentThread().getContextClassLoader(), //当前线程绑定的类加载器
        getClass().getClassLoader(),//加载当前类使用的类加载器
        systemClassLoader}; //System ClassLoader
}
URL getResourceAsURL(String resource, ClassLoader[] classLoader) {

    URL url;
    //遍历ClassLoader数组
    for (ClassLoader cl : classLoader) {

      if (null != cl) {
        //调用Class.getResource()方法找到指定的资源
        // look for the resource as passed in...
        url = cl.getResource(resource);
        if (null == url) {
          //尝试以"/"开头继续查找
          url = cl.getResource("/" + resource);
        }
        if (null != url) {  //查找到指定的资源
          return url;
        }
      }
    }
    // didn't find it anywhere.
    return null;
}

Resources是一一个提供了多个静态方法的工具类,其中封装了一个ClassLoaderWrapper类型的静态字段, Resources 提供的这些静态工具都是通过调用该ClassLoaderWrapper对象的相应方法实现的。代码比较简单,就不贴出来了。

ResolverUtil

ResolverUtil可以根据指定的条件查找指定包下的类,其中使用的条件由Test接口表示。ResolverUtil中使用classLoader字段(ClassLoader 类型)记录了当前使用的类加载器,默认情况下,使用的是当前线程上下文绑定的ClassLoader,我们可以通过setClassLoader()方法修改使用类加载器。
MyBatis提供了两个常用的Test接口实现,分别是IsAAnnotatedWith,如图所示。IsA用于检测类是否继承了指定的类或接口, AnnotatedWith用于检测类是否添加了指定的注解。开发人员也可以自己实现Test接口,实现指定条件的检测。

Test接口中定义了matches()方法,它用于检测指定类是否符合条件,代码如下:

public interface Test {
  //参数Type是待检测的类,如果该类符合条件,则matches()返回true,否则返回fals
  boolean matches(Class<?> type);
}

IsAAnnotationWith的具体实现如下:

public static class IsA implements Test { //用于检测指定类是否继承了parent指定的类
  private Class<?> parent;
  public IsA(Class<?> parentType) {
    //初始化parent字段
    this.parent = parentType;
  }
  @Override
  public boolean matches(Class<?> type) {
    return type != null && parent.isAssignableFrom(type);
  }
}
public static class AnnotatedWith implements Test { //检测指定类是否添加了Annotation注解
  private Class<? extends Annotation> annotation;
  public AnnotatedWith(Class<? extends Annotation> annotation) {
    //初始化annotation
    this.annotation = annotation;
  }
  @Override
  public boolean matches(Class<?> type) {
    return type != null && type.isAnnotationPresent(annotation);
  }
}

默认情况下,使用Thread.currentThread().getContextClassLoader()这个类加载器加载符合条件的类,代码如下:

public ClassLoader getClassLoader() {
  return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader;
}

我们可以在调用find()方法之前,调用setClassLoader(ClassLoader)设置需要使用的ClassLoader,这个ClassLoader可以从ClassLoaderWrapper中获取合适的类加载器。

ResolverUtil的使用方法如下:

ResolverUtil<ActionBean> resolver = new ResolverUtil<ActionBean>();
//在pkg1 和 pkg2 这两个报下寻找ActionBean这个类
resolver.findImplementation(ActionBean.class, pkg1, pkg2);
//在pkg1下寻找符合CustomTest条件的类
resolver.find(new CustomTest(), pkg1);
//在pkg2下寻找符合CustomTest条件的类
resolver.find(new CustomTest(), pkg2);
//获取上面三次查找的结果哦
Collection<ActionBean> beans = resolver.getClasses();

ResolverUtil.findImplementations()方法和ResolverUtil.findAnnotated()方法都是依赖ResolverUtil.find()方法实现的,findImplementations()方法会创建 IsA 对象作为检测条件,findAnnotated()方法会创建AnnotatedWith对象作为检测条件。这几个方法的调用关系如图所示。

ResolverUtil.find()方法实现如下:

public ResolverUtil<T> find(Test test, String packageName) {
  //根据包名获取其对应的路径
  String path = getPackagePath(packageName);

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

  return this;
}
protected void addIfMatching(Test test, String fqn) {
    try {
      //fqn 是类的完全限定名,包括类所在包的包名
      String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
      ClassLoader loader = getClassLoader();
      if (log.isDebugEnabled()) {
        //日志输出。。。
        log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
      }
      //加载指定类
      Class<?> type = loader.loadClass(externalName);
      //校验类是否符合test条件
      if (test.matches(type)) {
        //符合条件的类加入matches集合
        matches.add((Class<T>) type);
      }
    } catch (Throwable t) {
      log.warn("Could not examine class '" + fqn + "'" + " due to a " +
          t.getClass().getName() + " with message: " + t.getMessage());
    }
}

VFS

VFS表示虚拟文件系统(Virtual File System),它用来查找指定路径下的资源。VFS是一个抽象类,MyBatis中提供了JBoss6VFDefaultVFS两个VFS的实现,如图所示。用户也可以提供自定义的VFS实现类,后面介绍MyBatis初始化的流程时,还会提到这两个扩展点。

VFS的核心字段的含义如下:

//记录了Mybatis记录的两个实现类
public static final Class<?>[] IMPLEMENTATIONS = { JBoss6VFS.class, DefaultVFS.class };
//记录了用户自定义VFS的实现类,VFS.addImplClass()方法会将指定的VFS实现对应的Class对象添加到
// USER_IMPLEMENTATIONS 集合中
public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<>();

//单例模式,记录全局唯一的VFS对象
  private static class VFSHolder {
    static final VFS INSTANCE = createVFS();

    @SuppressWarnings("unchecked")
    static VFS createVFS() {
      // Try the user implementations first, then the built-ins
      // 有限使用用户自定义VFS,如果没有用户自定义的VFS实现类,则使用MyBatis提供的VFS实现
      List<Class<? extends VFS>> impls = new ArrayList<>();
      impls.addAll(USER_IMPLEMENTATIONS);
      impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));

      // Try each implementation class until a valid one is found
      //遍历impls集合,依次实例化VFS对象并检测VFS对象是否有效,一旦得到有效的VFS对象,则结束循环
      VFS vfs = null;
      for (int i = 0; vfs == null || !vfs.isValid(); i++) { //vfs.isValid()是一个抽象方法
        Class<? extends VFS> impl = impls.get(i);
        try {
          vfs = impl.getDeclaredConstructor().newInstance();
          if (!vfs.isValid()) {
            if (log.isDebugEnabled()) {
              //日志输出
              log.debug("VFS implementation " + impl.getName() +
                  " is not valid in this environment.");
            }
          }
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
          //异常日志记录
          log.error("Failed to instantiate " + impl, e);
          return null;
        }
      }

      if (log.isDebugEnabled()) {
        log.debug("Using VFS adapter " + vfs.getClass().getName());
      }

      return vfs;
    }
  }
}

VFS中定义了list(URL, String)isVali()两个抽象方法,isValid()负责检测当前VFS对象在当前环境下是否有效,list(URL,String)方法负责查找指定的资源名称列表,在ResolverUtil.find()方法查找类文件时会调用list()方法的重载方法,该重载最终会调用list(URL,String)这个重载。我们以DefaultVFS为例进行分析,感兴趣的读者可以参考JBoss6VFS的源码进行学习。DefaultVFS.list(URL,String)方法的实现如下:

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

    // First, try to find the URL of a JAR file containing the requested resource. If a JAR
    // file is found, then we'll list child resources by reading the JAR.
    //如果url指向的资源在一个jar包,则获取该jar包对应的url,否则返回null
    URL jarUrl = findJarForResource(url);
    if (jarUrl != null) {
      is = jarUrl.openStream();
      if (log.isDebugEnabled()) {
        log.debug("Listing " + url);
      }
      //遍历jar包资源,并返回以path开头的资源列表
      resources = listResources(new JarInputStream(is), path);
    }
    else {
      List<String> children = new ArrayList<>();
      // ... 遍历jar包指向的目录,将其下资源名称记录到children集合中... (略)

      // Iterate over immediate children, adding files and recursing into directories
      //遍历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输入流
        is.close();
      } catch (Exception e) {
        // Ignore
      }
    }
  }
}
protected List<String> listResources(JarInputStream jar, String path) throws IOException {
  // Include the leading and trailing slash when matching names
  //...如果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<>();
  for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
    if (!entry.isDirectory()) {
      // Add leading slash if it's missing
      StringBuilder name = new StringBuilder(entry.getName());
      //...如果name不是以"/"开头,则为其添加"/" (略)
      if (name.charAt(0) != '/') {
        name.insert(0, '/');
      }

      // Check file name
      if (name.indexOf(path) == 0) {  //检测name是否是以path开头
        if (log.isDebugEnabled()) {
          log.debug("Found resource: " + name);
        }
        // Trim leading slash
        resources.add(name.substring(1)); //记录资源名称
      }
    }
  }
  return resources;
}
正文到此结束
该篇文章的评论功能已被站长关闭