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

createFileExclusively方法

该方法用于创建文件,本地方法逻辑是,
1. 将路径转成宽字符形式。
2. 判断路径是否为系统保留设备名。
3. 调用 CreateFileW 函数创建文件,使用了 CREATE_NEW 模式,仅仅在不存在该文件时才创建。
4. 如果已经存在该文件,尽量不抛出异常,而是返回 false,此过程还会尝试读取该文件的属性,失败则抛IO异常。

    public native boolean createFileExclusively(String path)
                throws IOException;

    JNIEXPORT jboolean JNICALL
    Java_java_io_WinNTFileSystem_createFileExclusively(JNIEnv *env, jclass cls,
                                                       jstring path)
    {
        HANDLE h = NULL;
        WCHAR *pathbuf = pathToNTPath(env, path, JNI_FALSE);
        if (pathbuf == NULL)
            return JNI_FALSE;
        if (isReservedDeviceNameW(pathbuf)) {
            free(pathbuf);
            return JNI_FALSE;
        }
        h = CreateFileW(
            pathbuf,                             
            GENERIC_READ | GENERIC_WRITE,        
            FILE_SHARE_READ | FILE_SHARE_WRITE,   
            NULL,                                 
            CREATE_NEW,                           
            FILE_ATTRIBUTE_NORMAL |
                FILE_FLAG_OPEN_REPARSE_POINT,    
            NULL);

        if (h == INVALID_HANDLE_VALUE) {
            DWORD error = GetLastError();
            if ((error != ERROR_FILE_EXISTS) && (error != ERROR_ALREADY_EXISTS)) {
                DWORD a = GetFileAttributesW(pathbuf);
                if (a == INVALID_FILE_ATTRIBUTES) {
                    SetLastError(error);
                    JNU_ThrowIOExceptionWithLastError(env, "Could not open file");
                }
            }
            free(pathbuf);
            return JNI_FALSE;
        }
        free(pathbuf);
        CloseHandle(h);
        return JNI_TRUE;
    }

list方法

该方法用于列出指定目录下的所有文件和目录,本地方法处理逻辑如下,
1. 获取 java/lang/String类对象,并检查不能为NULL。
2. 获取 File 对象对应的路径。
3. 按照路径长度重新分配空间并将路径拷贝到 search_path。
4. 通过 GetFileAttributesW 函数获取指定路径的文件属性,如果得到 INVALID_FILE_ATTRIBUTES 或如果为目录则直接返回NULL。
5. 去除尾部多余的空格。
6. 在路径的尾部添加*\*
7. 通过 FindFirstFileW 函数获取到第一个文件。
8. 接着通过 while 循环和 FindNextFileW 函数不断获取下一个文件,并将得到的文件名放到字符串数组中,而且文件名不能为...
9. 返回文件名数组,其中可以看到文件名数组的初始长度为16,如果超过该长度后则按照原来长度的两倍重新创建字符串数组对象,再将原数组复制到新数组中。

    public native String[] list(File f);

    JNIEXPORT jobjectArray JNICALL
    Java_java_io_WinNTFileSystem_list(JNIEnv *env, jobject this, jobject file)
    {
        WCHAR *search_path;
        HANDLE handle;
        WIN32_FIND_DATAW find_data;
        int len, maxlen;
        jobjectArray rv, old;
        DWORD fattr;
        jstring name;
        jclass str_class;
        WCHAR *pathbuf;

        str_class = JNU_ClassString(env);
        CHECK_NULL_RETURN(str_class, NULL);

        pathbuf = fileToNTPath(env, file, ids.path);
        if (pathbuf == NULL)
            return NULL;
        search_path = (WCHAR*)malloc(2*wcslen(pathbuf) + 6);
        if (search_path == 0) {
            free (pathbuf);
            errno = ENOMEM;
            JNU_ThrowOutOfMemoryError(env, "native memory allocation failed");
            return NULL;
        }
        wcscpy(search_path, pathbuf);
        free(pathbuf);
        fattr = GetFileAttributesW(search_path);
        if (fattr == INVALID_FILE_ATTRIBUTES) {
            free(search_path);
            return NULL;
        } else if ((fattr & FILE_ATTRIBUTE_DIRECTORY) == 0) {
            free(search_path);
            return NULL;
        }

        len = (int)wcslen(search_path);
        while (search_path[len-1] == L' ') {
            len--;
        }
        search_path[len] = 0;

        if ((search_path[0] == L'\\' && search_path[1] == L'\0') ||
            (search_path[1] == L':'
            && (search_path[2] == L'\0'
            || (search_path[2] == L'\\' && search_path[3] == L'\0')))) {
            wcscat(search_path, L"*");
        } else {
            wcscat(search_path, L"\\*");
        }

        handle = FindFirstFileW(search_path, &find_data);
        free(search_path);
        if (handle == INVALID_HANDLE_VALUE) {
            if (GetLastError() != ERROR_FILE_NOT_FOUND) {
                return NULL;
            } else {
                rv = (*env)->NewObjectArray(env, 0, str_class, NULL);
                return rv;
            }
        }

        len = 0;
        maxlen = 16;
        rv = (*env)->NewObjectArray(env, maxlen, str_class, NULL);
        if (rv == NULL)
            return NULL;
        do {
            if (!wcscmp(find_data.cFileName, L".")
                                    || !wcscmp(find_data.cFileName, L".."))
               continue;
            name = (*env)->NewString(env, find_data.cFileName,
                                     (jsize)wcslen(find_data.cFileName));
            if (name == NULL)
                return NULL;
            if (len == maxlen) {
                old = rv;
                rv = (*env)->NewObjectArray(env, maxlen <<= 1, str_class, NULL);
                if (rv == NULL || JNU_CopyObjectArray(env, rv, old, len) < 0)
                    return NULL;
                (*env)->DeleteLocalRef(env, old);
            }
            (*env)->SetObjectArrayElement(env, rv, len++, name);
            (*env)->DeleteLocalRef(env, name);

        } while (FindNextFileW(handle, &find_data));

        if (GetLastError() != ERROR_NO_MORE_FILES)
            return NULL;
        FindClose(handle);

        if (len < maxlen) {
            old = rv;
            rv = (*env)->NewObjectArray(env, len, str_class, NULL);
            if (rv == NULL)
                return NULL;
            if (JNU_CopyObjectArray(env, rv, old, len) < 0)
                return NULL;
        }
        return rv;
    }

createDirectory方法

该方法用来创建目录,本地方法很简单,就是获取 File 对象对应的路径,再调用 CreateDirectoryW 函数创建目录。

    public native boolean createDirectory(File f);

    JNIEXPORT jboolean JNICALL
    Java_java_io_WinNTFileSystem_createDirectory(JNIEnv *env, jobject this,
                                                 jobject file)
    {
        BOOL h = FALSE;
        WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
        if (pathbuf == NULL) {
            return JNI_FALSE;
        }
        h = CreateDirectoryW(pathbuf, NULL);
        free(pathbuf);

        if (h == 0) {
            return JNI_FALSE;
        }

        return JNI_TRUE;
    }

setLastModifiedTime方法

该方法用来设置文件或目录的最后修改时间,本地方法是先获取 File 对象对应的路径,再用 CreateFileW 函数打开指定文件或目录,最后用 SetFileTime 函数设置最后修改时间。

    public native boolean setLastModifiedTime(File f, long time);

    JNIEXPORT jboolean JNICALL
    Java_java_io_WinNTFileSystem_setLastModifiedTime(JNIEnv *env, jobject this,
                                                     jobject file, jlong time)
    {
        jboolean rv = JNI_FALSE;
        WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
        HANDLE h;
        if (pathbuf == NULL)
            return JNI_FALSE;
        h = CreateFileW(pathbuf,
                        FILE_WRITE_ATTRIBUTES,
                        FILE_SHARE_READ | FILE_SHARE_WRITE,
                        NULL,
                        OPEN_EXISTING,
                        FILE_FLAG_BACKUP_SEMANTICS,
                        0);
        if (h != INVALID_HANDLE_VALUE) {
            LARGE_INTEGER modTime;
            FILETIME t;
            modTime.QuadPart = (time + 11644473600000L) * 10000L;
            t.dwLowDateTime = (DWORD)modTime.LowPart;
            t.dwHighDateTime = (DWORD)modTime.HighPart;
            if (SetFileTime(h, NULL, NULL, &t)) {
                rv = JNI_TRUE;
            }
            CloseHandle(h);
        }
        free(pathbuf);

        return rv;
    }

setReadOnly方法

该方法用于将指定文件设置成只读。本地方法逻辑为,
1. 获取 File 对象对应的路径。
2. 通过 GetFileAttributesW 函数获取文件属性。
3. 如果文件属于超链接或者快捷方式,则先获取对应的最终路径,然后再用 GetFileAttributesW 函数获取文件属性。
4. 判断不为目录的话则通过 SetFileAttributesW 函数设置文件为只读,对应标识为 FILE_ATTRIBUTE_READONLY。

    public native boolean setReadOnly(File f);

    JNIEXPORT jboolean JNICALL
    Java_java_io_WinNTFileSystem_setReadOnly(JNIEnv *env, jobject this,
                                             jobject file)
    {
        jboolean rv = JNI_FALSE;
        DWORD a;
        WCHAR *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 (SetFileAttributesW(pathbuf, a | FILE_ATTRIBUTE_READONLY))
                rv = JNI_TRUE;
        }
        free(pathbuf);
        return rv;
    }

delete方法

该方法用于删除 File 对象指定路径,需要将标准路径缓存和标准路径前缀缓存都清掉,然后调用本地方法 delete0 执行删除操作。

    public boolean delete(File f) {
            cache.clear();
            prefixCache.clear();
            return delete0(f);
        }

    private native boolean delete0(File f);

本地方法先获取 File 对象对应的路径,然后再调用 removeFileOrDirectory 函数删除目录或文件。而 removeFileOrDirectory 函数的逻辑是先将指定路径文件或目录设置成 FILE_ATTRIBUTE_NORMAL,然后再用 GetFileAttributesW 函数获取文件属性,最后如果指定路径为目录则调用 RemoveDirectoryW 函数删除目录,如果是文件则调用 DeleteFileW 函数删除文件。

    JNIEXPORT jboolean JNICALL
    Java_java_io_WinNTFileSystem_delete0(JNIEnv *env, jobject this, jobject file)
    {
        jboolean rv = JNI_FALSE;
        WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
        if (pathbuf == NULL) {
            return JNI_FALSE;
        }
        if (removeFileOrDirectory(pathbuf) == 0) {
            rv = JNI_TRUE;
        }
        free(pathbuf);
        return rv;
    }

    static int
    removeFileOrDirectory(const jchar *path)
    {
        DWORD a;

        SetFileAttributesW(path, FILE_ATTRIBUTE_NORMAL);
        a = GetFileAttributesW(path);
        if (a == INVALID_FILE_ATTRIBUTES) {
            return 1;
        } else if (a & FILE_ATTRIBUTE_DIRECTORY) {
            return !RemoveDirectoryW(path);
        } else {
            return !DeleteFileW(path);
        }
    }

rename方法

该方法用于重命名文件,需要将标准路径缓存和标准路径前缀缓存都清掉,然后调用本地方法 rename0 执行重命名操作。

    public boolean rename(File f1, File f2) {
            cache.clear();
            prefixCache.clear();
            return rename0(f1, f2);
        }

    private native boolean rename0(File f1, File f2);

本地方法分别先获取原来的文件路径和重命名的文件路径,再通过 _wrename 函数进行重命名操作。

    JNIEXPORT jboolean JNICALL
    Java_java_io_WinNTFileSystem_rename0(JNIEnv *env, jobject this, jobject from,
                                         jobject to)
    {

        jboolean rv = JNI_FALSE;
        WCHAR *frompath = fileToNTPath(env, from, ids.path);
        WCHAR *topath = fileToNTPath(env, to, ids.path);
        if (frompath != NULL && topath != NULL && _wrename(frompath, topath) == 0) {
            rv = JNI_TRUE;
        }
        free(frompath);
        free(topath);
        return rv;
    }

access方法

该方法用于检查指定路径文件或目录是否可读,这里主要是JVM层的权限检查,所以用的是 SecurityManager 安全管理器来检测。

    private boolean access(String path) {
            try {
                SecurityManager security = System.getSecurityManager();
                if (security != null) security.checkRead(path);
                return true;
            } catch (SecurityException x) {
                return false;
            }
        }

listRoots方法

该方法用于获取可用的文件系统的根文件对象的数组。逻辑如下,
1. 先通过 listRoots0 本地方法获取所有根文件。
2. listRoots0 方法很简单,就是直接用 GetLogicalDrives 函数获取到操作系统的逻辑驱动器字符。需要注意的是 GetLogicalDrives 返回的是一个 int 类型,那么它是怎么表示驱动器字符的呢?其实也是通过位来标识,每一位对应表示一个逻辑驱动器是否存在,比如第一位如果是”1”则表示驱动器”A:”存在, 第二位如果是“1”则表示驱动器“B:”存在,以此类推。
3. 得到所有驱动器字符后通过一个for循环遍历检测驱动器的访问权限,去掉无权限的驱动器,并统计一个有n个驱动器,这里只需要循环26次,因为最多就是26个大写字母。
4. 实例化一个 File 数组,大小为n。
5. 再次通过一个26次的循环遍历得到有权限的驱动器,根据此驱动器符号实例化一个 File 对象,添加到 File 数组中。
6. 返回 File 数组。

    public File[] listRoots() {
            int ds = listRoots0();
            int n = 0;
            for (int i = 0; i < 26; i++) {
                if (((ds >> i) & 1) != 0) {
                    if (!access((char)('A' + i) + ":" + slash))
                        ds &= ~(1 << i);
                    else
                        n++;
                }
            }
            File[] fs = new File[n];
            int j = 0;
            char slash = this.slash;
            for (int i = 0; i < 26; i++) {
                if (((ds >> i) & 1) != 0)
                    fs[j++] = new File((char)('A' + i) + ":" + slash);
            }
            return fs;
        }

    private static native int listRoots0();

    JNIEXPORT jint JNICALL
    Java_java_io_WinNTFileSystem_listRoots0(JNIEnv *env, jclass ignored)
    {
        return GetLogicalDrives();
    }

getSpace方法

该方法用于获取文件空间大小,包括总空间大小、剩余空间大小和可用空间大小,Java 层分别用 SPACE_TOTAL = 0 SPACE_FREE = 1 SPACE_USABLE = 2标识。要查询某个文件的根目录的某某空间大小则将对应的标识传入,通过 getSpace0 本地方法获得。

    public long getSpace(File f, int t) {
            if (f.exists()) {
                return getSpace0(f, t);
            }
            return 0;
        }

    private native long getSpace0(File f, int t);

本地方法的逻辑是,
1. 获取 File 对象对应的文件或目录路径。
2. 通过 GetVolumePathNameW 函数获取指定路径对应的根路径。
3. 通过 GetDiskFreeSpaceExW 函数将总空间大小、空闲空间大小和可用空间大小获取到。
4. 根据传入的标识返回不同的指标,比如 SPACE_TOTAL则返回总空间大小,其他类似。

    JNIEXPORT jlong JNICALL
    Java_java_io_WinNTFileSystem_getSpace0(JNIEnv *env, jobject this,
                                           jobject file, jint t)
    {
        WCHAR volname[MAX_PATH_LENGTH + 1];
        jlong rv = 0L;
        WCHAR *pathbuf = fileToNTPath(env, file, ids.path);

        if (GetVolumePathNameW(pathbuf, volname, MAX_PATH_LENGTH)) {
            ULARGE_INTEGER totalSpace, freeSpace, usableSpace;
            if (GetDiskFreeSpaceExW(volname, &usableSpace, &totalSpace, &freeSpace)) {
                switch(t) {
                case java_io_FileSystem_SPACE_TOTAL:
                    rv = long_to_jlong(totalSpace.QuadPart);
                    break;
                case java_io_FileSystem_SPACE_FREE:
                    rv = long_to_jlong(freeSpace.QuadPart);
                    break;
                case java_io_FileSystem_SPACE_USABLE:
                    rv = long_to_jlong(usableSpace.QuadPart);
                    break;
                default:
                    assert(0);
                }
            }
        }

        free(pathbuf);
        return rv;
    }

getNameMax方法

该方法用于获取系统允许的最大文件名长度。在调用 getNameMax0 本地方法前会先做一些处理,如果路径时绝对路径,则获取根路径并加上 \\

    public int getNameMax(String path) {
            String s = null;
            if (path != null) {
                File f = new File(path);
                if (f.isAbsolute()) {
                    Path root = f.toPath().getRoot();
                    if (root != null) {
                        s = root.toString();
                        if (!s.endsWith("\\")) {
                            s = s + "\\";
                        }
                    }
                }
            }
            return getNameMax0(s);
        }

    private native int getNameMax0(String path);

本地方法中通过 GetVolumeInformationW 函数得到系统允许的最大文件名长度 maxComponentLength

    JNIEXPORT jint JNICALL
    Java_java_io_WinNTFileSystem_getNameMax0(JNIEnv *env, jobject this,
                                             jstring pathname)
    {
        BOOL res = 0;
        DWORD maxComponentLength;

        if (pathname == NULL) {
                res = GetVolumeInformationW(NULL,
                                            NULL,
                                            0,
                                            NULL,
                                            &maxComponentLength,
                                            NULL,
                                            NULL,
                                            0);
        } else {
            WITH_UNICODE_STRING(env, pathname, path) {
                res = GetVolumeInformationW(path,
                                            NULL,
                                            0,
                                            NULL,
                                            &maxComponentLength,
                                            NULL,
                                            NULL,
                                            0);
            } END_UNICODE_STRING(env, path);
        }

        if (res == 0) {
            JNU_ThrowIOExceptionWithLastError(env,
                "Could not get maximum component length");
        }

        return (jint)maxComponentLength;
    }

compare方法

该方法用于比较两个 File 对象,其实就是直接比较路径字符串。

    public int compare(File f1, File f2) {
            return f1.getPath().compareToIgnoreCase(f2.getPath());
        }

hashCode方法

该方法用于获取 File 对象的哈希值,获取 File对象路径,再将字符串变成小写,再调用字符串的 hashCode 方法,最后与 1234321 进行异或运算,得到的值即为该文件的哈希值。

    public int hashCode(File f) {
            return f.getPath().toLowerCase(Locale.ENGLISH).hashCode() ^ 1234321;
        }
赞(0) 打赏

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

评论 抢沙发

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

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

扫描二维码关注我!


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

免费获取资源

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

支付宝扫一扫打赏

微信扫一扫打赏