JDK不同作業系統的FileSystem(unix-like)上篇

超人汪小建發表於2019-02-26

前言

我們知道不同的作業系統有各自的檔案系統,這些檔案系統又存在很多差異,而Java 因為是跨平臺的,所以它必須要統一處理這些不同平臺檔案系統之間的差異,才能往上提供統一的入口。

關於FileSystem類

JDK 裡面抽象出了一個 FileSystem 來表示檔案系統,不同的作業系統通過繼承該類實現各自的檔案系統,比如 Windows NT/2000 作業系統則為 WinNTFileSystem,而 unix-like 作業系統為 UnixFileSystem。

需要注意的一點是,WinNTFileSystem類 和 UnixFileSystem類並不是在同一個 JDK 裡面,也就是說它們是分開的,你只能在 Windows 版本的 JDK 中找到 WinNTFileSystem,而在 unix-like 版本的 JDK 中找到 UnixFileSystem,同樣地,其他作業系統也有自己的檔案系統實現類。

這裡分成兩個系列分析 JDK 對兩種(Windows 和 unix-like )作業系統的檔案系統的實現類,前面已經講了 Windows作業系統,對應為 WinNTFileSystem 類。這裡接著講 unix-like 作業系統,對應為 UnixFileSystem 類。篇幅所限,分為上中下篇,此為上篇。

繼承結構

--java.lang.Object
  --java.io.FileSystem
    --java.io.UnixFileSystem
複製程式碼

類定義

class UnixFileSystem extends FileSystem
複製程式碼

主要屬性

  • slash 表示斜槓符號。
  • colon 表示冒號符號。
  • javaHome 表示Java Home目錄。
  • cache 用於快取標準路徑。
  • javaHomePrefixCache 用於快取標準路徑字首。
    private final char slash;
    private final char colon;
    private final String javaHome;
    private ExpiringCache cache = new ExpiringCache();
    private ExpiringCache javaHomePrefixCache = new ExpiringCache();
複製程式碼

主要方法

構造方法

構造方法很簡答,直接從 System 中獲取到 Properties ,然後再分別根據 file.separator 、 path.separator 和 java.home 獲取對應的屬性值並賦給 UnixFileSystem 物件的屬性。

    public UnixFileSystem() {
        Properties props = GetPropertyAction.privilegedGetProperties();
        slash = props.getProperty("file.separator").charAt(0);
        colon = props.getProperty("path.separator").charAt(0);
        javaHome = props.getProperty("java.home");
    }
複製程式碼

其中的 GetPropertyAction.privilegedGetProperties()其實就是 System.getProperties(),這裡只是將安全管理器相關的處理抽離出來而已。

    public static Properties privilegedGetProperties() {
        if (System.getSecurityManager() == null) {
            return System.getProperties();
        } else {
            return AccessController.doPrivileged(
                    new PrivilegedAction<Properties>() {
                        public Properties run() {
                            return System.getProperties();
                        }
                    }
            );
        }
    }
複製程式碼

normalize方法

該方法主要是對路徑進行標準化, unix-like 的路徑標準化可比 Windows 簡單,不像 Windows 情況複雜且還要呼叫本地方法處理。

有兩個 normalize 方法,第一個 normalize 方法主要是負責檢查路徑是否標準,如果不是標準的則要傳入第二個 normalize 方法進行標準化處理。而判斷路徑是否標準的邏輯主要有兩個,

  1. 路徑中是否有連著2個以上/
  2. 路徑是否以/結尾。
    public String normalize(String pathname) {
        int n = pathname.length();
        char prevChar = 0;
        for (int i = 0; i < n; i++) {
            char c = pathname.charAt(i);
            if ((prevChar == `/`) && (c == `/`))
                return normalize(pathname, n, i - 1);
            prevChar = c;
        }
        if (prevChar == `/`) return normalize(pathname, n, n - 1);
        return pathname;
    }
複製程式碼

進入到路徑標準處理後的邏輯如下,

  1. 長度為0則直接返回傳入的路徑。
  2. 用 while 迴圈從尾部向前搜尋/,主要作用是去掉尾部多餘的斜槓,如果全部都是/(比如///////)則直接返回/
  3. off 變數表示偏移量,這個是由第一個 normalize 方法遍歷得出的,此變數前面的路徑表示符合標準化要求,無需再做標準化處理。直接擷取其前面的字串。
  4. 用 for 迴圈處理剩下的路徑,遇到連著兩個/則直接跳過,這個其實就是隻保留一個/
    private String normalize(String pathname, int len, int off) {
        if (len == 0) return pathname;
        int n = len;
        while ((n > 0) && (pathname.charAt(n - 1) == `/`)) n--;
        if (n == 0) return "/";
        StringBuilder sb = new StringBuilder(pathname.length());
        if (off > 0) sb.append(pathname, 0, off);
        char prevChar = 0;
        for (int i = off; i < n; i++) {
            char c = pathname.charAt(i);
            if ((prevChar == `/`) && (c == `/`)) continue;
            sb.append(c);
            prevChar = c;
        }
        return sb.toString();
    }
複製程式碼

prefixLength方法

該方法用於返回路徑字首長度,對於傳進來的標準路徑,以/開始則返回1,否則返回0。

    public int prefixLength(String pathname) {
        if (pathname.length() == 0) return 0;
        return (pathname.charAt(0) == `/`) ? 1 : 0;
    }
複製程式碼

resolve方法

有兩個 resolve 方法,第一個方法用於合併父路徑和子路徑得到一個新的路徑,邏輯為,

  1. 如果子路徑為空則直接返回父路徑。
  2. 在子路徑以/開頭的情況下,如果父路徑為/則直接返回子路徑,否則則返回父路徑+子路徑。
  3. 如果父路徑為/則返回父路徑+子路徑。
  4. 以上都不是則返回父路徑+/+子路徑。
    public String resolve(String parent, String child) {
        if (child.equals("")) return parent;
        if (child.charAt(0) == `/`) {
            if (parent.equals("/")) return child;
            return parent + child;
        }
        if (parent.equals("/")) return parent + child;
        return parent + `/` + child;
    }
    
    public String resolve(File f) {
        if (isAbsolute(f)) return f.getPath();
        return resolve(System.getProperty("user.dir"), f.getPath());
    }
複製程式碼

第二個 resolve 方法用於相容處理 File 物件,邏輯是,

  1. 如果是絕對路徑則直接返回 File 物件的路徑。
  2. 否則則從 System 中獲取user.dir屬性值作為父路徑,然後 File 物件對應的路徑作為子路徑,再呼叫第一個 resolve 方法合併父路徑和子路徑。

getDefaultParent方法

該方法獲取預設父路徑,直接返回/

    public String getDefaultParent() {
        return "/";
    }
複製程式碼

fromURIPath方法

該方法主要是格式化路徑。主要邏輯是完成類似以下的轉換處理:

  1. /root/ –> /root
  2. 但是 / –> /,這是通過長度來限制的,即當長度超過1時才會去掉尾部的 /
    public String fromURIPath(String path) {
        String p = path;
        if (p.endsWith("/") && (p.length() > 1)) {
            p = p.substring(0, p.length() - 1);
        }
        return p;
    }
複製程式碼

isAbsolute方法

該方法判斷 File 物件是否為絕對路徑,直接根據 File 類的 getPrefixLength 方法獲取字首長度是否為0作為判斷條件,該方法最終就是呼叫該類的 prefixLength 方法,有字首就說明是絕對路徑。

    public boolean isAbsolute(File f) {
        return (f.getPrefixLength() != 0);
    }
複製程式碼

canonicalize方法

該方法用來標準化某路徑,標準路徑不僅是一個絕對路徑而且還是唯一的路徑,而且標準的定義是依賴於作業系統的。比較典型的就是處理包含”.”或”..”的路徑,還有符號連結等。下面看 unix-like 作業系統如何標準化路徑:

  1. 如果不使用快取則直接呼叫 canonicalize0 本地方法獲取標準化路徑。
  2. 如果使用了快取則在快取中查詢,存在則直接返回,否則先呼叫 canonicalize0 本地方法獲取標準化路徑,再將路徑放進快取中。
  3. 另外,還提供了字首快取可以使用,它快取了標準路徑的父目錄,這樣就可以節省了字首部分的處理,字首快取的邏輯也是第一次標準化後將其快取起來,下次則可從字首快取中查詢。
  4. 使用字首快取這裡有一個條件,就是必須是在Java Home目錄下的檔案才能被快取,否則不予許。字首快取的使用節省了一些工作,提高效率。
    public String canonicalize(String path) throws IOException {
        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 = javaHomePrefixCache.get(dir);
                        if (resDir != null) {
                            String filename = path.substring(1 + dir.length());
                            res = resDir + slash + filename;
                            cache.put(dir + slash + filename, res);
                        }
                    }
                }
                if (res == null) {
                    res = canonicalize0(path);
                    cache.put(path, res);
                    if (useCanonPrefixCache &&
                        dir != null && dir.startsWith(javaHome)) {
                        resDir = parentOrNull(res);
                        if (resDir != null && resDir.equals(dir)) {
                            File f = new File(res);
                            if (f.exists() && !f.isDirectory()) {
                                javaHomePrefixCache.put(dir, resDir);
                            }
                        }
                    }
                }
            }
            return res;
        }
    }
    
    private native String canonicalize0(String path) throws IOException;
複製程式碼

本地方法 canonicalize0 如下,處理邏輯通過 canonicalize 函式實現,由於函式較長,這裡不再貼出來,主要的處理邏輯:

  1. 路徑長度不能超過 1024。
  2. 嘗試用 realpath 函式將路徑轉成絕對路徑,該函式主要用於擴充套件符號連線、解決/./ /../符號的表示、多餘的/符號。但有時對於一些特殊的非正常寫法可能導致無法通過 realpath 函式處理掉,比如.......,所以接著還得再判斷是否需要進一步處理,需要則繼續處理,否則直接返回路徑。
  3. 如果 realpath 函式處理失敗了則說明原路徑有問題,這時需要不斷嘗試去掉尾部元素,然後繼續用 realpath 函式處理擷取後的路徑,子路徑也可能處理失敗,原因有,
    ① 子路徑檔案不存在。
    ② 作業系統拒絕訪問。
    ③ I/O問題也可能導致失敗。
    子路徑如果處理成功則直接將尾部元素新增到子路徑中得到最終的標準路徑,最後將.......情況處理掉並返回標準路徑。
JNIEXPORT jstring JNICALL
Java_java_io_UnixFileSystem_canonicalize0(JNIEnv *env, jobject this,
                                          jstring pathname)
{
    jstring rv = NULL;

    WITH_PLATFORM_STRING(env, pathname, path) {
        char canonicalPath[JVM_MAXPATHLEN];
        if (canonicalize((char *)path,
                         canonicalPath, JVM_MAXPATHLEN) < 0) {
            JNU_ThrowIOExceptionWithLastError(env, "Bad pathname");
        } else {
#ifdef MACOSX
            rv = newStringPlatform(env, canonicalPath);
#else
            rv = JNU_NewStringPlatform(env, canonicalPath);
#endif
        }
    } END_PLATFORM_STRING(env, path);
    return rv;
}
複製程式碼

以下是***廣告***和***相關閱讀***

=============廣告時間===============

公眾號的選單已分為“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。

鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以購買。感謝各位朋友。

為什麼寫《Tomcat核心設計剖析》

=========================

相關閱讀:

JDK不同作業系統的FileSystem(Windows)上篇

JDK不同作業系統的FileSystem(Windows)中篇

JDK不同作業系統的FileSystem(Windows)下篇

歡迎關注:

這裡寫圖片描述

相關文章