MyBatis源码剖析

[MyBatis源码分析 - 资源加载模块]

2020-11-08  本文已影响0人  小胡_鸭

一、类加载

  Java 虚拟机使用类加载器来加载来自文件系统、网络或其他来源的类文件。默认有三种类加载器,分别为 Bootstrap ClassLoaderExtension ClassLoaderSystem ClassLoader(也称为 Application ClassLoader),它们的区别如下:

  测试案例:

import com.sun.java.accessibility.AccessBridge;

public class ClassLoaderTest {
    public static void main(String[] args) {
        // java.lang 包中的类属于核心类库,类加载器为 Bootstrap ClassLoader
        ClassLoader bootCl = Integer.class.getClassLoader();
        System.out.println(bootCl);

        // AccessBridge属于扩展类库中的类,类加载器为 Extension ClassLoader
        ClassLoader extCl = AccessBridge.class.getClassLoader();
        System.out.println(extCl);

        // 应用程序类的类加载器为 Application ClassLoader
        ClassLoader appCl = ClassLoaderTest.class.getClassLoader();
        System.out.println(appCl);
    }
}

  执行结果:

null            // null 就代表 Bootstrap ClassLoader
sun.misc.Launcher$ExtClassLoader@5305068a
sun.misc.Launcher$AppClassLoader@58644d46

  根据自身需要,开发人员也可以通过继承 java.lang.ClassLoader 的方式自定义类加载器,类加载器有个运作模式,叫 “双亲委派模式”,如下图所示:


  在双亲委派模式中,加载类文件时,子加载器会先委托父加载器,父加载器先检查是否已加载该类,如果是则加载过程结束;否则继续委托给上一层的父加载器,当顶层的加载器也没有检查到该类是否已加载,则尝试从对应路径中加载该类文件,如果加载失败,则由子加载器继续尝试加载,直到发起加载请求的子加载器为止。

  双亲委派模式可以保证:
(1)子加载器可以使用父加载器加载过的类,避免类的重复加载,而父加载器无法使用子加载器已加载的类。
(2)父加载器已加载过的类无法被子加载器再次加载,其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Integer 的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改,保证 JVM 的安全性和稳定性。

二、资源加载模块简介

  资源加载模块实现了对类加载器进行封装,确定类加载器的使用顺序,并提供了加载类文件和其他资源文件的功能,相关的类位于 org.apache.ibatis.io 包中,如下图所示:


  类图如下:


三、ClassLoaderWrapper

3.1 初始化

public class ClassLoaderWrapper {

  ClassLoader defaultClassLoader;     // 默认 ClassLoader
  ClassLoader systemClassLoader;      // 系统 ClassLoader,一般为AppClassLoader

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

3.2 getClassLoaders

【功能】获取类加载器的数组,按照使用优先级顺序返回。
【源码】

  ClassLoader[] getClassLoaders(ClassLoader classLoader) {
    return new ClassLoader[]{
        classLoader,                                      // 根据传入的参数指定
        defaultClassLoader,                               // 不设null,则为 BootstrapClassLoader
        Thread.currentThread().getContextClassLoader(),   // 当前线程上下文的类加载器
        getClass().getClassLoader(),                      // 加载当前类的加载器,应用代码一般为AppClassLoader
        systemClassLoader};                               // 系统类加载器 AppClassLoader
  }

【解析】
  加载器数组中的类加载器的使用优先级为:指定的类加载器 > 默认类加载器 > 当前线程上下文的类加载器 > 加载当前类的加载器 > 系统类加载器。由上面的案例可知,核心类库的加载器的值为 null,所以不能用 BootstrapClassLoader 来加载类;如果用 ExtClassLoader 加载非扩展类库中的类,会报错 Class not found,所以如果没有自定义类加载器,实际只有 AppClassLoader 可用,这里我认为更多地还是考虑扩展性,因为在一些服务器中如 Tomcat、JBoss,是会自定义类加载器来满足自身的类加载需求的。

3.3 getResourceAsURL

【功能】获取指定资源并以一个 URL 对象的形式返回。
【源码与注解】

  public URL getResourceAsURL(String resource) {
    return getResourceAsURL(resource, getClassLoaders(null));
  }

  public URL getResourceAsURL(String resource, ClassLoader classLoader) {
    return getResourceAsURL(resource, getClassLoaders(classLoader));
  }

  URL getResourceAsURL(String resource, ClassLoader[] classLoader) {
    URL url;
    for (ClassLoader cl : classLoader) {
      if (null != cl) {
        // 尝试找到传参进来的资源
        url = cl.getResource(resource);

        // 有一些类加载器需要资源路径以"/"开头,所以如果上面加载失败的话,这里再补上"/"重新尝试一下加载资源
        if (null == url) {
          url = cl.getResource("/" + resource);
        }

        // 假如已加载到资源了,直接返回URL对象,否则尝试用下一个类加载器加载
        if (null != url) {
          return url;
        }
      }
    }

    // 没有加载到资源返回null
    return null;
  }

3.4 getResourceAsStream

【功能】获取指定资源并返回一个 InputStream 对象。
【源码与注解】

  public InputStream getResourceAsStream(String resource) {
    return getResourceAsStream(resource, getClassLoaders(null));
  }

  public InputStream getResourceAsStream(String resource, ClassLoader classLoader) {
    return getResourceAsStream(resource, getClassLoaders(classLoader));
  }

  InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
    for (ClassLoader cl : classLoader) {
      if (null != cl) {

        // 尝试找到传参进来的资源
        // try to find the resource as passed
        InputStream returnValue = cl.getResourceAsStream(resource);

        // 有一些类加载器需要资源路径以"/"开头,所以如果上面加载失败的话,这里再补上"/"重新尝试一下加载资源
        if (null == returnValue) {
          returnValue = cl.getResourceAsStream("/" + resource);
        }

        // 假如已加载到资源了,直接返回URL对象,否则尝试用下一个类加载器加载
        if (null != returnValue) {
          return returnValue;
        }
      }
    }
    return null;
  }

3.5 classForName

【功能】根据类的权限定名加载 Class 对象。
【源码与注解】

  public Class<?> classForName(String name) throws ClassNotFoundException {
    return classForName(name, getClassLoaders(null));
  }

  public Class<?> classForName(String name, ClassLoader classLoader) throws ClassNotFoundException {
    return classForName(name, getClassLoaders(classLoader));
  }

  Class<?> classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException {
    for (ClassLoader cl : classLoader) {
      if (null != cl) {
        try {
          Class<?> c = Class.forName(name, true, cl);
          // 如果加载到Class马上返回,否则接着由下一个加载器尝试加载
          if (null != c) {
            return c;
          }
        } catch (ClassNotFoundException e) {
        }
      }
    }
    // 如果最终没加载到Class,抛出异常
    throw new ClassNotFoundException("Cannot find class: " + name);
  }

四、Resources

  Resources 是一个基于 ClassLoaderWrapper 的加载功能进行封装的类,并实现将资源转换为更多的形式返回的方法,以满足更多的使用场景和需求。

4.1 初始化

public class Resources {

  private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper();
  private static Charset charset;

  Resources() {
  }

  public static ClassLoader getDefaultClassLoader() {
    return classLoaderWrapper.defaultClassLoader;
  }

  public static void setDefaultClassLoader(ClassLoader defaultClassLoader) {
    classLoaderWrapper.defaultClassLoader = defaultClassLoader;
  }

  public static Charset getCharset() {
    return charset;
  }

  public static void setCharset(Charset charset) {
    Resources.charset = charset;
  }

   // other code
}

【解析】
  定义了两个成员变量,classLoaderWrapper 是功能方法实现依赖的底层对象,charset 是一些方法时需要指定的字符集。

4.2 getResourceURL

【功能】加载指定路径的资源,以 URL 形式返回。

  public static URL getResourceURL(String resource) throws IOException {
      // issue #625
      return getResourceURL(null, resource);
  }

  public static URL getResourceURL(ClassLoader loader, String resource) throws IOException {
    URL url = classLoaderWrapper.getResourceAsURL(resource, loader);
    // 若加载不带资源抛出IO异常
    if (url == null) {
      throw new IOException("Could not find resource " + resource);
    }
    return url;
  }

4.3 getResourceAsStream

【功能】加载指定路径的资源,以 InputStream 形式返回。

  public static InputStream getResourceAsStream(String resource) throws IOException {
    return getResourceAsStream(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);
    }
    return in;
  }

4.4 getResourceAsProperties

【功能】一般用来加载指定路径下的 properties 文件,返回 Properties 对象。

  public static Properties getResourceAsProperties(String resource) throws IOException {
    Properties props = new Properties();
    InputStream in = getResourceAsStream(resource);
    props.load(in);   // 先得到InputStream对象,再调用 Properties.load 加载属性文件
    in.close();
    return props;
  }

  public static Properties getResourceAsProperties(ClassLoader loader, String resource) throws IOException {
    Properties props = new Properties();
    InputStream in = getResourceAsStream(loader, resource);
    props.load(in);
    in.close();
    return props;
  }

4.5 getResourceAsReader

【功能】加载指定路径的资源,以 Reader 的形式返回。

  public static Reader getResourceAsReader(String resource) throws IOException {
    Reader reader;
    if (charset == null) {
      reader = new InputStreamReader(getResourceAsStream(resource));
    } else {
      reader = new InputStreamReader(getResourceAsStream(resource), charset);
    }
    return reader;
  }

  public static Reader getResourceAsReader(ClassLoader loader, String resource) throws IOException {
    Reader reader;
    if (charset == null) {
      reader = new InputStreamReader(getResourceAsStream(loader, resource));
    } else {
      reader = new InputStreamReader(getResourceAsStream(loader, resource), charset);
    }
    return reader;
  }

4.6 getResourceAsFile

【功能】加载指定路径的资源,以 File的形式返回。

  public static File getResourceAsFile(String resource) throws IOException {
    return new File(getResourceURL(resource).getFile());
  }

  public static File getResourceAsFile(ClassLoader loader, String resource) throws IOException {
    return new File(getResourceURL(loader, resource).getFile());
  }

4.7 getUrlAs*

【功能】从网络加载资源,并转化为指定的形式返回。

  // 以 InputStream 形式返回
  public static InputStream getUrlAsStream(String urlString) throws IOException {
    URL url = new URL(urlString);
    URLConnection conn = url.openConnection();
    return conn.getInputStream();
  }

  // 以 Reader 形式返回
  public static Reader getUrlAsReader(String urlString) throws IOException {
    Reader reader;
    if (charset == null) {
      reader = new InputStreamReader(getUrlAsStream(urlString));
    } else {
      reader = new InputStreamReader(getUrlAsStream(urlString), charset);
    }
    return reader;
  }
  // 以 Properties 形式返回
  public static Properties getUrlAsProperties(String urlString) throws IOException {
    Properties props = new Properties();
    InputStream in = getUrlAsStream(urlString);
    props.load(in);
    in.close();
    return props;
  }

4.8 classForName

【功能】根据类的全限定名加载得到 Class 对象。

  public static Class<?> classForName(String className) throws ClassNotFoundException {
    return classLoaderWrapper.classForName(className);
  }

五、ResolverUtil

  ResolverUtil 用来找出指定包中指定类型或者被指定注解标注的类。

5.1 Test

【功能】ResolverUtil 的内部类,用来判断被测试的类是否符合要求。

  public static interface Test {
    boolean matches(Class<?> type);
  }

5.2 IsA

【功能】判断一个类是否为指定的类型。

  public static class IsA implements Test {
    private Class<?> parent;            // 指定判断的类

    public IsA(Class<?> parentType) {
      this.parent = parentType;
    }

    @Override
    public boolean matches(Class<?> type) {
      return type != null && parent.isAssignableFrom(type);
    }

    @Override
    public String toString() {
      return "is assignable to " + parent.getSimpleName();
    }
  }

5.3 AnnotatedWith

【功能】判断一个类是否被指定注解标注

  public static class AnnotatedWith implements Test {
    private Class<? extends Annotation> annotation;      // 指定判断的注解

    public AnnotatedWith(Class<? extends Annotation> annotation) {
      this.annotation = annotation;
    }

    @Override
    public boolean matches(Class<?> type) {
      return type != null && type.isAnnotationPresent(annotation);
    }

    @Override
    public String toString() {
      return "annotated with @" + annotation.getSimpleName();
    }
  }

5.4 初始化

  private static final Log log = LogFactory.getLog(ResolverUtil.class);

  private Set<Class<? extends T>> matches = new HashSet<Class<? extends T>>();

  private ClassLoader classloader;

  public Set<Class<? extends T>> getClasses() {
    return matches;
  }

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

  public void setClassLoader(ClassLoader classloader) {
    this.classloader = classloader;
  }

  ResolverUtil 的成员 matches 用来保存符合匹配条件的类,classloader 是用于加载类的加载器。

5.5 find

【功能】找到指定包中符合指定类型的类。
【源码与注解】

  public ResolverUtil<T> find(Test test, String packageName) {
    // (1)将包名转化为包路径
    String path = getPackagePath(packageName);

    try {
      // (2)通过 VFS 接口获取包路径中的所有资源文件
      List<String> children = VFS.getInstance().list(path);
      for (String child : children) {
        // (3)只处理class文件
        if (child.endsWith(".class")) {
          // (4)测试类是否符合条件,是则添加到matches集合中
          addIfMatching(test, child);
        }
      }
    } catch (IOException ioe) {
      log.error("Could not read package: " + packageName, ioe);
    }

    return this;
  }

【解析】

  protected String getPackagePath(String packageName) {
    return packageName == null ? null : packageName.replace('.', '/');
  }
  protected void addIfMatching(Test test, String fqn) {
    try {
       // 将 org/apache/ibatis/io/Resources.class 转化为 org.apache.ibatis.io.Resources
      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);
      // 测试是否匹配,是则加入到 matches 中
      if (test.matches(type)) {
        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());
    }
  }

5.6 findImplementations

【功能】基于 find 方法实现,查找符合指定类型的多个包中匹配的类。
【源码】

   // packageNames 是一个可变参数列表,实际处理时当成 String[] 即可
  public ResolverUtil<T> findImplementations(Class<?> parent, String... packageNames) {
    if (packageNames == null) {
      return this;
    }

    Test test = new IsA(parent);
    for (String pkg : packageNames) {
      find(test, pkg);
    }

    return this;
  }

5.7 findAnnotated

【功能】基于 find 方法实现,查找符合指定注解标注的多个包中匹配的类。
【源码】

  public ResolverUtil<T> findAnnotated(Class<? extends Annotation> annotation, String... packageNames) {
    if (packageNames == null) {
      return this;
    }

    Test test = new AnnotatedWith(annotation);
    for (String pkg : packageNames) {
      find(test, pkg);
    }

    return this;
  }

5.8 测试案例

public class ResolverUtilTest {
    @Test
    public void test_find() {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
        resolverUtil.find(new ResolverUtil.IsA(ResolverUtil.Test.class), "org.apache.ibatis.io");
        for (Class<?> clazz : resolverUtil.getClasses()) {
            System.out.println(clazz);
        }
    }

    @Test
    public void test_findImplementations() {
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
        resolverUtil.findImplementations(ResolverUtil.Test.class, "org.apache.ibatis.io");
        for (Class<?> clazz : resolverUtil.getClasses()) {
            System.out.println(clazz);
        }
    }
}

执行结果:



六、VFS

  VFS 提供了用于访问服务器内资源的非常简单的API,这是一个抽象类,框架内置了 DefaultVFSJBoss6VFS 两个子类,默认使用 DefaultVFS

6.1 数据结构

  // 框架内建的实现类
  public static final Class<?>[] IMPLEMENTATIONS = { JBoss6VFS.class, DefaultVFS.class };

  // 通过调用 #addImplClass(Class) 方法添加的用户实现类
  public static final List<Class<? extends VFS>> USER_IMPLEMENTATIONS = new ArrayList<Class<? extends VFS>>();

  // 使用单例模式
  private static VFS instance;
  public static void addImplClass(Class<? extends VFS> clazz) {
    if (clazz != null) {
      USER_IMPLEMENTATIONS.add(clazz);
    }
  }

6.2 getInstance

【功能】获取 VFS 类实例。
【源码】

  public static VFS getInstance() {
    // 如果单例已经初始化了直接返回
    if (instance != null) {
      return instance;
    }

    // 添加实现类到 impls 列表中,并且用户实现类优先于内置实现类
    List<Class<? extends VFS>> impls = new ArrayList<Class<? extends VFS>>();
    impls.addAll(USER_IMPLEMENTATIONS);
    impls.addAll(Arrays.asList((Class<? extends VFS>[]) IMPLEMENTATIONS));

    // 遍历每个实现类知道找到合法的实现类
    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()) {
          if (log.isDebugEnabled()) {
            log.debug("VFS implementation " + impl.getName() +
              " is not valid in this environment.");
          }
        }
      } catch (InstantiationException e) {
        log.error("Failed to instantiate " + impl, e);
        return null;
      } catch (IllegalAccessException e) {
        log.error("Failed to instantiate " + impl, e);
        return null;
      }
    }

    if (log.isDebugEnabled()) {
      log.debug("Using VFS adapter " + vfs.getClass().getName());
    }
    // 给单例赋值
    VFS.instance = vfs;
    return VFS.instance;
  }

【解析】
  这里有两点要注意的地方,第一是单例采用懒加载的模式,但是由于 instance 被声明为 static 所以不需要额外加锁来保证线程安全,这是一种优雅的写法值得学习;第二是实例化对象时,会优先选择用户实现类实例化,因为在不同的环境中可能会使用不同的,为了检验 VFS 的运行环境是否合法,还需要调用 #isValid() 方法校验,正常实例化且校验通过后,赋值给单例并返回。

6.3 反射操作

  VFS 还提供了反射操作相关方法,如下:

  // 1. 通过类的全限定名加载类
  protected static Class<?> getClass(String className) {
    try {
      return Thread.currentThread().getContextClassLoader().loadClass(className);
    } catch (ClassNotFoundException e) {
      if (log.isDebugEnabled()) {
        log.debug("Class not found: " + className);
      }
      return null;
    }
  }
  
  // 2. 通过方法名和参数类型获取类方法 Method 对象
  protected static Method getMethod(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
    if (clazz == null) {
      return null;
    }
    try {
      return clazz.getMethod(methodName, parameterTypes);
    } catch (SecurityException e) {
      log.error("Security exception looking for method " + clazz.getName() + "." + methodName + ".  Cause: " + e);
      return null;
    } catch (NoSuchMethodException e) {
      log.error("Method not found " + clazz.getName() + "." + methodName + "." + methodName + ".  Cause: " + e);
      return null;
    }
  }

  // 3. 通过反射调用类方法
  protected static <T> T invoke(Method method, Object object, Object... parameters)
      throws IOException, RuntimeException {
    try {
      return (T) method.invoke(object, parameters);
    } catch (IllegalArgumentException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    } catch (InvocationTargetException e) {
      if (e.getTargetException() instanceof IOException) {
        throw (IOException) e.getTargetException();
      } else {
        throw new RuntimeException(e);
      }
    }
  }

6.4 list

【功能】找到指定路径下的所有类对象,并以字符串形式返回类的路径。
【源码与注解】

  public List<String> list(String path) throws IOException {
    // 保存加载到的类路径的列表
    List<String> names = new ArrayList<String>();
    // (1)调用 #getResources(String path) 加载指定路径下的资源,返回值是一个URL对象列表
    for (URL url : getResources(path)) {
      // (2)解析URL对象中的类并添加到names中
      names.addAll(list(url, path));
    }
    return names;
  }
  protected static List<URL> getResources(String path) throws IOException {
    return Collections.list(Thread.currentThread().getContextClassLoader().getResources(path));
  }
protected abstract List<String> list(URL url, String forPath) throws IOException;

6.5 DefaultVFS

  DefaultVFS 是 VFS 的默认实现子类,它的 #list 方法的实现中,主要分两个方向,一种是直接从提供的类路径中加载类;另一种是从 Jar 路径中加载类,此时会先加载路径中的 jar 包,再加载 jar 包中的资源列表。

6.5.1 list

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

      // 尝试从 URL 路径中找到 JAR 包文件,文件中会包含需要加载的资源
      // (1)如果找到了jar包,调用 #listResources 加载资源
      URL jarUrl = findJarForResource(url);
      if (jarUrl != null) {
        is = jarUrl.openStream();
        if (log.isDebugEnabled()) {
          log.debug("Listing " + url);
        }
        resources = listResources(new JarInputStream(is), path);
      }
      else {
        List<String> children = new ArrayList<String>();
        try {
          // (2)在有些情况下 URL 引用的资源实际上并不是一个 jar,但是打开一个URL的连接
          // 返回的流对象是一个JAR的流
          if (isJar(url)) {
            // Some versions of JBoss VFS might give a JAR stream even if the resource
            // referenced by the URL isn't actually a JAR
            is = url.openStream();
            JarInputStream jarInput = new JarInputStream(is);
            if (log.isDebugEnabled()) {
              log.debug("Listing " + url);
            }
            for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
              if (log.isDebugEnabled()) {
                log.debug("Jar entry: " + entry.getName());
              }
              children.add(entry.getName());
            }
            jarInput.close();
          }
          else {
            // (3)最常见的情况,通常是加载工程中的资源,不用去加载jar包
            // 这里可能有两种情况:
            // (3.1) url 指向的可能是一个路径,那么下面的line就是每个资源文件名
            // (3.2) url 指向的可能是一个文件,那么下面的line就是文件中每行的文本内容
            is = url.openStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
            List<String> lines = new ArrayList<String>();
            for (String line; (line = reader.readLine()) != null;) {
              if (log.isDebugEnabled()) {
                log.debug("Reader entry: " + line);
              }
              lines.add(line);
              // (3.3)如果line为文件文本,则这里不会加载到内容,清空lines,并跳出循环
              // lines是用来保存资源文件名的列表
              if (getResources(path + "/" + line).isEmpty()) {
                lines.clear();
                break;
              }
            }

            if (!lines.isEmpty()) {
              if (log.isDebugEnabled()) {
                log.debug("Listing " + url);
              }
              children.addAll(lines);
            }
          }
        } catch (FileNotFoundException e) {
          /*
           * For file URLs the openStream() call might fail, depending on the servlet
           * container, because directories can't be opened for reading. If that happens,
           * then list the directory directly instead.
           */
          if ("file".equals(url.getProtocol())) {
            File file = new File(url.getFile());
            if (log.isDebugEnabled()) {
                log.debug("Listing directory " + file.getAbsolutePath());
            }
            if (file.isDirectory()) {
              if (log.isDebugEnabled()) {
                  log.debug("Listing " + url);
              }
              children = Arrays.asList(file.list());
            }
          }
          else {
            // No idea where the exception came from so rethrow it
            throw e;
          }
        }

        // (3.4)给 url 路径补上 /
        String prefix = url.toExternalForm();
        if (!prefix.endsWith("/")) {
          prefix = prefix + "/";
        }

        // (3.5)child指向路径有可能是一个目录,也是一个文件
        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) {
          // Ignore
        }
      }
    }
  }

【解析】

6.5.2 findJarForResource

【功能】如果URL中有指向一个Jar文件资源,则返回该Jar Resource,否则返回null。

  protected URL findJarForResource(URL url) throws MalformedURLException {
    if (log.isDebugEnabled()) {
      log.debug("Find JAR URL: " + url);
    }

    // 这段代码看起来比较神奇,虽然看起来没有 break 的条件,但是是通过 MalformedURLException 异常进行
    // 正如上面英文注释,如果 URL 的文件部分本身就是 URL ,那么该 URL 可能指向 JAR
    try {
      for (;;) {
        url = new URL(url.getFile());
        if (log.isDebugEnabled()) {
          log.debug("Inner URL: " + url);
        }
      }
    } catch (MalformedURLException e) {
      // This will happen at some point and serves as a break in the loop
    }

    // 找到 url 中的jar文件的路径部分
    StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
    int index = jarUrl.lastIndexOf(".jar");
    if (index >= 0) {
      jarUrl.setLength(index + 4);
      if (log.isDebugEnabled()) {
        log.debug("Extracted JAR URL: " + jarUrl);
      }
    }
    else {
      if (log.isDebugEnabled()) {
        log.debug("Not a JAR: " + jarUrl);
      }
      return null;
    }

    // 创建jar文件路径对应的URL对象,并测试是否为一个真实的jar
    try {
      URL testUrl = new URL(jarUrl.toString());
      if (isJar(testUrl)) {
        return testUrl;
      }
      else {
        // WebLogic fix: check if the URL's file exists in the filesystem.
        if (log.isDebugEnabled()) {
          log.debug("Not a JAR: " + jarUrl);
        }
        jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
        File file = new File(jarUrl.toString());

        // 处理路径编码问题
        if (!file.exists()) {
          try {
            file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
          } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Unsupported encoding?  UTF-8?  That's unpossible.");
          }
        }

        // 处理完编码问题再次再次确认文件是否存在
        if (file.exists()) {
          if (log.isDebugEnabled()) {
            log.debug("Trying real file: " + file.getAbsolutePath());
          }
          testUrl = file.toURI().toURL();
          if (isJar(testUrl)) {
            return testUrl;
          }
        }
      }
    } catch (MalformedURLException e) {
      log.warn("Invalid JAR URL: " + jarUrl);
    }

    if (log.isDebugEnabled()) {
      log.debug("Not a JAR: " + jarUrl);
    }
    return null;
  }

6.5.3 listResources

【功能】遍历 Jar Resources。

  protected List<String> listResources(JarInputStream jar, String path) throws IOException {
    // Include the leading and trailing slash when matching names
    // 前后补齐 /
    if (!path.startsWith("/")) {
      path = "/" + path;
    }
    if (!path.endsWith("/")) {
      path = path + "/";
    }

    // 遍历条目并收集以请求路径开头的条目
    // Iterate over the entries and collect those that begin with the requested path
    List<String> resources = new ArrayList<String>();
    for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
      if (!entry.isDirectory()) {
        // Add leading slash if it's missing
        String name = entry.getName();
        if (!name.startsWith("/")) {
          name = "/" + name;
        }

        // Check file name
        if (name.startsWith(path)) {
          if (log.isDebugEnabled()) {
            log.debug("Found resource: " + name);
          }
          // Trim leading slash
          resources.add(name.substring(1));
        }
      }
    }
    return resources;
  }

6.5.4 isJar

【功能】判断一个 URL 指向的资源是否是一个Jar资源。

  protected boolean isJar(URL url) {
    return isJar(url, new byte[JAR_MAGIC.length]);
  }

  protected boolean isJar(URL url, byte[] buffer) {
    InputStream is = null;
    try {
      is = url.openStream();
      // 根据Jar包前几个特殊的标识符,判断文件是否是一个Jar
      is.read(buffer, 0, JAR_MAGIC.length);
      if (Arrays.equals(buffer, JAR_MAGIC)) {
        if (log.isDebugEnabled()) {
          log.debug("Found JAR: " + url);
        }
        return true;
      }
    } catch (Exception e) {
      // Failure to read the stream means this is not a JAR
    } finally {
      if (is != null) {
        try {
          is.close();
        } catch (Exception e) {
          // Ignore
        }
      }
    }

    return false;
  }

6.6 测试案例

6.6.1 案例1

    @Test
    public void test_listProjResource() throws IOException {
        VFS vfs = VFS.getInstance();
        List<String> resources = vfs.list("org/apache/ibatis");
        for (String resource : resources) {
            System.out.println(resource);
        }
    }

  在 VFS 中会根据传入路径加载 URL 对象列表,再调用 DefaultVFS.list 处理。


  很明显这个 url 指向的不是一个 jar resource,看看 #findJarForResource 的处理。

  这里返回 null,进入下面的分支,就会调用 #isJar 判断是否 url 是否为一个 jar 文件。


  不匹配,进入下面分支的第二个分支,加载 url 对应的目录资源下的 .class 文件。

  得到由包路径和class文件组成的资源字符串,这也是 VFS.list 最后返回的资源字符串列表中返回字符串的形式。

  除了指向class文件的资源字符串,字符串还可能是目录和其他文件资源,如下:


6.6.2 案例2

    @Test
    public void test_listLibResource() throws IOException {
        VFS vfs = VFS.getInstance();
        List<String> resources = vfs.list("net/sf/cglib/beans");
        for (String resource : resources) {
            System.out.println(resource);
        }
    }

  如果要加载的资源路径是 Jar 包里面的,则要先加载Jar包,类加载器加载该路径时,可能得到带 jar 包的绝对路径的 URL。


  将 URL 路径 file:/E:/maven_repository/cglib/cglib/3.2.2/cglib-3.2.2.jar!/net/sf/cglib/beans 中的 file:/E:/maven_repository/cglib/cglib/3.2.2/cglib-3.2.2.jar 解析出来,创建一个指向 Jar 包的新的 URL 对象。

  加载 Jar 包中的资源并过滤掉非指定路径前缀的资源。

  逐个资源处理判断,最后放到列表返回,测试案例的结果如下:

  还有最后一个分支,是URL 引用的资源实际上并不是一个 jar,但是打开一个URL的连接返回的流对象是一个JAR的流,常规资源加载就上面两种情况,这种可能是通过网络去加载一些资源。


上一篇 下一篇

猜你喜欢

热点阅读