JDK不同操作系统的FileSystem(Windows)中篇

作者:超人汪小建(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

getDefaultParent方法

返回默认的父路径,直接返回 slash 即\

    public String getDefaultParent() {
            return ("" + slash);
        }

fromURIPath方法

该方法主要是格式化路径。主要逻辑是完成类似以下的转换处理:
1. /c:/test –> c:/test
2. c:/test/ –> c:/test,但要注意,c:/ –> c:/,这是通过长度来限制的,即当长度超过3时才会去掉尾部的 /
3. /test/ –> /test

    public String fromURIPath(String path) {
            String p = path;
            if ((p.length() > 2) && (p.charAt(2) == ':')) {
                p = p.substring(1);
                if ((p.length() > 3) && p.endsWith("/"))
                    p = p.substring(0, p.length() - 1);
            } else if ((p.length() > 1) && p.endsWith("/")) {
                p = p.substring(0, p.length() - 1);
            }
            return p;
        }

isAbsolute方法

判断文件是否是绝对路径,先获取文件路径头部长度,如果长度为3则为绝对路径,如果长度为2且路径第一个字符为\也为绝对路径。

        public boolean isAbsolute(File f) {
            int pl = f.getPrefixLength();
            return (((pl == 2) && (f.getPath().charAt(0) == slash))
                    || (pl == 3));
        }

canonicalize方法

该方法用来标准化一个路径,标准路径不仅是一个绝对路径而且还是唯一的路径,而且标准的定义是依赖于操作系统的,一般在标准化路径时会先将路径转成绝对路径,然后才根据操作系统解析成唯一形式的路径。这个过程比较典型的就是处理包含”.”或”..”的路径,还有符号链接和驱动盘符号大小写等。

  1. 如果路径长度为2,且路径第一个字符为字母,且路径第二个字符为:,此时路径类似为c:,直接返回路径或加上:返回。
  2. 如果路径长度为3,且路径第一个字符为字母,且路径第二个字符为:,且路径第三个字符为\,此时路径类似为c:\,直接返回路径或加上:返回。
  3. 如果不使用缓存则直接调用 canonicalize0 本地方法获取标准化路径。
  4. 如果使用了缓存则在缓存中查找,存在则直接返回,否则先调用 canonicalize0 本地方法获取标准化路径,再将路径放进缓存中。
  5. 另外,还提供了前缀缓存可以使用,它缓存了标准路径的父目录,这样就可以节省了前缀部分的处理,前缀缓存的逻辑也是第一次标准化后将其缓存起来,下次则可从前缀缓存中查询。
  6. 使用前缀缓存来标准化路径时是调用 canonicalizeWithPrefix 方法的,该方法最终也是调一个本地方法 canonicalizeWithPrefix0,这里不再列出该方法代码,此本地方法与前面的 canonicalize0 方法的逻辑差不多,唯一不同的是它不用遍历整个路径从头开始检查路径的有效性,而只需要检查一次整个路径的有效性,这也是存在前缀缓存的原因,节省了一些工作。
        public String canonicalize(String path) throws IOException {
            int len = path.length();
            if ((len == 2) &&
                (isLetter(path.charAt(0))) &&
                (path.charAt(1) == ':')) {
                char c = path.charAt(0);
                if ((c >= 'A') && (c <= 'Z'))
                    return path;
                return "" + ((char) (c-32)) + ':';
            } else if ((len == 3) &&
                       (isLetter(path.charAt(0))) &&
                       (path.charAt(1) == ':') &&
                       (path.charAt(2) == '\\')) {
                char c = path.charAt(0);
                if ((c >= 'A') && (c <= 'Z'))
                    return path;
                return "" + ((char) (c-32)) + ':' + '\\';
            }
            if (!useCanonCaches) {
                return canonicalize0(path);
            } else {
                String res = cache.get(path);
                if (res == null) {
                    String dir = null;
                    String resDir = null;
                    if (useCanonPrefixCache) {
                        dir = parentOrNull(path);
                        if (dir != null) {
                            resDir = prefixCache.get(dir);
                            if (resDir != null) {
                                String filename = path.substring(1 + dir.length());
                                res = canonicalizeWithPrefix(resDir, filename);
                                cache.put(dir + File.separatorChar + filename, res);
                            }
                        }
                    }
                    if (res == null) {
                        res = canonicalize0(path);
                        cache.put(path, res);
                        if (useCanonPrefixCache && dir != null) {
                            resDir = parentOrNull(res);
                            if (resDir != null) {
                                File f = new File(res);
                                if (f.exists() && !f.isDirectory()) {
                                    prefixCache.put(dir, resDir);
                                }
                            }
                        }
                    }
                }
                return res;
            }
        }

    private native String canonicalize0(String path)
                throws IOException;

    private String canonicalizeWithPrefix(String canonicalPrefix,
                String filename) throws IOException
        {
            return canonicalizeWithPrefix0(canonicalPrefix,
                    canonicalPrefix + File.separatorChar + filename);
        }


    private native String canonicalizeWithPrefix0(String canonicalPrefix,
                String pathWithCanonicalPrefix)
                throws IOException;

因为标准化的具体实现是依赖于操作系统的,所以这部分工作交由本地方法去做,主要逻辑如下,
* 标准路径最大长度是 MAX_PATH_LENGTH 即1024个字符。
* 获取路径长度并且通过 currentDirLength 函数获取当前工作目录长度,该函数代码不再贴出来,主要逻辑是判断是否为驱动盘相对路径,如果是的话则根据驱动盘符号获取对应驱动盘的工作目录并返回该目录长度。否则就根据C的API函数 _wgetcwd 获取工作目录并返回长度。路径长度加上工作目录长度则为总长度。
* 通过 malloc 分配空间,注意这里是宽字符。
* 最后通过 wcanonicalize 函数标准化路径。代码较长,不再贴出,主要逻辑是,
1. 检查路径是否包含通配符,是则标准化失败。
2. 通过 _wfullpath 函数获取绝对路径,这个函数他会处理 \..等符号,比如 “test”、”\test” 和”..\test”,则会处理成”C:\Documents and Settings\user\My Documents\test”、”C:\test”和”C:\Documents and Settings\user\test”。
3. 检查路径中是否包含了非法的 .
4. 获取标准化驱动盘符号,将其大写,保存到结果字符数组中。
5. 如果是 UNC 路径的话,UNC路径格式如\\server\share\file_path,则此时分别获取 server 和 share,将其保存到结果字符数组中。
6. 通过循环检测剩余路径的有效性,主要是使用 FindFirstFileW 函数获取路径对应文件的属性信息,根据返回值可判断其有效性,有效地路径则保存到结果字符数组中。
7. 最终结果字符数组则为标准化后的路径。

    JNIEXPORT jstring JNICALL
    Java_java_io_WinNTFileSystem_canonicalize0(JNIEnv *env, jobject this,
                                               jstring pathname)
    {
        jstring rv = NULL;
        WCHAR canonicalPath[MAX_PATH_LENGTH];

        WITH_UNICODE_STRING(env, pathname, path) {
            int len = (int)wcslen(path);
            len += currentDirLength(path, len);
            if (len  > MAX_PATH_LENGTH - 1) {
                WCHAR *cp = (WCHAR*)malloc(len * sizeof(WCHAR));
                if (cp != NULL) {
                    if (wcanonicalize(path, cp, len) >= 0) {
                        rv = (*env)->NewString(env, cp, (jsize)wcslen(cp));
                    }
                    free(cp);
                }
            } else
            if (wcanonicalize(path, canonicalPath, MAX_PATH_LENGTH) >= 0) {
                rv = (*env)->NewString(env, canonicalPath, (jsize)wcslen(canonicalPath));
            }
        } END_UNICODE_STRING(env, path);
        if (rv == NULL) {
            JNU_ThrowIOExceptionWithLastError(env, "Bad pathname");
        }
        return rv;
    }

getBooleanAttributes方法

这是一个本地方法,它主要的作用是可以用来判断 File 对象对应的文件或目录是否存在,判断 File 对象对应的是不是文件,判断 File 对象对应的是不是目录,判断 File 对象是不是隐藏文件或目录。

但这里为什么返回的是一个 int 类型呢?因为这里为了高效利用数据,用位作为不同属性的标识,分别为
0x01、0x02、0x04、0x08,分别代表是否存在、是否为文件、是否为目录和是否为隐藏文件或目录。

    public native int getBooleanAttributes(File f);

本地方法的主要逻辑是先得到文件的路径,再判断路径是否为 windows 保留设备名称,它包括以下这些,

    static WCHAR* ReservedDEviceNames[] = {
        L"CON", L"PRN", L"AUX", L"NUL",
        L"COM1", L"COM2", L"COM3", L"COM4", L"COM5", L"COM6", L"COM7", L"COM8", L"COM9",
        L"LPT1", L"LPT2", L"LPT3", L"LPT4", L"LPT5", L"LPT6", L"LPT7", L"LPT8", L"LPT9",
        L"CLOCK$"
    };

接着通过 GetFileAttributesExW 函数获取文件属性,如果文件属于超链接或者快捷方式,则需要再通过 CreateFileW 函数与 GetFileInformationByHandle 函数组合获取文件信息,其中 CreateFileW 函数要使用 OPEN_EXISTING 表示仅仅打开文件而不是创建。得到文件属性后通过位的或操作来标识是否存在、是否文件、是否目录、是否隐藏。

此外有一个特例也需要处理,就是 pagefile.sys 文件,它比较特殊,主要用于虚拟内存,它是文件而不是目录。

    JNIEXPORT jint JNICALL
    Java_java_io_WinNTFileSystem_getBooleanAttributes(JNIEnv *env, jobject this,
                                                      jobject file)
    {

        jint rv = 0;
        jint pathlen;

    #define SPECIALFILE_NAMELEN 12

        WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
        WIN32_FILE_ATTRIBUTE_DATA wfad;
        if (pathbuf == NULL)
            return rv;
        if (!isReservedDeviceNameW(pathbuf)) {
            if (GetFileAttributesExW(pathbuf, GetFileExInfoStandard, &wfad)) {
                DWORD a = getFinalAttributesIfReparsePoint(pathbuf, wfad.dwFileAttributes);
                if (a != INVALID_FILE_ATTRIBUTES) {
                    rv = (java_io_FileSystem_BA_EXISTS
                        | ((a & FILE_ATTRIBUTE_DIRECTORY)
                            ? java_io_FileSystem_BA_DIRECTORY
                            : java_io_FileSystem_BA_REGULAR)
                        | ((a & FILE_ATTRIBUTE_HIDDEN)
                            ? java_io_FileSystem_BA_HIDDEN : 0));
                }
            } else {
                if (GetLastError() == ERROR_SHARING_VIOLATION) {
                    rv = java_io_FileSystem_BA_EXISTS;
                    if ((pathlen = (jint)wcslen(pathbuf)) >= SPECIALFILE_NAMELEN &&
                        (_wcsicmp(pathbuf + pathlen - SPECIALFILE_NAMELEN,
                                  L"pagefile.sys") == 0) ||
                        (_wcsicmp(pathbuf + pathlen - SPECIALFILE_NAMELEN,
                                  L"hiberfil.sys") == 0))
                      rv |= java_io_FileSystem_BA_REGULAR;
                }
            }
        }
        free(pathbuf);
        return rv;
    }

checkAccess方法

这是一个本地方法,它主要的作用是判断某个文件或目录是否可读、是否可写、是否可执行。这里同样用位标识这些属性,分别用0x01、0x02、0x04表示可执行、可写、可读。

    public native boolean checkAccess(File f, int access);

本地方法的逻辑是先获取文件或目录路径,接着通过 GetFileAttributesW 函数获取文件属性,如果文件属于超链接或者快捷方式,则需要再通过 CreateFileW 函数与 GetFileInformationByHandle 函数组合获取文件信息,其中 CreateFileW 函数要使用 OPEN_EXISTING 表示仅仅打开文件而不是创建。可以看到如果为 INVALID_FILE_ATTRIBUTES 则是获取失败了,此时任何权限都是没有的。获取成功则当判断可读性和可执行性时都返回true,但检查可写时则还要判断是否为目录(目录直接可写),而且还要看文件属性是否为 FILE_ATTRIBUTE_READONLY,只读的话则不可写。

    JNIEXPORT jboolean
    JNICALL Java_java_io_WinNTFileSystem_checkAccess(JNIEnv *env, jobject this,
                                                     jobject file, jint access)
    {
        DWORD attr;
        WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
        if (pathbuf == NULL)
            return JNI_FALSE;
        attr = GetFileAttributesW(pathbuf);
        attr = getFinalAttributesIfReparsePoint(pathbuf, attr);
        free(pathbuf);
        if (attr == INVALID_FILE_ATTRIBUTES)
            return JNI_FALSE;
        switch (access) {
        case java_io_FileSystem_ACCESS_READ:
        case java_io_FileSystem_ACCESS_EXECUTE:
            return JNI_TRUE;
        case java_io_FileSystem_ACCESS_WRITE:
            if ((attr & FILE_ATTRIBUTE_DIRECTORY) ||
                (attr & FILE_ATTRIBUTE_READONLY) == 0)
                return JNI_TRUE;
            else
                return JNI_FALSE;
        default:
            assert(0);
            return JNI_FALSE;
        }
    }

getLastModifiedTime方法

该方法用于获取文件或目录的最后修改时间,本地方法先获取 File 对象的路径,再通过 CreateFileW 函数和 GetFileTime 函数获取最后修改时间,其中 CreateFileW 函数要使用 OPEN_EXISTING 表示仅仅打开文件而不是创建。

    public native long getLastModifiedTime(File f);

    JNIEXPORT jlong JNICALL
    Java_java_io_WinNTFileSystem_getLastModifiedTime(JNIEnv *env, jobject this,
                                                     jobject file)
    {
        jlong rv = 0;
        LARGE_INTEGER modTime;
        FILETIME t;
        HANDLE h;
        WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
        if (pathbuf == NULL)
            return rv;
        h = CreateFileW(pathbuf,
                        0,
                        FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
                        NULL,
                        OPEN_EXISTING,
                        FILE_FLAG_BACKUP_SEMANTICS,
                        NULL);
        if (h != INVALID_HANDLE_VALUE) {
            if (GetFileTime(h, NULL, NULL, &t)) {
                modTime.LowPart = (DWORD) t.dwLowDateTime;
                modTime.HighPart = (LONG) t.dwHighDateTime;
                rv = modTime.QuadPart / 10000;
                rv -= 11644473600000;
            }
            CloseHandle(h);
        }
        free(pathbuf);
        return rv;
    }

getLength方法

该方法御用获取文件或目录的长度。逻辑为,
1. 获取 File 对象的路径。
2. 通过 GetFileAttributesExW 函数获取文件属性,然后通过 nFileSizeHigh 和 nFileSizeLow 得到文件的长度。
3. 如果文件属于超链接或者快捷方式,则需要再通过 CreateFileW 函数与 GetFileInformationByHandle 函数组合获取文件信息,其中 CreateFileW 函数要使用 OPEN_EXISTING 表示仅仅打开文件而不是创建,然后通过 nFileSizeHigh 和 nFileSizeLow 得到文件的长度。
4. 如果另一个进程在使用文件,则不能访问该文件,并报错 ERROR_SHARING_VIOLATION,此时尝试用 _wstati64 函数获取文件的长度。

     public native long getLength(File f);

    JNIEXPORT jlong JNICALL
    Java_java_io_WinNTFileSystem_getLength(JNIEnv *env, jobject this, jobject file)
    {
        jlong rv = 0;
        WIN32_FILE_ATTRIBUTE_DATA wfad;
        WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
        if (pathbuf == NULL)
            return rv;
        if (GetFileAttributesExW(pathbuf,
                                 GetFileExInfoStandard,
                                 &wfad)) {
            if ((wfad.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) {
                rv = wfad.nFileSizeHigh * ((jlong)MAXDWORD + 1) + wfad.nFileSizeLow;
            } else {
                BY_HANDLE_FILE_INFORMATION finfo;
                if (getFileInformation(pathbuf, &finfo)) {
                    rv = finfo.nFileSizeHigh * ((jlong)MAXDWORD + 1) +
                        finfo.nFileSizeLow;
                }
            }
        } else {
            if (GetLastError() == ERROR_SHARING_VIOLATION) {
                struct _stati64 sb;
                if (_wstati64(pathbuf, &sb) == 0) {
                    rv = sb.st_size;
                }
            }
        }
        free(pathbuf);
        return rv;
    }

setPermission方法

该方法主要用于设置 File 对象的访问权限。逻辑如下,
1. 如果设置额权限为 ACCESS_READ 或 ACCESS_EXECUTE,则直接返回传入的参数enable。
2. 获取 File 对象的路径。
3. 通过 GetFileAttributesW 函数获取文件属性。
4. 如果文件属于超链接或者快捷方式,则先获取对应的最终路径,然后再用 GetFileAttributesW 函数获取文件属性。
5. 判断如果不是目录的话则根据传入的参数enable设置属性,并且通过 SetFileAttributesW 函数设置文件的读写权限。

    public native boolean setPermission(File f, int access, boolean enable,
                boolean owneronly);

    JNIEXPORT jboolean JNICALL
    Java_java_io_WinNTFileSystem_setPermission(JNIEnv *env, jobject this,
                                               jobject file,
                                               jint access,
                                               jboolean enable,
                                               jboolean owneronly)
    {
        jboolean rv = JNI_FALSE;
        WCHAR *pathbuf;
        DWORD a;
        if (access == java_io_FileSystem_ACCESS_READ ||
            access == java_io_FileSystem_ACCESS_EXECUTE) {
            return enable;
        }
        pathbuf = fileToNTPath(env, file, ids.path);
        if (pathbuf == NULL)
            return JNI_FALSE;
        a = GetFileAttributesW(pathbuf);

        if ((a != INVALID_FILE_ATTRIBUTES) &&
            ((a & FILE_ATTRIBUTE_REPARSE_POINT) != 0))
        {
            WCHAR *fp = getFinalPath(env, pathbuf);
            if (fp == NULL) {
                a = INVALID_FILE_ATTRIBUTES;
            } else {
                free(pathbuf);
                pathbuf = fp;
                a = GetFileAttributesW(pathbuf);
            }
        }
        if ((a != INVALID_FILE_ATTRIBUTES) &&
            ((a & FILE_ATTRIBUTE_DIRECTORY) == 0))
        {
            if (enable)
                a =  a & ~FILE_ATTRIBUTE_READONLY;
            else
                a =  a | FILE_ATTRIBUTE_READONLY;
            if (SetFileAttributesW(pathbuf, a))
                rv = JNI_TRUE;
        }
        free(pathbuf);
        return rv;
    }
赞(0) 打赏

如未加特殊说明,此网站文章均为原创,转载必须注明出处。Java 技术驿站 » JDK不同操作系统的FileSystem(Windows)中篇
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

关注【Java 技术驿站】公众号,每天早上 8:10 为你推送一篇技术文章

扫描二维码关注我!


关注【Java 技术驿站】公众号 回复 “VIP”,获取 VIP 地址永久关闭弹出窗口

免费获取资源

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏