JDK不同作業系統的FileSystem(Windows)上篇
前言
我們知道不同的作業系統有各自的檔案系統,這些檔案系統又存在很多差異,而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);
}
以下是廣告
========廣告時間========
公眾號的選單已分為“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。
鄙人的新書《Tomcat核心設計剖析》已經在京東銷售了,有需要的朋友可以購買。感謝各位朋友。
=========================
歡迎關注: