# 精尽 MyBatis 源码分析 —— IO 模块 # 1. 概述 本文,我们来分享 MyBatis 的 IO 模块,对应 `io` 包。如下图所示:[![`io` 包](11-mybatis-IO 模块.assets/01.png)](http://static.iocoder.cn/images/MyBatis/2020_01_28/01.png)`io` 包 在 [《精尽 MyBatis 源码解析 —— 项目结构一览》](http://svip.iocoder.cn/MyBatis/intro) 中,简单介绍了这个模块如下: > 资源加载模块,主要是对类加载器进行封装,确定类加载器的使用顺序,并提供了加载类文件以及其他资源文件的功能 。 本文涉及的类如下图所示:[![类图](11-mybatis-IO 模块.assets/02.png)](http://static.iocoder.cn/images/MyBatis/2020_01_28/02.png)类图 # 2. ClassLoaderWrapper `org.apache.ibatis.io.ClassLoaderWrapper` ,ClassLoader 包装器。可使用多个 ClassLoader 加载对应的资源,直到有一成功后返回资源。 ## 2.1 构造方法 ``` // ClassLoaderWrapper.java /** * 默认 ClassLoader 对象 */ ClassLoader defaultClassLoader; /** * 系统 ClassLoader 对象 */ ClassLoader systemClassLoader; ClassLoaderWrapper() { try { systemClassLoader = ClassLoader.getSystemClassLoader(); } catch (SecurityException ignored) { // AccessControlException on Google App Engine } } ``` - `defaultClassLoader` 属性,默认 ClassLoader 对象。目前不存在初始化该属性的构造方法。可通过 `ClassLoaderWrapper.defaultClassLoader = xxx` 的方式,进行设置。 - `systemClassLoader` 属性,系统 ClassLoader 对象。在构造方法中,已经初始化。 ## 2.2 getClassLoaders `#getClassLoaders(ClassLoader classLoader)` 方法,获得 ClassLoader 数组。代码如下: ``` // ClassLoaderWrapper.java ClassLoader[] getClassLoaders(ClassLoader classLoader) { return new ClassLoader[]{ classLoader, defaultClassLoader, Thread.currentThread().getContextClassLoader(), getClass().getClassLoader(), systemClassLoader}; } ``` ## 2.3 getResourceAsURL `#getResourceAsURL(String resource, ...)` 方法,获得指定资源的 URL 。代码如下: ``` // ClassLoaderWrapper.java /** * Get a resource as a URL using the current class path * * @param resource - the resource to locate * @return the resource or null */ public URL getResourceAsURL(String resource) { return getResourceAsURL(resource, getClassLoaders(null)); } /** * Get a resource from the classpath, starting with a specific class loader * * @param resource - the resource to find * @param classLoader - the first classloader to try * @return the stream or null */ public URL getResourceAsURL(String resource, ClassLoader classLoader) { return getResourceAsURL(resource, getClassLoaders(classLoader)); } ``` - 先调用 `#getClassLoaders(ClassLoader classLoader)` 方法,获得 ClassLoader 数组。 - 再调用 `#getResourceAsURL(String resource, ClassLoader[] classLoader)` 方法,获得指定资源的 InputStream 。代码如下: ``` // ClassLoaderWrapper.java /** * Get a resource as a URL using the current class path * * @param resource - the resource to locate * @param classLoader - the class loaders to examine * @return the resource or null */ URL getResourceAsURL(String resource, ClassLoader[] classLoader) { URL url; // 遍历 ClassLoader 数组 for (ClassLoader cl : classLoader) { if (null != cl) { // 获得 URL ,不带 / // look for the resource as passed in... url = cl.getResource(resource); // 获得 URL ,带 / // ...but some class loaders want this leading "/", so we'll add it // and try again if we didn't find the resource if (null == url) { url = cl.getResource("/" + resource); } // "It's always in the last place I look for it!" // ... because only an idiot would keep looking for it after finding it, so stop looking already. // 成功获得到,返回 if (null != url) { return url; } } } // didn't find it anywhere. return null; } ``` ## 2.4 getResourceAsStream `#getResourceAsStream(String resource, ...)` 方法,获得指定资源的 InputStream 对象。代码如下: ``` // ClassLoaderWrapper.java /** * Get a resource from the classpath * * @param resource - the resource to find * @return the stream or null */ public InputStream getResourceAsStream(String resource) { return getResourceAsStream(resource, getClassLoaders(null)); } /** * Get a resource from the classpath, starting with a specific class loader * * @param resource - the resource to find * @param classLoader - the first class loader to try * @return the stream or null */ public InputStream getResourceAsStream(String resource, ClassLoader classLoader) { return getResourceAsStream(resource, getClassLoaders(classLoader)); } ``` - 先调用 `#getClassLoaders(ClassLoader classLoader)` 方法,获得 ClassLoader 数组。 - 再调用 `#getResourceAsStream(String resource, ClassLoader[] classLoader)` 方法,获得指定资源的 InputStream 。代码如下: ``` // ClassLoaderWrapper.java InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) { // 遍历 ClassLoader 数组 for (ClassLoader cl : classLoader) { if (null != cl) { // 获得 InputStream ,不带 / // try to find the resource as passed InputStream returnValue = cl.getResourceAsStream(resource); // now, some class loaders want this leading "/", so we'll add it and try again if we didn't find the resource // 获得 InputStream ,带 / if (null == returnValue) { returnValue = cl.getResourceAsStream("/" + resource); } // 成功获得到,返回 if (null != returnValue) { return returnValue; } } } return null; } ``` - 可使用多个 ClassLoader 加载对应的资源,直到有一成功后返回资源。 ## 2.5 classForName `#classForName(String name, ...)` 方法,获得指定类名对应的类。代码如下: ``` // ClassLoaderWrapper.java /** * Find a class on the classpath (or die trying) * * @param name - the class to look for * @return - the class * @throws ClassNotFoundException Duh. */ public Class classForName(String name) throws ClassNotFoundException { return classForName(name, getClassLoaders(null)); } /** * Find a class on the classpath, starting with a specific classloader (or die trying) * * @param name - the class to look for * @param classLoader - the first classloader to try * @return - the class * @throws ClassNotFoundException Duh. */ public Class classForName(String name, ClassLoader classLoader) throws ClassNotFoundException { return classForName(name, getClassLoaders(classLoader)); } ``` - 先调用 `#getClassLoaders(ClassLoader classLoader)` 方法,获得 ClassLoader 数组。 - 再调用 `#classForName(String name, ClassLoader[] classLoader)` 方法,获得指定类名对应的类。代码如下: ``` // ClassLoaderWrapper.java Class classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException { // 遍历 ClassLoader 数组 for (ClassLoader cl : classLoader) { if (null != cl) { try { // 获得类 Class c = Class.forName(name, true, cl); // 成功获得到,返回 if (null != c) { return c; } } catch (ClassNotFoundException e) { // we'll ignore this until all classloaders fail to locate the class } } } // 获得不到,抛出 ClassNotFoundException 异常 throw new ClassNotFoundException("Cannot find class: " + name); } ``` - 可使用多个 ClassLoader 加载对应的类,直到有一成功后返回类。 # 3. Resources `org.apache.ibatis.io.Resources` ,Resource 工具类。 ## 3.1 构造方法 ``` // Resources.java /** * ClassLoaderWrapper 对象 */ private static ClassLoaderWrapper classLoaderWrapper = new ClassLoaderWrapper(); /** * 字符集 */ private static Charset charset; Resources() { } public static void setDefaultClassLoader(ClassLoader defaultClassLoader) {defaultClassLoader classLoaderWrapper.defaultClassLoader = defaultClassLoader; // 修改 ClassLoaderWrapper. } public static void setCharset(Charset charset) { Resources.charset = charset; } ``` ## 3.2 getResource 基于 `classLoaderWrapper` 属性的封装。 ### 3.2.1 getResourceURL `#getResourceURL(String resource)` **静态**方法,获得指定资源的 URL 。代码如下: ``` // Resources.java 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); if (url == null) { throw new IOException("Could not find resource " + resource); } return url; } ``` ### 3.2.2 getResourceAsStream `#getResourceAsStream(String resource)` **静态**方法,获得指定资源的 InputStream 。代码如下: ``` // Resources.java 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; } ``` ### 3.2.3 getResourceAsReader `#getResourceAsReader(String resource)` **静态**方法,获得指定资源的 Reader 。代码如下: ``` // Resources.java 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; } ``` ### 3.2.4 getResourceAsFile `#getResourceAsFile(String resource)` **静态**方法,获得指定资源的 File 。代码如下: ``` // Resources.java 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()); } ``` - 基于 `classLoaderWrapper` 属性的封装。 ### 3.2.5 getResourceAsProperties `#getResourceAsProperties(ClassLoader loader)` **静态**方法,获得指定资源的 Properties 。代码如下: ``` // Resources.java public static Properties getResourceAsProperties(String resource) throws IOException { Properties props = new Properties(); // 读取 try (InputStream in = getResourceAsStream(resource)) { props.load(in); } return props; } public static Properties getResourceAsProperties(ClassLoader loader, String resource) throws IOException { Properties props = new Properties(); // 读取 try (InputStream in = getResourceAsStream(loader, resource)) { props.load(in); } return props; } ``` ## 3.3 getUrl ### 3.3.1 getUrlAsStream `#getUrlAsStream(String urlString)` **静态**方法,获得指定 URL 。代码如下: ``` // Resources.java public static InputStream getUrlAsStream(String urlString) throws IOException { URL url = new URL(urlString); // 打开 URLConnection URLConnection conn = url.openConnection(); return conn.getInputStream(); } ``` ### 3.3.2 getUrlAsReader `#getUrlAsReader(String urlString)` **静态**方法,指定 URL 的 Reader 。代码如下: ``` // Resources.java 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; } ``` ### 3.3.3 getUrlAsProperties `#getUrlAsReader(String urlString)` **静态**方法,指定 URL 的 Properties 。代码如下: ``` // Resources.java public static Properties getUrlAsProperties(String urlString) throws IOException { Properties props = new Properties(); try (InputStream in = getUrlAsStream(urlString)) { props.load(in); } return props; } ``` ## 3.4 classForName `#classForName(String className)` **静态**方法,获得指定类名对应的类。代码如下: ``` // Resources.java public static Class classForName(String className) throws ClassNotFoundException { return classLoaderWrapper.classForName(className); } ``` # 4. ResolverUtil `org.apache.ibatis.io.ResolverUtil` ,解析器工具类,用于获得指定目录**符合条件**的类们。 ## 4.1 Test Test ,匹配判断接口。代码如下: ``` // ResolverUtil.java 内部类 /** * A simple interface that specifies how to test classes to determine if they * are to be included in the results produced by the ResolverUtil. */ public interface Test { /** * Will be called repeatedly with candidate classes. Must return True if a class * is to be included in the results, false otherwise. */ boolean matches(Class type); } ``` ### 4.1.1 IsA IsA ,实现 Test 接口,判断是否为指定类。代码如下: ``` // ResolverUtil.java 内部类 /** * A Test that checks to see if each class is assignable to the provided class. Note * that this test will match the parent type itself if it is presented for matching. */ public static class IsA implements Test { /** * 指定类 */ private Class parent; /** Constructs an IsA test using the supplied Class as the parent class/interface. */ public IsA(Class parentType) { this.parent = parentType; } /** Returns true if type is assignable to the parent type supplied in the constructor. */ @Override public boolean matches(Class type) { return type != null && parent.isAssignableFrom(type); } } ``` ### 4.1.2 AnnotatedWith AnnotatedWith ,判断是否有指定注解。代码如下: ``` // ResolverUtil.java 内部类 /** * A Test that checks to see if each class is annotated with a specific annotation. If it * is, then the test returns true, otherwise false. */ public static class AnnotatedWith implements Test { /** * 注解 */ private Class annotation; /** Constructs an AnnotatedWith test for the specified annotation type. */ public AnnotatedWith(Class annotation) { this.annotation = annotation; } /** Returns true if the type is annotated with the class provided to the constructor. */ @Override public boolean matches(Class type) { return type != null && type.isAnnotationPresent(annotation); } } ``` ## 4.2 构造方法 ``` // ResolverUtil.java /** The set of matches being accumulated. */ private Set> matches = new HashSet<>(); // 符合条件的类的集合 private ClassLoader classloader; public Set> getClasses() { return matches; } public ClassLoader getClassLoader() { return classloader == null ? Thread.currentThread().getContextClassLoader() : classloader; } public void setClassLoader(ClassLoader classloader) { this.classloader = classloader; } ``` ## 4.3 find `#find(Test test, String packageName)` 方法,获得指定包下,符合条件的类。代码如下: ``` // ResolverUtil.java public ResolverUtil find(Test test, String packageName) { // <1> 获得包的路径 String path = getPackagePath(packageName); try { // <2> 获得路径下的所有文件 List children = VFS.getInstance().list(path); // <3> 遍历 for (String child : children) { // 是 Java Class if (child.endsWith(".class")) { // 如果匹配,则添加到结果集 addIfMatching(test, child); } } } catch (IOException ioe) { log.error("Could not read package: " + packageName, ioe); } return this; } ``` - `<1>` 处,调用 `#getPackagePath(String packageName)` 方法,获得包的路径。代码如下: ``` // ResolverUtil.java protected String getPackagePath(String packageName) { return packageName == null ? null : packageName.replace('.', '/'); } ``` - `<2>` 处,获得路径下的所有文件。详细解析,见 [「5. VFS」](https://svip.iocoder.cn/MyBatis/io-package/#) 。 - `<3>` 处,遍历 Java Class 文件,调用 `#addIfMatching(Test test, String fqn)` 方法,如果匹配,则添加到结果集。代码如下: ``` // ResolverUtil.java protected void addIfMatching(Test test, String fqn) { try { // 获得全类名 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); // 判断是否匹配 if (test.matches(type)) { matches.add((Class) type); } } catch (Throwable t) { log.warn("Could not examine class '" + fqn + "'" + " due to a " + t.getClass().getName() + " with message: " + t.getMessage()); } } ``` ``` * 使用对应的 `test` 的进行匹配。 ``` ### 4.3.1 findImplementations `#findImplementations(Class parent, String... packageNames)` 方法,判断指定目录下**们**,符合**指定类**的类们。代码如下: ``` // ResolverUtil.java public ResolverUtil findImplementations(Class parent, String... packageNames) { if (packageNames == null) { return this; } Test test = new IsA(parent); for (String pkg : packageNames) { find(test, pkg); } return this; } ``` ### 4.3.2 findAnnotated `#findAnnotated(Class annotation, String... packageNames)` 方法,判断指定目录下**们**,符合**指定注解**的类们。代码如下: ``` // ResolverUtil.java public ResolverUtil findAnnotated(Class annotation, String... packageNames) { if (packageNames == null) { return this; } Test test = new AnnotatedWith(annotation); for (String pkg : packageNames) { find(test, pkg); } return this; } ``` # 5. VFS `org.apache.ibatis.io.VFS` ,虚拟文件系统( Virtual File System )**抽象类**,用来查找指定路径下的的文件们。 ## 5.1 静态属性 ``` // VFS.java /** The built-in implementations. */ public static final Class[] IMPLEMENTATIONS = {JBoss6VFS.class, DefaultVFS.class}; // 内置的 VFS 实现类的数组 /** The list to which implementations are added by {@link #addImplClass(Class)}. */ public static final List> USER_IMPLEMENTATIONS = new ArrayList<>(); // 自定义的 VFS 实现类的数组 public static void addImplClass(Class clazz) { if (clazz != null) { USER_IMPLEMENTATIONS.add(clazz); } } ``` - `IMPLEMENTATIONS` **静态**属性,内置的 VFS 实现类的数组。目前 VFS 有 JBoss6VFS 和 DefaultVFS 两个实现类。 - `USER_IMPLEMENTATIONS` **静态**属性,自定义的 VFS 实现类的数组。可通过 `#addImplClass(Class clazz)` 方法,进行添加。 ## 5.2 getInstance `#getInstance()` 方法,获得 VFS 单例。代码如下: ``` // VFS.java public static VFS getInstance() { return VFSHolder.INSTANCE; } private static class VFSHolder { static final VFS INSTANCE = createVFS(); @SuppressWarnings("unchecked") static VFS createVFS() { // Try the user implementations first, then the built-ins List> impls = new ArrayList<>(); impls.addAll(USER_IMPLEMENTATIONS); impls.addAll(Arrays.asList((Class[]) IMPLEMENTATIONS)); // Try each implementation class until a valid one is found // 创建 VFS 对象,选择最后一个符合的 VFS vfs = null; for (int i = 0; vfs == null || !vfs.isValid(); i++) { Class 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 | IllegalAccessException e) { log.error("Failed to instantiate " + impl, e); return null; } } if (log.isDebugEnabled()) { log.debug("Using VFS adapter " + vfs.getClass().getName()); } return vfs; } } ``` - 单例有多种实现方式,该类采用的是“懒汉式,线程安全”,感兴趣的胖友,可以看看 [《单例模式的七种写法》](http://cantellow.iteye.com/blog/838473) 。 - `INSTANCE` 属性,最后通过 `#createVFS()` **静态**方法来创建,虽然 `USER_IMPLEMENTATIONS` 和 `IMPLEMENTATIONS` 有多种 VFS 的实现类,但是最终选择的是,**最后一个符合**的创建的 VFS 对象。 ## 5.3 反射相关方法 因为 VFS 自己有反射调用方法的需求,所以自己实现了三个方法。代码如下: ``` // VFS.java 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; } } 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; } } protected static T invoke(Method method, Object object, Object... parameters) throws IOException, RuntimeException { try { return (T) method.invoke(object, parameters); } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { if (e.getTargetException() instanceof IOException) { throw (IOException) e.getTargetException(); } else { throw new RuntimeException(e); } } } ``` ## 5.4 isValid `#isValid()` **抽象**方法,判断是否为合法的 VFS 。代码如下: ``` // VFS.java /** Return true if the {@link VFS} implementation is valid for the current environment. */ public abstract boolean isValid(); ``` - 该方法由子类实现。 ## 5.5 list `#list(String path)` 方法,获得指定路径下的所有资源。代码如下: ``` // VFS.java public List list(String path) throws IOException { List names = new ArrayList<>(); for (URL url : getResources(path)) { names.addAll(list(url, path)); } return names; } ``` - 先调用 `#getResources(String path)` **静态**方法,获得指定路径下的 URL 数组。代码如下: ``` // VFS.java protected static List getResources(String path) throws IOException { return Collections.list(Thread.currentThread().getContextClassLoader().getResources(path)); } ``` - 后遍历 URL 数组,调用 `#list(URL url, String forPath)` 方法,**递归**的列出所有的资源们。代码如下: ``` // VFS.java /** * Recursively list the full resource path of all the resources that are children of the * resource identified by a URL. * * @param url The URL that identifies the resource to list. * @param forPath The path to the resource that is identified by the URL. Generally, this is the * value passed to {@link #getResources(String)} to get the resource URL. * @return A list containing the names of the child resources. * @throws IOException If I/O errors occur */ protected abstract List list(URL url, String forPath) throws IOException; ``` - 该方法由子类进行实现。 ## 5.5 DefaultVFS `org.apache.ibatis.io.DefaultVFS` ,继承 VFS 抽象类,默认的 VFS 实现类。 ### 5.5.1 isValid ``` // DefaultVFS.java @Override public boolean isValid() { return true; } ``` - 都返回 `true` ,因为默认支持。 ### 5.5.2 list `#list(URL url, String path)` 方法,**递归**的列出所有的资源们。代码如下: ``` // DefaultVFS.java @Override public List list(URL url, String path) throws IOException { InputStream is = null; try { List 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 Resource ,则返回该 Jar Resource ,否则返回 null URL jarUrl = findJarForResource(url); if (jarUrl != null) { is = jarUrl.openStream(); if (log.isDebugEnabled()) { log.debug("Listing " + url); } // 遍历 Jar Resource resources = listResources(new JarInputStream(is), path); } else { List children = new ArrayList<>(); try { // 判断为 JAR URL 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(); try (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()); } } } else { /* * Some servlet containers allow reading from directory resources like a * text file, listing the child resources one per line. However, there is no * way to differentiate between directory and file resources just by reading * them. To work around that, as each line is read, try to look it up via * the class loader as a child of the current resource. If any line fails * then we assume the current resource is not a directory. */ // 【重点】<1> 获得路径下的所有资源 is = url.openStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); List lines = new ArrayList<>(); for (String line; (line = reader.readLine()) != null; ) { if (log.isDebugEnabled()) { log.debug("Reader entry: " + line); } lines.add(line); 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; } } // The URL prefix to use when recursively listing child resources // 【重点】<2> 计算 prefix String prefix = url.toExternalForm(); if (!prefix.endsWith("/")) { prefix = prefix + "/"; } // Iterate over immediate children, adding files and recursing into directories // 【重点】 <2> 遍历子路径 for (String child : children) { // 添加到 resources 中 String resourcePath = path + "/" + child; resources.add(resourcePath); // 递归遍历子路径,并将结果添加到 resources 中 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>` 和 `<2>` 处的代码,基本就可以了。大体逻辑就是,不断递归文件夹,获得到所有文件。设计到对 Jar 的处理,感兴趣的胖友,可以自己理解下。😈 艿艿暂时没看的特别细。 - `#findJarForResource(URL url)` 方法,如果 `url` 指向的是 Jar Resource ,则返回该 Jar Resource ,否则返回 `null` 。代码如下: ``` // DefaultVFS.java protected URL findJarForResource(URL url) throws MalformedURLException { if (log.isDebugEnabled()) { log.debug("Find JAR URL: " + url); } // If the file part of the URL is itself a URL, then that URL probably points to the JAR // 这段代码看起来比较神奇,虽然看起来没有 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 } // Look for the .jar extension and chop off everything after that // 判断是否意 .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 结尾,则直接返回 null } // Try to open and test it try { URL testUrl = new URL(jarUrl.toString()); // 判断是否为 Jar 文件 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()); // File name might be URL-encoded 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(); // 判断是否为 Jar 文件 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; } ``` - 会判断要求,`url` 以 `.jar` 结尾。 - `#isJar(URL url)` 方法,判断是否为 JAR URL 。代码如下: ``` // DefaultVFS.java /** The magic header that indicates a JAR (ZIP) file. */ private static final byte[] JAR_MAGIC = {'P', 'K', 3, 4}; 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(); // 读取文件头 is.read(buffer, 0, JAR_MAGIC.length); // 判断文件头的 magic number 是否符合 JAR 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; } ``` - `#listResources(JarInputStream jar, String path)` 方法,遍历 Jar Resource 。代码如下: ``` // DefaultVFS.java protected List 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 resources = new ArrayList<>(); 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; } ``` ## 5.6 JBoss6VFS `org.apache.ibatis.io.JBoss6VFS` ,继承 VFS 抽象类,基于 JBoss 的 VFS 实现类。使用时,需要引入如下: ``` org.jboss jboss-vfs ${version> ``` 因为实际基本没使用到,所以暂时不分析这个类。感兴趣的胖友,可以自己瞅瞅。还是简单的。反正艿艿暂时不感兴趣,哈哈哈。 # 666. 彩蛋 周末偷懒了,写到 1 点多,还没写完。这篇对 `DefaultVFS#list(URL url, String path)` 方法的源码解析,有点着急,代码看着有点乱和糟心。后续有时间,在详细补充下。 参考和推荐如下文章: - 无忌 [《MyBatis 源码解读之工具类》](https://my.oschina.net/wenjinglian/blog/1631911) - 徐郡明 [《MyBatis 技术内幕》](https://item.jd.com/12125531.html) 的 [「2.5 资源加载」](https://svip.iocoder.cn/MyBatis/io-package/#) 小节