讀古今文學網 > Java程序員修煉之道 > 2.4 NIO.2的文件系統I/O >

2.4 NIO.2的文件系統I/O

對於文件系統的操作任務,比如移動文件、修改文件屬性,以及處理文件內容等,在NIO.2中都有所改善。對這些操作的支持主要是由Files類提供的。

表2-2中有關於Files類的詳細介紹,此外本節還會介紹另外一個也很重要的類:WatchService

表2-2 文件處理的基礎類

類 說明 Files 讓你輕鬆複製、移動、刪除或處理文件的工具類,有你需要的所有方法 WatchService用來監視文件或目錄的核心類,不管它們有沒有變化

在本節中,你將學會如何在文件和文件系統上執行下面這些任務:

  • 創建和刪除文件;
  • 移動、複製、重命名和刪除文件;
  • 文件屬性的讀寫;
  • 文件內容的讀取和寫入;
  • 處理符號鏈接;
  • WatchService發出文件修改通知;
  • 使用SeekableByteChannel——一個可以指定位置及大小的增強型字節通道。

這看起來可能挺恐怖的,但由於設計巧妙,API提供了很多輔助方法,把抽像層隱藏了起來,讓你可以輕鬆快捷地處理文件系統。

警告 NIO.2 API對原子操作的支持有很大改進,但涉及文件系統處理時,仍然主要依靠代碼來提供保護。即使是執行了一半的操作,也很可能會因為突然斷網、咖啡潑到服務器上,或某個冒失鬼在錯誤的UNIX機器上執行了shutdown now命令(本書作者之一親身經歷的著名事件)等諸多原因而出錯。儘管API的某些方法還是會偶爾拋出個RuntimeException,但某些異常狀況可以由Files.exists(Path)這樣的輔助方法來緩解。

學習新API最好的辦法就是讀寫代碼。接下來我們來看一些實際案例,先從基本的文件創建和刪除開始。

2.4.1 創建和刪除文件

只需要調用Files類裡的輔助方法,就可以很容易地創建和刪除文件。當然,你接到的任務不可能總像默認情況那麼簡單,所以我們額外加了一些選項,比如在新創建的文件上設定可讀/可寫/可執行的安全訪問權限。

提示 如果你要在自己的機器上運行本節中的代碼,請用實際路徑替換掉代碼中的路徑。

下面的代碼展示了基本的文件創建操作,用到了Files.createFile(Path target)方法。如果你的操作系統裡有個D:\Backup目錄,運行代碼之後就會在那裡創建一個MyStuff.txt文件。

Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Path file = Files.createFile(target);
  

通常出於安全考慮,要定義所創建的文件是用於讀、寫、執行,或三者權限的某種組合時,你要指明該文件的某些FileAttributes。因為這取決於文件系統,所以需要使用與文件系統相關的文件權限類。

下面是一個在POSIX文件系統1上為屬主、屬主組內用戶和所有用戶設置讀/寫許可的例子。這種方法允許所有用戶對即將創建的文件D:\Backup\MyStuff.txt進行讀寫操作。

1 可移植操作系統接口(UNIX),是一種許多操作系統都支持的基本標準。

Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Set<PosixFilePermission> perms =
    PosixFilePermissions.fromString("rw-rw-rw-");
FileAttribute<Set<PosixFilePermission>> attr =
    PosixFilePermissions.asFileAttribute(perms);
Files.createFile(target, attr);
  

java.nio.file.attribute包裡有一大串已經寫好的*FilePermission類。對文件屬性的支持在2.4.3節中還有更詳細的論述。

警告 如果在創建文件時要指定訪問許可,不要忽略其父目錄強加給該文件的umask限制或受限許可。比如說,你會發現即便你為新文件指定了rw-rw-rw許可,但由於目錄的掩碼,實際上文件最終的訪問許可卻是rw-r--r--

刪除文件要簡單一些,可以用Files.delete(Path)方法。下面的代碼刪除了剛剛創建的D:\Backup\MyStuff.txt文件。當然,運行這個Java程序的用戶需要有刪除文件的權限。

Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Files.delete(target);
  

接下來你將學到如何在文件系統中複製和移動文件。

2.4.2 文件的複製和移動

使用Files類中簡單的輔助方法可以很輕鬆地完成文件的複製和移動。

下面的代碼演示了如何用Files.copy(Path source, Path target)方法完成基本的複製操作。

Path source = Paths.get("C:\\My Documents\\Stuff.txt");
Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Files.copy(source, target);
  

複製文件時通常需要設置某些選項。下面這個例子用到了覆蓋即替換已有文件的選項。

import static java.nio.file.StandardCopyOption.*;

Path source = Paths.get("C:\\My Documents\\Stuff.txt");
Path target = Paths.get("D:\\Backup\\MyStuff.txt");
Files.copy(source, target, REPLACE_EXISTING);
  

其他的複製選項包括COPY_ATTRIBUTES(複製文件屬性)和ATOMIC_MOVE(確保在兩邊的操作都成功,否則回滾)。

移動和複製很像,都是用原子Files.move(Path source, Path target)方法完成的。通常在移動文件時,你想要用複製選項,此時便可以用Files.move(Path source, Path target, CopyOptions...)方法,但要注意變參的使用。

在下面這個例子中,我們要在移動源文件時保留其屬性,並且覆蓋目標文件(如果存在的話)。

import static java.nio.file.StandardCopyOption.*;

Path source = Paths.get("C:\\My Documents\\Stuff.txt");
Path target = Paths.get("D:\\Backup\\MyStuff.txt");

Files.move(source, target, REPLACE_EXISTING, COPY_ATTRIBUTES);
  

現在你已經能創建、刪除、複製和移動文件了,下面該認真研究一下Java 7對文件屬性的支持了。

2.4.3 文件的屬性

文件屬性控制著誰能對文件做什麼。一般情況下,做什麼許可包括能否讀取、寫入或執行文件,而由誰許可包括屬主、群組或所有人。

本節從討論文件的基本屬性組開始,比如文件最後訪問時間以及它是目錄還是符號鏈接等。本節的第二部分討論對特定文件系統的文件屬性的支持,因為不同的文件系統都有它們自己的屬性集和屬性含義的解釋,所以這部分比較難。

讓我們先從瞭解Java 7 對基本文件屬性的支持開始吧。

1. 基本文件屬性支持

真正通用的文件屬性並不多,但確實有一組大多數文件系統都支持的屬性。接口BasicFileAttributes定義了這個通用集,但實際上工具類Files就可以回答與文件相關的各種問題,比如下面這些:

  • 最後修改時間是什麼時候?
  • 它有多大?
  • 它是符號連接嗎?
  • 它是目錄嗎?

代碼清單2-4說明了Files類中用於收集這些基本文件屬性的方法。代碼輸出了/usr/bin/zip的相關信息,你看到的輸出應該和下面的類似:

/usr/bin/zip
2011-07-20T16:50:18Z
351872
false
false
{lastModifiedTime=2011-07-20T16:50:18Z,
fileKey=(dev=e000002,ino=30871217), isDirectory=false,
lastAccessTime=2011-06-13T23:31:11Z, isOther=false,
isSymbolicLink=false, isRegularFile=true,
creationTime=2011-07-20T16:50:18Z, size=351872}
  

注意,所有這些屬性都是調用Files.readAttributes(Path path, Stringattributes, LinkOption... options)得到的。代碼清單2-4如下所示:

代碼清單2-4 通用的文件屬性

try
{
  Path zip = Paths.get("/usr/bin/zip");//獲取Path
  /**輸出屬性*/
  System.out.println(Files.getLastModifiedTime(zip));
  System.out.println(Files.size(zip));
  System.out.println(Files.isSymbolicLink(zip));
  System.out.println(Files.isDirectory(zip));
  System.out.println(Files.readAttributes(zip, "*"));//執行批量讀取
}
catch (IOException ex)
{
  System.out.println("Exception [" + ex.getMessage + "]");
}
  

還有一些可以從Files類的方法中採集到的通用文件屬性信息。這樣的信息包括文件屬主,是否為符號鏈接等。請參照Files類的Javadoc查看完整的輔助方法列表。

Java 7也支持跨文件系統的文件屬性查看和處理功能。

2. 特定文件屬性支持

在2.4.1節創建文件時你已經見過FileAttribute接口和PosixFilePermissions類了。為了支持文件系統特定的文件屬性,Java 7允許文件系統提供者實現FileAttributeViewBasicFileAttributes接口。

警告 我們之前已經說過了,但有必要再重複一次。在編寫特定文件系統的代碼時一定要小心。一定要確保你的邏輯和異常處理考慮到了代碼在不同文件系統上運行的情況。

來看一個例子,其中你想用Java 7保證正確的訪問許可被設置在特定文件中。圖2-2顯示了Admin用戶的home目錄的列表。注意那個特殊的.profile隱藏文件,它只允許Admin用戶寫,其他任何人都沒有寫權限,但所有人都可以讀取該文件。

圖2-2 Admin用戶的home目錄列表,顯示.profile的訪問許可

在下面的代碼中,你要確保.profile文件的訪問許可設置正確,與圖2-2對應。Admin用戶希望其他所有用戶都可以讀取該文件,但只有他自己來寫。你可以用特定的POSIX PosixFilePermissionPosixFileAttributes類來保證訪問許可(rw-r--r--)是正確的。

代碼清單2-5 Java 7對文件屬性的支持

import static java.nio.file.attribute.PosixFilePermission.*;
try
{
  Path profile = Paths.get("/user/Admin/.profile");

  PosixFileAttributes attrs =
      Files.readAttributes(profile,
                           PosixFileAttributes.class);//1獲取屬性視圖
  Set<PosixFilePermission> posixPermissions =
                               attrs.permissions;//2讀取訪問許可
  posixPermissions.clear; //3清除訪問許可
  /**日誌信息*/
  String owner = attrs.owner.getName;
  String perms =
      PosixFilePermissions.toString(posixPermissions);
  System.out.format("%s %s%n", owner, perms); 
  /**4設置新的訪問許可*/
  posixPermissions.add(OWNER_READ);
  posixPermissions.add(GROUP_READ);
  posixPermissions.add(OTHER_READ);
  posixPermissions.add(OWNER_WRITE);
  Files.setPosixFilePermissions(profile, posixPermissions);
}
catch(IOException e)
{
  System.out.println(e.getMessage);
}
  

代碼從導入PosixFilePermission常量還有其他未顯示的導入開始,然後得到.profile文件的PathFiles類中有個輔助方法讓你可以讀取特定文件系統的屬性,在這個例子中是PosixFileAttributes1。然後你就可以訪問PosixFilePermission2。在清除了已有的許可之後3,你可以為文件添加新的訪問許可,當然還是用Files中的方法4。

你可能已經注意到了,PosixFilePermission是一個enum,因此沒有實現FileAttributeView接口。為什麼這裡沒用PosixFileAttributeView呢?實際上是Files輔助類把它隱藏了起來,這樣你就可以用readAttributes方法直接讀取文件屬性了,也可以用setPosixFilePermissions方法直接設置訪問許可。

除了基本屬性,Java 7還有一個用來支持特別操作系統特性的擴展系統。可惜,我們不可能囊括所有特殊情況,但我們會給你看一個擴展系統的例子:Java 7對符號鏈接的支持。

3. 符號鏈接

你可以把符號鏈接看做指向另一個文件或目錄的入口,並且在大多數情況下它們都是透明的。比如切換到符號鏈接的目錄下會把你帶到符號鏈接所指向的目錄下。但在寫軟件時,比如備份工具或部署腳本,你需要慎重考慮是否應該跟隨符號鏈接,NIO.2允許你做出選擇。

我們再用一下2.2.3節的例子。你要在*nix系統上查詢/usr/logs目錄下的日誌文件log1.txt的信息。但/usr/logs目錄實際上是一個指向/application/logs目錄的符號鏈接(指針),/application/logs目錄才是日誌文件的真正位置。

符號鏈接在宿主操作系統中使用,包括(但不限於)UNIX、Linux、Windows 7和Mac OS X。Java 7對符號鏈接的支持遵循UNIX操作系統中實現的語義。

下面的代碼在讀取基本文件屬性之前先檢查指向安裝Java的 /opt/platform目錄的Path,看它是否為符號鏈接,我們想讀取文件真正位置的屬性。代碼清單2-6如下所示:

代碼清單2-6 探索符號鏈接

Path file = Paths.get("/opt/platform/java");
try
{
  if(Files.isSymbolicLink(file)) //1 檢查符號鏈接
  {
    file = Files.readSymbolicLink(file); //2讀取符號鏈接
  }
  Files.readAttributes(file, BasicFileAttributes.class);//3 讀取文件屬性
}
catch (IOException e)
{
  System.out.println(e.getMessage);
}
  

Files類提供了一個isSymbolicLink(Path)方法來檢查符號鏈接1。它還有一個輔助方法,可以用於返回符號鏈接目標的真實Path2,所以你能讀到正確的文件屬性3。

NIO.2 API默認會跟隨符號鏈接。如果不想跟隨,需要用LinkOption.NOFOLLOW_LINKS選項。這一選項可以用在幾個方法調用上。如果你要讀取符號鏈接本身的基本文件屬性,應該調用:

Files.readAttributes(target,
                     BasicFileAttributes.class,
                     LinkOption.NOFOLLOW_LINKS);
  

符號鏈接是Java 7對特定文件系統支持最常用的例子,API設計者也考慮到了未來對特定文件系統支持特性的擴展,比如量子加密文件系統。

你已經做過文件處理了,現在可以開始研究對文件內容的處理了。

2.4.4 快速讀寫數據

Java 7可以盡可能多地提供用來讀取和寫入文件內容的輔助方法。當然,這些新方法使用Path,但它們也可以與那些java.io包裡基於流的類進行互操作。因此,你用一個方法就可以讀取文件中的所有行或全部字節。

本節會向你介紹打開文件(帶選項)的過程,以及一小組常用的文件讀/寫例子。讓我們先從打開文件的不同方式開始。

1. 打開文件

Java 7可以直接用帶緩衝區的讀取器和寫入器或輸入輸出流(為了和以前的Java I/O代碼兼容)打開文件。下面的代碼演示了Java 7如何用Files.newBufferedReader方法打開文件並按行讀取其中的內容。

Path logFile = Paths.get("/tmp/app.log");
try (BufferedReader reader = Files.newBufferedReader(logFile, StandardCharsets.UTF_8)) {
  String line;
  while ((line = reader.readLine) != null) {
     ...
  }
}
  

打開一個用於寫入的文件也很簡單。

Path logFile = Paths.get("/tmp/app.log");
try (BufferedWriter writer =
     Files.newBufferedWrite(logFile, StandardCharsets.UTF_8, StandardOpenOption.WRITE)) {
   writer.write("Hello World!");
    ..
}
  

注意StandardOpenOption.WRITE選項的使用,這是可以添加的幾個OpenOption變參之一。它可以確保寫入的文件有正確的訪問許可。其他常用的文件打開選項還有READAPPEND

InputStreamOutputStream的交互是通過Files.newInputStream(Path,OpenOption...)Files.newOutputStream(Path,OpenOption...)實現的。它們為過去基於java.io包的I/O和新的基於java.nio包的文件I/O之間架起了一座橋樑。

提示 在處理String時,不要忘了查看它的字符編碼。忘記設置字符編碼(通過StandardCharsets類,比如new String(byte,StandardCharsets.UTF_8)) 可能導致不可預料的字符編碼問題。

前面的代碼片段還是用Java 6及之前版本編寫的讀取和寫入文件代碼,仍然屬於比較繁瑣的底層代碼。而Java 7具備更高層的抽像能力,可以幫你避免很多不必要的繁瑣編碼工作。

2.簡化讀取和寫入

輔助類Files有兩個輔助方法,用於讀取文件中的全部行和全部字節。也就是說你沒必要再用while循環把數據從字節數組讀到緩衝區裡去。下面的代碼演示了如何調用輔助方法。

Path logFile = Paths.get("/tmp/app.log");
List<String> lines = Files.readAllLines(logFile, StandardCharsets.UTF_8);
byte bytes = Files.readAllBytes(logFile);
  

對於某些軟件來說,什麼時候讀、寫是個問題,特別是在處理屬性文件或日誌時。這時就該文件修改通知系統大顯身手了。

2.4.5 文件修改通知

在Java 7中可以用java.nio.file.WatchService類監測文件或目錄的變化。該類用客戶線程監視註冊文件或目錄的變化,並且在檢測到變化時返回一個事件。這種事件通知對於安全監測、屬性文件中的數據刷新等很多用例都很有用。是現在某些應用程序中常用的輪詢機制(相對而言性能較差)的理想替代品。

下面的代碼用WatchService監測用戶karianna主目錄的變化,每當發現變化時就會在控制台中輸出一個事件通知。和很多持續輪詢的設計一樣,它也需要一個輕量的退出機制。代碼清單2-7如下所示:

代碼清單2-7 使用WatchService

import static java.nio.file.StandardWatchEventKinds.*;

try
{
   WatchService watcher =  
     FileSystems.getDefault.newWatchService;

   Path dir =  
     FileSystems.getDefault.getPath("/usr/karianna");

   WatchKey key = dir.register(watcher, ENTRY_MODIFY); //1監測變化

   while(!shutdown) //2檢查shutdown標誌
   {
     /**3得到下一個 key及其事件*/
     key = watcher.take; 
     for (WatchEvent<?> event: key.pollEvents) 
     {
         if (event.kind == ENTRY_MODIFY) //4檢查是否為變化事件
         {
            System.out.println("Home dir changed!"); 
         }
     }
     key.reset; //5重置監測key
   }
}
catch (IOException | InterruptedException e)
{
  System.out.println(e.getMessage);
}
  

在得到默認的WatchService後,將karianna的主目錄登記到變化監測名單中1。然後在一個無限循環(直到shutdown標誌改變)2中執行WatcherService take方法,直到WatchKey的到來。一旦得到WatchKey,代碼就遍歷其WatchEvent進行檢測3。如果發現了類型為ENTRY_MODIFYWatchEvent4,就詔告天下karianna的主目錄發生了變化!最後重置key5準備迎接下一個事件,繼續等待。

還有其他可以監測的事件,比如ENTRY_CREATEENTRY_DELETEOVERFLOW(可以表明事件已經丟失或被丟棄了)。

接下來,我們要進入一個非常重要的、抽像的新API——用於數據的讀寫,使異步I/O成為現實的SeekableByteChannel

2.4.6 SeekableByteChannel

Java 7引入SeekableByteChannel接口,是為了讓開發人員能夠改變字節通道的位置和大小。比如,應用服務器為了分析日誌中的某個錯誤碼,可以讓多個線程去訪問連接在一個大型日誌文件上的字節通道。

JDK中有一個java.nio.channels.SeekableByteChannel接口的實現類——java.nio.channels.FileChannel。這個類可以在文件讀取或寫入時保持當前位置。比如說,你可能想要寫一段代碼讀取日誌文件中的最後1000個字符,或者向一個文本文件中的特定位置寫入一些價格數據。

下面的代碼展示了如何運用FileChannel的尋址能力讀取日誌文件中的最後1000個字符。

Path logFile = Paths.get("c:\\temp.log");
ByteBuffer buffer = ByteBuffer.allocate(1024);
FileChannel channel = FileChannel.open(logFile, StandardOpenOption.READ);
channel.read(buffer, channel.size - 1000);
  

FileChannel類的尋址能力意味著開發人員可以更加靈活地處理文件內容。我們期待能由此產生一些有趣的開源項目,比如針對大型文件的並行訪問。隨著對該接口的不斷擴展,可能還會有網絡數據流的續傳。

NIO.2 API中下一個主要修改是異步I/O,它可以使用多個後台線程讀寫文件、套接字和通道中的數據。