美文网首页MyBatis源码剖析
[MyBatis源码分析 - 资源加载模块]

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

作者: 小胡_鸭 | 来源:发表于2020-11-08 22:58 被阅读0次

    一、类加载

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

    • Bootstrap ClassLoader:负责加载 JDK 核心类库(rt.jar)的类加载器,同时是所有类加载器的父加载器,并且是最顶层的类加载器,就好像 Object 是最顶层的基类一样。
    • Extension ClassLoader:加载 Java 的扩展类库,也就是从 jre/lib/ext 目录下或者 java.ext.dirs 系统属性指定的目录下加载类,该加载器是 Bootstrap ClassLoader 的子加载器。
    • System ClassLoader:负责从 classpath 环境变量中加载类文件,是 Extension ClassLoder 的子加载器,通常应用代码就是由该加载器加载。

      测试案例:

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

    【解析】

    • (1)将包名转化为包路径,eg: org.apache.ibatis.io -> org/apache/ibatis/io,调用 #getPackagePath() 方法处理,如下:
      protected String getPackagePath(String packageName) {
        return packageName == null ? null : packageName.replace('.', '/');
      }
    
    • (2)通过 VFS 接口获取包路径中的所有资源文件,List 中的字符串形如 org/apache/ibatis/io/Resources.class
    • (3)只处理class文件。
    • (4)测试类是否符合条件,是则添加到matches集合中,#addIfMatching() 方法如下:
      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;
    
    • IMPLEMENTATIONS:框架内建的实现类。
    • USER_IMPLEMENTATIONS:通过调用 #addImplClass(Class) 方法添加的用户实现类,该方法如下:
      public static void addImplClass(Class<? extends VFS> clazz) {
        if (clazz != null) {
          USER_IMPLEMENTATIONS.add(clazz);
        }
      }
    
    • instance:单例模式,获取 VFS 实例要调用 #getInstance 方法,使得调用者无需关心底层实现类的逻辑。

    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;
      }
    
    • (1)调用 #getResources(String path) 加载指定路径下的资源,返回值是一个URL对象列表,该方法就是使用类加载器去加载资源,如下:
      protected static List<URL> getResources(String path) throws IOException {
        return Collections.list(Thread.currentThread().getContextClassLoader().getResources(path));
      }
    
    • (2)解析URL下所有类路径并添加到names中,调用 #list(URL url, String forPath) 方法实现,在 VFS 中该方法是抽象方法,具体实现逻辑取决于 VFS 的子类。
    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
            }
          }
        }
      }
    

    【解析】

    • (1)调用 #listResources 加载资源,试从 URL 路径中找到 JAR 包文件,文件中会包含需要加载的资源。
    • (2)在有些情况下 URL 引用的资源实际上并不是一个 jar,但是打开一个URL的连接返回的流对象是一个JAR的流。
    • (3)最常见的情况,通常是加载工程中的资源,不用去加载jar包
      • (3.1) url 指向的可能是一个路径,那么下面的line就是每个资源文件名。
      • (3.2)url 指向的可能是一个文件,那么下面的line就是文件中每行的文本内容。
      • (3.3)如果line为文件文本,则这里不会加载到内容,清空 lines,并跳出循环。
      • (3.4)给 url 路径补上 /。
      • (3.5)child 指向路径有可能是一个目录,也是一个文件,将 child 添加到要返回的 resources 列表中,再递归调用 #list 方法加载子目录下的资源,如果文件中的每行存储了指向资源的路径列表,也会加载对应的资源。

    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的流,常规资源加载就上面两种情况,这种可能是通过网络去加载一些资源。


    相关文章

      网友评论

        本文标题:[MyBatis源码分析 - 资源加载模块]

        本文链接:https://www.haomeiwen.com/subject/gtqnvktx.html