對於文件的IO操做應該是咱們常常會使用到的,由於文件的複雜性,咱們在使用File操做的時候也有不少須要注意的地方,下面我一塊兒來看看吧。java
不論是在windows仍是linux,文件都有權限控制的概念,咱們能夠設置文件的owner,還有文件的permission,若是文件權限沒有控制好的話,惡意用戶就有可能對咱們的文件進行惡意操做。linux
因此咱們在文件建立的時候就須要考慮到權限的問題。git
很遺憾的是,java並非以文件操做見長的,因此在JDK1.6以前,java的IO操做是很是弱的,基本的文件操做類,好比FileOutputStream和FileWriter並無權限的選項。github
Writer out = new FileWriter("file");
那麼怎麼處理呢?windows
在JDK1.6以前,咱們須要藉助於一些本地方法來實現權限的修改功能。數組
在JDK1.6以後,java引入了NIO,能夠經過NIO的一些特性來控制文件的權限功能。安全
咱們看一下Files工具類的createFile方法:網絡
public static Path createFile(Path path, FileAttribute<?>... attrs) throws IOException { newByteChannel(path, DEFAULT_CREATE_OPTIONS, attrs).close(); return path; }
其中FileAttribute就是文件的屬性,咱們看一下怎麼指定文件的權限:ide
public void createFileWithPermission() throws IOException { Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-------"); FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms); Path file = new File("/tmp/www.flydean.com").toPath(); Files.createFile(file,attr); }
java中不少文件操做是有返回值的,好比file.delete(),咱們須要根據返回值來判斷文件操做是否完成,因此不要忽略了返回值。工具
若是咱們使用到不須要永久存儲的文件時,就能夠很方便的使用File的createTempFile來建立臨時文件。臨時文件的名字是隨機生成的,咱們但願在臨時文件使用完畢以後將其刪除。
怎麼刪除呢?File提供了一個deleteOnExit方法,這個方法會在JVM退出的時候將文件刪除。
注意,這裏的JVM必定要是正常退出的,若是是非正常退出,文件不會被刪除。
咱們看下面的例子:
public void wrongDelete() throws IOException { File f = File.createTempFile("tmpfile",".tmp"); FileOutputStream fop = null; try { fop = new FileOutputStream(f); String str = "Data"; fop.write(str.getBytes()); fop.flush(); } finally { // 由於Stream沒有被關閉,因此文件在windows平臺上面不會被刪除 f.deleteOnExit(); // 在JVM退出的時候刪除臨時文件 if (fop != null) { try { fop.close(); } catch (IOException x) { // Handle error } } } }
上面的例子中,咱們建立了一個臨時文件,而且在finally中調用了deleteOnExit方法,可是由於在調用該方法的時候,Stream並無關閉,因此在windows平臺上會出現文件沒有被刪除的狀況。
怎麼解決呢?
NIO提供了一個DELETE_ON_CLOSE選項,能夠保證文件在關閉以後就被刪除:
public void correctDelete() throws IOException { Path tempFile = null; tempFile = Files.createTempFile("tmpfile", ".tmp"); try (BufferedWriter writer = Files.newBufferedWriter(tempFile, Charset.forName("UTF8"), StandardOpenOption.DELETE_ON_CLOSE)) { // Write to file } }
上面的例子中,咱們在writer的建立過程當中加入了StandardOpenOption.DELETE_ON_CLOSE,那麼文件將會在writer關閉以後被刪除。
若是資源再也不被使用了,咱們須要記得關閉他們,不然就會形成資源的泄露。
可是不少時候咱們可能會忘記關閉,那麼該怎麼辦呢?JDK7中引入了try-with-resources機制,只要把實現了Closeable接口的資源放在try語句中就會自動被關閉,很方便。
NIO中提供了不少很是有用的Buffer類,好比IntBuffer, CharBuffer 和 ByteBuffer等,這些Buffer其實是對底層的數組的封裝,雖然建立了新的Buffer對象,可是這個Buffer是和底層的數組相關聯的,因此不要輕易的將Buffer暴露出去,不然可能會修改底層的數組。
public CharBuffer getBuffer(){ char[] dataArray = new char[10]; return CharBuffer.wrap(dataArray); }
上面的例子暴露了CharBuffer,實際上也暴露了底層的char數組。
有兩種方式對其進行改進:
public CharBuffer getBuffer1(){ char[] dataArray = new char[10]; return CharBuffer.wrap(dataArray).asReadOnlyBuffer(); }
第一種方式就是將CharBuffer轉換成爲只讀的。
第二種方式就是建立一個新的Buffer,切斷Buffer和數組的聯繫:
public CharBuffer getBuffer2(){ char[] dataArray = new char[10]; CharBuffer cb = CharBuffer.allocate(dataArray.length); cb.put(dataArray); return cb; }
java中能夠經過Runtime.exec()來執行native的命令,而Runtime.exec()是有返回值的,它的返回值是一個Process對象,用來控制和獲取native程序的執行信息。
默認狀況下,建立出來的Process是沒有本身的I/O stream的,這就意味着Process使用的是父process的I/O(stdin, stdout, stderr),Process提供了下面的三種方法來獲取I/O:
getOutputStream() getInputStream() getErrorStream()
若是是使用parent process的IO,那麼在有些系統上面,這些buffer空間比較小,若是出現大量輸入輸出操做的話,就有可能被阻塞,甚至是死鎖。
怎麼辦呢?咱們要作的就是將Process產生的IO進行處理,以防止Buffer的阻塞。
public class StreamProcesser implements Runnable{ private final InputStream is; private final PrintStream os; StreamProcesser(InputStream is, PrintStream os){ this.is=is; this.os=os; } @Override public void run() { try { int c; while ((c = is.read()) != -1) os.print((char) c); } catch (IOException x) { // Handle error } } public static void main(String[] args) throws IOException, InterruptedException { Runtime rt = Runtime.getRuntime(); Process proc = rt.exec("vscode"); Thread errorGobbler = new Thread(new StreamProcesser(proc.getErrorStream(), System.err)); Thread outputGobbler = new Thread(new StreamProcesser(proc.getInputStream(), System.out)); errorGobbler.start(); outputGobbler.start(); int exitVal = proc.waitFor(); errorGobbler.join(); outputGobbler.join(); } }
上面的例子中,咱們建立了一個StreamProcesser來處理Process的Error和Input。
InputStream和Reader都有一個read()方法,這兩個方法的不一樣之處就是InputStream read的是Byte,而Reader read的是char。
雖然Byte的範圍是-128到127,可是InputStream.read()會將讀取到的Byte轉換成0-255(0x00-0xff)範圍的int。
Char的範圍是0x0000-0xffff,Reader.read()將會返回一樣範圍的int值:0x0000-0xffff。
若是返回值是-1,表示的是Stream結束了。這裏-1的int表示是:0xffffffff。
咱們在使用的過程當中,須要對讀取的返回值進行判斷,以用來區分Stream的邊界。
咱們考慮這樣的一個問題:
FileInputStream in; byte data; while ((data = (byte) in.read()) != -1) { }
上面咱們將InputStream的read結果先進行byte的轉換,而後再判斷是否等於-1。會有什麼問題呢?
若是Byte自己的值是0xff,自己是一個-1,可是InputStream在讀取以後,將其轉換成爲0-255範圍的int,那麼轉換以後的int值是:0x000000FF, 再次進行byte轉換,將會截取最後的Oxff, Oxff == -1,最終致使錯誤的判斷Stream結束。
因此咱們須要先作返回值的判斷,而後再進行轉換:
FileInputStream in; int inbuff; byte data; while ((inbuff = in.read()) != -1) { data = (byte) inbuff; // ... }
拓展閱讀:這段代碼的輸出結果是多少呢? (int)(char)(byte)-1
首先-1轉換成爲byte:-1是0xffffffff,轉換成爲byte直接截取最後幾位,獲得0xff,也就是-1.
而後byte轉換成爲char:0xff byte是有符號的,轉換成爲2個字節的char須要進行符號位擴展,變成0xffff,可是char是無符號的,對應的十進制是65535。
最後char轉換成爲int,由於char是無符號的,因此擴展成爲0x0000ffff,對應的十進制數是65535.
一樣的下面的例子中,若是提早使用char對int進行轉換,由於char的範圍是無符號的,因此永遠不可能等於-1.
FileReader in; char data; while ((data = (char) in.read()) != -1) { // ... }
在OutputStream中有一個很奇怪的方法,就是write,咱們看下write方法的定義:
public abstract void write(int b) throws IOException;
write接收一個int參數,可是實際上寫入的是一個byte。
由於int和byte的範圍不同,因此傳入的int將會被截取最後的8位來轉換成一個byte。
因此咱們在使用的時候必定要判斷寫入的範圍:
public void writeInt(int value){ int intValue = Integer.valueOf(value); if (intValue < 0 || intValue > 255) { throw new ArithmeticException("Value超出範圍"); } System.out.write(value); System.out.flush(); }
或者有些Stream操做是能夠直接writeInt的,咱們能夠直接調用。
InputStream有兩種帶數組的read方法:
public int read(byte b[]) throws IOException
和
public int read(byte b[], int off, int len) throws IOException
若是咱們使用了這兩種方法,那麼必定要注意讀取到的byte數組是否被填滿,考慮下面的一個例子:
public String wrongRead(InputStream in) throws IOException { byte[] data = new byte[1024]; if (in.read(data) == -1) { throw new EOFException(); } return new String(data, "UTF-8"); }
若是InputStream的數據並無1024,或者說由於網絡的緣由並無將1024填充滿,那麼咱們將會獲得一個沒有填充滿的數組,那麼咱們使用起來實際上是有問題的。
怎麼正確的使用呢?
public String readArray(InputStream in) throws IOException { int offset = 0; int bytesRead = 0; byte[] data = new byte[1024]; while ((bytesRead = in.read(data, offset, data.length - offset)) != -1) { offset += bytesRead; if (offset >= data.length) { break; } } String str = new String(data, 0, offset, "UTF-8"); return str; }
咱們須要記錄實際讀取的byte數目,經過記載偏移量,咱們獲得了最終實際讀取的結果。
或者咱們可使用DataInputStream的readFully方法,保證讀取完整的byte數組。
java中的數據默認是以big-endian的方式來存儲的,DataInputStream中的readByte(), readShort(), readInt(), readLong(), readFloat(), 和 readDouble()默認也是以big-endian來讀取數據的,若是在和其餘的以little-endian進行交互的過程當中,就可能出現問題。
咱們須要的是將little-endian轉換成爲big-endian。
怎麼轉換呢?
好比,咱們想要讀取一個int,能夠首先使用read方法讀取4個字節,而後再對讀取的4個字節作little-endian到big-endian的轉換。
public void method1(InputStream inputStream) throws IOException { try(DataInputStream dis = new DataInputStream(inputStream)) { byte[] buffer = new byte[4]; int bytesRead = dis.read(buffer); // Bytes are read into buffer if (bytesRead != 4) { throw new IOException("Unexpected End of Stream"); } int serialNumber = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt(); } }
上面的例子中,咱們使用了ByteBuffer提供的wrap和order方法來對Byte數組進行轉換。
固然咱們也能夠本身手動進行轉換。
還有一個最簡單的方法,就是調用JDK1.5以後的reverseBytes() 直接進行小端到大端的轉換。
public int reverse(int i) { return Integer.reverseBytes(i); }
本文的代碼:
learn-java-base-9-to-20/tree/master/security
本文已收錄於 http://www.flydean.com/java-security-code-line-file-io/最通俗的解讀,最深入的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!
歡迎關注個人公衆號:「程序那些事」,懂技術,更懂你!