2021-05-04 19:36  阅读(1857)
文章分类:从 JDK 源码看 Java 文章标签:JavaJDK 源码
©  原文作者:超人汪小建(seaboat) 原文地址:https://blog.csdn.net/wangyangzhizhou/column/info/16032

前言

我们知道不同的操作系统有各自的文件系统,这些文件系统又存在很多差异,而Java 因为是跨平台的,所以它必须要统一处理这些不同平台文件系统之间的差异,才能往上提供统一的入口。

关于FileSystem类

JDK 里面抽象出了一个 FileSystem 来表示文件系统,不同的操作系统通过继承该类实现各自的文件系统,比如 Windows NT/2000 操作系统则为 WinNTFileSystem,而 unix-like 操作系统为 UnixFileSystem。

需要注意的一点是,WinNTFileSystem类 和 UnixFileSystem类并不是在同一个 JDK 里面,也就是说它们是分开的,你只能在 Windows 版本的 JDK 中找到 WinNTFileSystem,而在 Linux 版本的 JDK 中找到 UnixFileSystem,同样地,其他操作系统也有自己的文件系统实现类。

这里分成两个系列分析 JDK 对两种(Windows 和Linux)操作系统的文件系统的实现类,先讲 Windows操作系统,对应为 WinNTFileSystem 类。 由于篇幅较长,《JDK不同操作系统的FileSystem(Windows)》分为上中下篇,此为上篇。

继承结构

        --java.lang.Object
          --java.io.FileSystem
            --java.io.WinNTFileSystem
    

类定义

        class WinNTFileSystem extends FileSystem
    

主要属性

  • slash 表示斜杠符号。
  • altSlash 与slash相反的斜杠。
  • semicolon 表示分号。
  • driveDirCache 表示驱动盘目录缓存。
  • cache 用于缓存标准路径。
  • prefixCache 用于缓存标准路径前缀。
    
            private final char slash;
            private final char altSlash;
            private final char semicolon;
            private static String[] driveDirCache = new String[26];
            private ExpiringCache cache       = new ExpiringCache();
            private ExpiringCache prefixCache = new ExpiringCache();
    

主要方法

构造方法

构造方法很简单,先通过 System.getProperties() 获取 Properties 对象,然后获取其里面的 file.separator 属性和 path.separator 属性的值, 分别赋值给相应变量,在 Windows 中这两个值分别为 \ 和 ; 。最后将斜杠 / 赋给 altSlash。

            public WinNTFileSystem() {
                Properties props = GetPropertyAction.privilegedGetProperties();
                slash = props.getProperty("file.separator").charAt(0);
                semicolon = props.getProperty("path.separator").charAt(0);
                altSlash = (this.slash == '\\') ? '/' : '\\';
            }
    

isSlash方法

判断是不是斜杠。

            private boolean isSlash(char c) {
                return (c == '\\') || (c == '/');
            }
    

isLetter方法

判断是不是字母。

            private boolean isLetter(char c) {
                return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
            }
    

slashify方法

判断一个字符串是否以斜杠开头,不是则帮其开头添加斜杠,是则不作处理。

            private String slashify(String p) {
                if ((p.length() > 0) && (p.charAt(0) != slash)) return slash + p;
                else return p;
            }
    

normalize方法

该方法主要是对路径进行标准化,它在实现过程中依赖另外一个 normalize 方法和 normalizePrefix 方法,这两个方法都是 private 的。

针对传入来的 path 变量,用一个 for 循环遍历每个字符,分别对以下三种情况处理,

  1. 当遇到 altSlash 时,即 / 时,则把 path 传入另外一个 normalize 方法中进行处理,其中涉及 prev == slash 判断条件,prev 其实就是前一个字符,相等就说明两个 / 连着。
  2. 当遇到连续两个 slash 时,即连续两个 \ 时,而且 i 还要大于1时,则把 path 传入另外一个 normalize 方法中进行处理。
  3. 当遇到 : 字符且 i 大于1时,则把 path 传入另外一个 normalize 方法中进行处理。

如果都不在上述情况内,则要继续判断最后一个字符是否为 slash ,如果是则还要传入另外一个 normalize 方法中进行处理。否则直接返回 path ,这时其实 path 就是以一个或两个 \ 开头且后面不再存在斜杠或反斜杠或冒号,这种情况是可以直接返回的。

            public String normalize(String path) {
                int n = path.length();
                char slash = this.slash;
                char altSlash = this.altSlash;
                char prev = 0;
                for (int i = 0; i < n; i++) {
                    char c = path.charAt(i);
                    if (c == altSlash)
                        return normalize(path, n, (prev == slash) ? i - 1 : i);
                    if ((c == slash) && (prev == slash) && (i > 1))
                        return normalize(path, n, i - 1);
                    if ((c == ':') && (i > 1))
                        return normalize(path, n, 0);
                    prev = c;
                }
                if (prev == slash) return normalize(path, n, n - 1);
                return path;
            }
    

往下看具体的处理逻辑,这里有三个参数,第一个是路径字符串,第二个是路径长度,第三个是路径字符串的偏移,偏移量用来表示从哪个位置开始,偏移量 off 不能小于3,这是考虑到了UNC路径。继续往下如果偏移量等于0的话则先处理前缀,这时调用 normalizePrefix 方法处理。偏移量非0的情况下则表示已经有部分已经标准化好了,将其先 append 到 StringBuilder 对象中。

接着开始处理从偏移量开始到结尾的路径,用 while 循环遍历剩余路径中的每个字符,如果有连着都是斜杠的情况则跳过重复的斜杠,这里斜杠包括了 /\ 。非斜杠的情况则直接将字符 append 到 StringBuilder 对象中,多个斜杠则只添加一个斜杠。最后 src == len 条件则表示已经到结尾了,这时要考虑一些特殊情况的处理,比如 c:\\\\\\\\

          private String normalize(String path, int len, int off) {
              if (len == 0) return path;
              if (off < 3) off = 0;   
              int src;
              char slash = this.slash;
              StringBuilder sb = new StringBuilder(len);
              if (off == 0) {
                  src = normalizePrefix(path, len, sb);
              } else {
                  src = off;
                  sb.append(path, 0, off);
              }
              while (src < len) {
                  char c = path.charAt(src++);
                  if (isSlash(c)) {
                      while ((src < len) && isSlash(path.charAt(src))) src++;
                      if (src == len) {
                          int sn = sb.length();
                          if ((sn == 2) && (sb.charAt(1) == ':')) {
                              sb.append(slash);
                              break;
                          }
                          if (sn == 0) {
                              sb.append(slash);
                              break;
                          }
                          if ((sn == 1) && (isSlash(sb.charAt(0)))) {
                              sb.append(slash);
                              break;
                          }
                          break;
                      } else {
                          sb.append(slash);
                      }
                  } else {
                      sb.append(c);
                  }
              }
              return sb.toString();
          }
    

正常情况下,Windows的路径不会存在连着的两个斜杠(除了UNC路径可能会两个斜杠开头),同时也不会以斜杠结束。路径一般分为:目录相对路径、驱动盘相对路径、UNC绝对路径和本地绝对路径。以下两种逻辑分别处理类似c:\\

          private int normalizePrefix(String path, int len, StringBuilder sb) {
              int src = 0;
              while ((src < len) && isSlash(path.charAt(src))) src++;
              char c;
              if ((len - src >= 2)
                  && isLetter(c = path.charAt(src))
                  && path.charAt(src + 1) == ':') {
                  sb.append(c);
                  sb.append(':');
                  src += 2;
              } else {
                  src = 0;
                  if ((len >= 2)
                      && isSlash(path.charAt(0))
                      && isSlash(path.charAt(1))) {
                      src = 1;
                      sb.append(slash);
                  }
              }
              return src;
          }
    

综上处理逻辑,为帮助我们更好地理解,用以下不同路径格式看看对应的标准化后是什么样的。

              System.out.println(f.normalize("d:\\\\test\\test////"));
              System.out.println(f.normalize("d://test\\test////"));
              System.out.println(f.normalize("d://test\\test////test.txt"));
              System.out.println(f.normalize("d:\\test/test\\\\"));
              System.out.println(f.normalize("d:\\test/test/"));
              System.out.println(f.normalize("d:/test/test//"));
              System.out.println(f.normalize("test\\"));
              System.out.println(f.normalize("\\"));
              System.out.println(f.normalize("/"));
              System.out.println(f.normalize("c:\\"));
              System.out.println(f.normalize("c:test"));
              System.out.println(f.normalize("/c:/test"));
              System.out.println(f.normalize("file://c:/test"));
              System.out.println(f.normalize("\\\\test\\"));
              System.out.println(f.normalize("\\\\test/"));
    
        d:\test\test
        d:\test\test
        d:\test\test\test.txt
        d:\test\test
        d:\test\test
        d:\test\test
        test
        \
        \
        c:\
        c:test
        c:\test
        file:\c:\test
        \\test
        \\test
    

prefixLength方法

该方法主要是获取路径前缀的长度。按照顺序看下逻辑,获取第一个第二个字符,如果都为 slash ,即两个\,则为 UNC 路径,形如 \\test,返回2;如果第二个字符不是\则为驱动盘相对路径,形如\test,返回1;当第一个字符为字母且第二个为:时,如果第三个字符为\,则为本地绝对路径,形如c:\test,返回3;如果第三个字符为非\,则为目录相对路径,形如c:test;最后则为相对路径,形如test

            public int prefixLength(String path) {
                char slash = this.slash;
                int n = path.length();
                if (n == 0) return 0;
                char c0 = path.charAt(0);
                char c1 = (n > 1) ? path.charAt(1) : 0;
                if (c0 == slash) {
                    if (c1 == slash) return 2;  
                    return 1;                   
                }
                if (isLetter(c0) && (c1 == ':')) {
                    if ((n > 2) && (path.charAt(2) == slash))
                        return 3;               
                    return 2;                   
                }
                return 0;                       
            }
    

getUserPath方法

通过 System 获取 user.dir 属性作为用户路径。

        private String getUserPath() {
                return normalize(System.getProperty("user.dir"));
            }
    

getDrive方法

获取驱动盘,先获取路径头部长度,再截取驱动盘。

            private String getDrive(String path) {
                int pl = prefixLength(path);
                return (pl == 3) ? path.substring(0, 2) : null;
            }
    

driveIndex方法

获取驱动盘的索引值,按照字母顺序,比如 a 或 A 则索引值为0。

            private static int driveIndex(char d) {
                if ((d >= 'a') && (d <= 'z')) return d - 'a';
                if ((d >= 'A') && (d <= 'Z')) return d - 'A';
                return -1;
            }
    

getDriveDirectory方法

获取指定驱动盘下的工作目录,每个驱动盘都有工作目录。可以看到有两个 getDriveDirectory 方法,其中一个本地方法,实现需要本地方法来支持。其中逻辑是先根据驱动盘获取对应的驱动盘索引,然后再将索引加一并通过本地方法获取对应驱动盘当前工作目录,这里还会将其缓存起来,方便后面查询。

            private native String getDriveDirectory(int drive);
    
            private String getDriveDirectory(char drive) {
                int i = driveIndex(drive);
                if (i < 0) return null;
                String s = driveDirCache[i];
                if (s != null) return s;
                s = getDriveDirectory(i + 1);
                driveDirCache[i] = s;
                return s;
            }
    

本地的实现如下,主要看函数 currentDir,先通过操作系统的API函数 GetDriveTypeW 判断是否为不合格的驱动盘类型,这其中参数都是用宽字符。接着通过 _wgetdcwd 函数获取指定驱动器上的当前工作目录的完整路径,同时去掉驱动盘和冒号,返回给 Java 层一个表示当前工作目录路径的字符串。

        JNIEXPORT jobject JNICALL
        Java_java_io_WinNTFileSystem_getDriveDirectory(JNIEnv *env, jobject this,
                                                       jint drive)
        {
            jstring ret = NULL;
            jchar *p = currentDir(drive);
            jchar *pf = p;
            if (p == NULL) return NULL;
            if (iswalpha(*p) && (p[1] == L':')) p += 2;
            ret = (*env)->NewString(env, p, (jsize)wcslen(p));
            free (pf);
            return ret;
        }
    
        WCHAR*
        currentDir(int di) {
            UINT dt;
            WCHAR root[4];
            root[0] = L'A' + (WCHAR)(di - 1);
            root[1] = L':';
            root[2] = L'\\';
            root[3] = L'\0';
            dt = GetDriveTypeW(root);
            if (dt == DRIVE_UNKNOWN || dt == DRIVE_NO_ROOT_DIR) {
                return NULL;
            } else {
                return _wgetdcwd(di, NULL, MAX_PATH);
            }
        }
    

resolve方法

有两个resolve方法。

第一个 resolve 方法主要是针对传入的两个参数,一个是父路径一个是子路径,对它们进行解析然后得到一个新路径。此过程需要考虑两个路径的格式。逻辑如下:

  1. 先分别获取父路径长度和子路径长度。
  2. 根据父路径判断是否为目录相对路径,形如c:的。
  3. 若子路径以 slash 即\开头,则可能是 UNC 路径,这时要丢弃它的头部,所以子路径从第2的位置开始;也可能是驱动盘相对路径,这时丢弃它的头部,子路径从第1的位置开始;最后如果子路径为两个 slash 即 \\时,则直接返回父路径,当然父路径如果以 slash 结尾也要将其去掉。
  4. 此时确定好了父路径的长度、父路径的结束位置、子路径的长度和子路径的开始位置,就可以得到最终的新路径的长度了。
  5. 根据上述的长度和位置信息将父路径和子路径合并,返回一个新的路径。
            public String resolve(String parent, String child) {
                int pn = parent.length();
                if (pn == 0) return child;
                int cn = child.length();
                if (cn == 0) return parent;
    
                String c = child;
                int childStart = 0;
                int parentEnd = pn;
    
                boolean isDirectoryRelative =
                    pn == 2 && isLetter(parent.charAt(0)) && parent.charAt(1) == ':';
    
                if ((cn > 1) && (c.charAt(0) == slash)) {
                    if (c.charAt(1) == slash) {
                        childStart = 2;
                    } else if (!isDirectoryRelative) {
                        childStart = 1;
    
                    }
                    if (cn == childStart) {
                        if (parent.charAt(pn - 1) == slash)
                            return parent.substring(0, pn - 1);
                        return parent;
                    }
                }
    
                if (parent.charAt(pn - 1) == slash)
                    parentEnd--;
    
                int strlen = parentEnd + cn - childStart;
                char[] theChars = null;
                if (child.charAt(childStart) == slash || isDirectoryRelative) {
                    theChars = new char[strlen];
                    parent.getChars(0, parentEnd, theChars, 0);
                    child.getChars(childStart, cn, theChars, parentEnd);
                } else {
                    theChars = new char[strlen + 1];
                    parent.getChars(0, parentEnd, theChars, 0);
                    theChars[parentEnd] = slash;
                    child.getChars(childStart, cn, theChars, parentEnd + 1);
                }
                return new String(theChars);
            }
    

第二个 resolve 方法传入的是 File,主要是根据 File 对应的不同类型路径解析处理然后返回。

  1. 获取路径头部。
  2. 如果头部长为2且以\开头,此时为 UNC 路径,直接返回路径。
  3. 如果头部长为3,则为本地绝对路径,直接返回路径。
  4. 如果长度为0,则为相对路径,返回用户路径+此相对路径。
  5. 如果长度为1,则为驱动盘相对路径,此时尝试根据用户路径获取驱动盘,存在驱动盘则返回驱动盘+此路径,不存在驱动盘则说明用户路径是一个 UNC 路径,返回用户路径+此路径。
  6. 如果头部长度为2,则为目录相对路径。此时先获取用户路径,再根据用户路径获取对应驱动盘,如果路径以驱动盘开头,则直接返回用户路径+去掉驱动盘后的路径。如果继续往下则通过 getDriveDirectory 获取指定驱动盘的工作目录,将驱动盘+:+工作目录+路径等拼接起来得到最终的新路径,然后还要用安全管理器检查是否有读的权限。
        public String resolve(File f) {
                String path = f.getPath();
                int pl = f.getPrefixLength();
                if ((pl == 2) && (path.charAt(0) == slash))
                    return path;                       
                if (pl == 3)
                    return path;                        
                if (pl == 0)
                    return getUserPath() + slashify(path);
                if (pl == 1) {                          
                    String up = getUserPath();
                    String ud = getDrive(up);
                    if (ud != null) return ud + path;
                    return up + path;                   
                }
                if (pl == 2) {                
                    String up = getUserPath();
                    String ud = getDrive(up);
                    if ((ud != null) && path.startsWith(ud))
                        return up + slashify(path.substring(2));
                    char drive = path.charAt(0);
                    String dir = getDriveDirectory(drive);
                    String np;
                    if (dir != null) {
                        String p = drive + (':' + dir + slashify(path.substring(2)));
                        SecurityManager security = System.getSecurityManager();
                        try {
                            if (security != null) security.checkRead(p);
                        } catch (SecurityException x) {
                            throw new SecurityException("Cannot resolve path " + path);
                        }
                        return p;
                    }
                    return drive + ":" + slashify(path.substring(2));
                }
                throw new InternalError("Unresolvable path: " + path);
            }
    
点赞(1)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> JDK不同操作系统的FileSystem(Windows)上篇
上一篇
从JDK源码看Writer
下一篇
JDK不同操作系统的FileSystem(Windows)中篇