天天看點

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

目錄

一、InputStream

二、FileInputStream

1、定義

2、initIDs 

3、 open0 / close0

4、read0/ readBytes 

5、skip0 / available0

三、FileDescriptor

1、定義

2、attach / closeAll

3、initIDs / sync

四、OutputStream

 五、FileOutputStream

1、定義

2、initIDs / open0 / close0

3、write / writeBytes

從本篇部落格開始會逐一講解InputStream / OutputStream及其對應子類的使用與實作細節。

一、InputStream

      InputStream是一個抽象類而非接口類,該類實作的接口如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

其中AutoCloseable接口是JDK 1.7引入的,try代碼塊依賴此接口實作資源自動釋放;Closeable接口是JDK 1.5引入的,覆寫了AutoCloseable定義的close方法,将其變成public并抛出IOException異常。InputStream的子類衆多,重點關注io包下的子類,如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

其中帶紅點的都是内部私有的類,其他public子類在後續的部落格中會陸續探讨。

     InputStream定義的方法如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

其中子類必須實作的抽象方法隻有一個,用于讀取流中下一個位元組的無參的read方法,傳回值在0到255之間,如果到達流末尾了則傳回-1。該方法會阻塞目前線程,直到讀取到資料,到了流末尾或者抛出了異常,其定義如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

 重載的兩個read方法和skip方法都是基于無參的read方法實作,其他方法都是無意義的空實作,如下:

public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

//從流中讀取位元組資料,将讀取到的第一個位元組到b的off處,然後依次讀取len個,傳回實際讀取的位元組數
public int read(byte b[], int off, int len) throws IOException {
        //校驗參數
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) { //流終止
            return -1;
        }
        //傳回值在0到255之間,是以可以強轉成byte
        b[off] = (byte)c;

        int i = 1;
        try {
            //i從1開始,讀取len-1個位元組
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }

//跳過指定的位元組數,傳回實際跳過的位元組數
public long skip(long n) throws IOException {

        long remaining = n;
        int nr;

        if (n <= 0) {
            return 0;
        }
        //MAX_SKIP_BUFFER_SIZE的值是2048
        int size = (int)Math.min(MAX_SKIP_BUFFER_SIZE, remaining);
        byte[] skipBuffer = new byte[size];
        while (remaining > 0) {
            //讀取指定的位元組數
            nr = read(skipBuffer, 0, (int)Math.min(size, remaining));
            if (nr < 0) {//流終止,退出循環
                break;
            }
            //計算剩餘的需要跳過的位元組數
            remaining -= nr;
        }

        return n - remaining;
    }
           

二、FileInputStream

1、定義

       FileInputStream繼承自InputStream,包含的屬性如下:

/* 檔案描述符 */
    private final FileDescriptor fd;

    /**
     * 檔案路徑
     */
    private final String path;
    
    /*
     * NIO使用的FileChannel
     */
    private FileChannel channel = null;

    private final Object closeLock = new Object();

    //檔案是否已關閉的辨別
    private volatile boolean closed = false;
           

 其構造方法如下:

public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
    }

public FileInputStream(File file) throws FileNotFoundException {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            //檢查檔案通路權限
            security.checkRead(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) { //檢查路徑是否合法
            throw new FileNotFoundException("Invalid file path");
        }
        //初始化fd
        fd = new FileDescriptor();
        fd.attach(this);
        path = name;
        //打開檔案描述符
        open(name);
    }

//适用于從已經打開的fdObj中讀取資料,即多個FileInputStream執行個體共享一個fdObj
//可以通過getFD方法擷取目前綁定的FileDescriptor執行個體
public FileInputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        }
        if (security != null) {
            security.checkRead(fdObj);
        }
        fd = fdObj;
        path = null;

        /*
         *将fd同目前File綁定
         */
        fd.attach(this);
    }

private void open(String name) throws FileNotFoundException {
        //本地方法實作
        open0(name);
    }
           

 FileInputStream改寫了父類所有方法的實作,其核心都通過本地方法實作,如下:

public int read() throws IOException {
        //本地方法
        return read0();
    }

public int read(byte b[]) throws IOException {
       //本地方法
        return readBytes(b, 0, b.length);
    }

public int read(byte b[], int off, int len) throws IOException {
        //本地方法
        return readBytes(b, off, len);
    }

public long skip(long n) throws IOException {
        //本地方法
        return skip0(n);
    }

public int available() throws IOException {
        //本地方法
        return available0();
    }

public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                //擷取鎖,如果已關閉則傳回
                return;
            }
            //未關閉,将closed置為true
            closed = true;
        }
        if (channel != null) {
           channel.close();
        }

        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               //本地方法
               close0();
           }
        });
    }
           

 下面逐一說明各本地方法的實作細節,位于OpenJDK jdk\src\share\native\java\io\FileInputStream.c中,FileInputStream.c中檔案操作相關方法的實作在jdk\src\solaris\native\java\io\io_util_md.c中。

2、initIDs 

      initIDs本地方法是FileInputStream的靜态代碼塊執行的,如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

該方法用于初始化FileInputStream中fd字段的引用ID,知道此引用ID和具體的FileInputStream執行個體就可擷取對應的fd字段引用了,如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

3、 open0 / close0

       open0的核心就是打開檔案描述符的open64函數,傳回的fd是一個數字,目前未使用的最小的檔案描述符,實際是目前程序打開檔案的檔案指針數組的索引,一般從3開始,因為标準輸入流0,标準輸出流1,标準錯誤流2是Linux預配置設定的。close0的核心就是關閉檔案描述符的close函數。

JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
    fileOpen(env, this, path, fis_fd, O_RDONLY);
}

JNIEXPORT void JNICALL
Java_java_io_FileInputStream_close0(JNIEnv *env, jobject this) {
    fileClose(env, this, fis_fd);
}


void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    WITH_PLATFORM_STRING(env, path, ps) {
        FD fd;

#if defined(__linux__) || defined(_ALLBSD_SOURCE)
        /* Remove trailing slashes, since the kernel won't */
        char *p = (char *)ps + strlen(ps) - 1;
        while ((p > ps) && (*p == '/'))
            *p-- = '\0';
#endif
        //打開檔案描述符
        fd = handleOpen(ps, flags, 0666);
        if (fd != -1) {
            //fid就是FileInputStream執行個體的fd字段的jfieldID
            //儲存fd到FileInputStream的fd屬性對應的FileDescriptor執行個體的fd字段中
            SET_FD(this, fd, fid);
        } else {
            throwFileNotFoundException(env, path);
        }
    } END_PLATFORM_STRING(env, ps);
}

void
fileClose(JNIEnv *env, jobject this, jfieldID fid)
{
    //擷取目前FileInputStream執行個體的fd屬性對應的FileDescriptor執行個體fd字段的值
    FD fd = GET_FD(this, fid);
    if (fd == -1) { //為-1,說明檔案未打開
        return;
    }

    /* 
    實際關閉前先将其置為-1,可減少執行關閉動作時其他線程仍在通路該fd
     */
    SET_FD(this, -1, fid);

    /*
     * 如果是标準輸入流0,标準輸出流1,标準錯誤流2則不關閉他們,而是将其重定向到/dev/null
     */
    if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) {
        //打開/dev/null的檔案描述符
        int devnull = open("/dev/null", O_WRONLY);
        if (devnull < 0) {
            SET_FD(this, fd, fid); // restore fd
            JNU_ThrowIOExceptionWithLastError(env, "open /dev/null failed");
        } else {
            //将fd重定向到devnull,實際是将devnull對應的檔案描述符拷貝到fd處
            dup2(devnull, fd);
            //關閉devnull對應的檔案描述符
            close(devnull);
        }
    } else if (close(fd) == -1) { //非标準流
        JNU_ThrowIOExceptionWithLastError(env, "close failed");
    }
}

FD
handleOpen(const char *path, int oflag, int mode) {
    FD fd;
    //調用open64函數打開檔案描述符,傳回的fd是一個數字,目前未使用的最小的檔案描述符,實際是一個目前程序打開檔案的檔案指針數組的索引
    RESTARTABLE(open64(path, oflag, mode), fd);
    if (fd != -1) {
        struct stat64 buf64;
        int result;
        //調用fstat64函數擷取檔案屬性
        RESTARTABLE(fstat64(fd, &buf64), result);
        if (result != -1) {
            //如果目标檔案是檔案夾則關閉檔案描述符
            if (S_ISDIR(buf64.st_mode)) {
                //調用close函數關閉fd
                close(fd);
                errno = EISDIR;
                fd = -1;
            }
        } else {
            //調用fstat64函數失敗,關閉檔案描述符
            close(fd);
            fd = -1;
        }
    }
    return fd;
}

#define SET_FD(this, fd, fid) \
    //fid是FileInputStream執行個體的fd字段的jfieldID,即先擷取FileInputStream執行個體的fd屬性
    if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \
        //IO_fd_fdID是FileDescriptor執行個體fd字段的jfieldID
        (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))

#define GET_FD(this, fid) \
    //如果FileInputStream執行個體的fd屬性為空,則傳回-1,不為空則傳回該屬性對應的FileDescriptor執行個體fd字段的值
    (*env)->GetObjectField(env, (this), (fid)) == NULL ? \ 
        -1 : (*env)->GetIntField(env, (*env)->GetObjectField(env, (this), (fid)), IO_fd_fdID)
           

4、read0/ readBytes 

      read0用于讀取單個位元組,readBytes 是讀取多個位元組的資料并寫入數組中指定位置,這兩個方法底層都是依賴可讀取多個位元組的read函數,該函數傳回實際讀取的位元組數,如果為0,則表示已經讀取到流末尾。

JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_read0(JNIEnv *env, jobject this) {
    return readSingle(env, this, fis_fd);
}

JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,
        jbyteArray bytes, jint off, jint len) {
    return readBytes(env, this, bytes, off, len, fis_fd);
}

jint
readSingle(JNIEnv *env, jobject this, jfieldID fid) {
    jint nread;
    char ret;
    //擷取關聯的檔案描述符
    FD fd = GET_FD(this, fid);
    if (fd == -1) {
        //為-1,描述符已關閉
        JNU_ThrowIOException(env, "Stream Closed");
        return -1;
    }
    //讀取單個位元組,讀取的結果儲存在ret中
    //IO_Read通過宏定義指向handleRead方法
    nread = IO_Read(fd, &ret, 1);
    if (nread == 0) { /* EOF */
        return -1; //讀取結束
    } else if (nread == -1) { /* IO異常 */
        JNU_ThrowIOExceptionWithLastError(env, "Read error");
    }
    //求且,保證ret的值不超過255
    return ret & 0xFF;
}

jint
readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
          jint off, jint len, jfieldID fid)
{
    jint nread;
    //BUF_SIZE是一個宏,取值為8192
    char stackBuf[BUF_SIZE];
    char *buf = NULL;
    FD fd;

    if (IS_NULL(bytes)) {
        //儲存結果資料的數組為空,則抛出異常
        JNU_ThrowNullPointerException(env, NULL);
        return -1;
    }

    if (outOfBounds(env, off, len, bytes)) {
        //數組越界
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
        return -1;
    }

    if (len == 0) {
        return 0;
    } else if (len > BUF_SIZE) {
        //超過了最大長度,則另外配置設定一個緩存
        buf = malloc(len);
        if (buf == NULL) {
            //抛出記憶體不足異常
            JNU_ThrowOutOfMemoryError(env, NULL);
            return 0;
        }
    } else {
        //正常使用stackBuf
        buf = stackBuf;
    }
    
    //擷取目前檔案關聯的fd
    fd = GET_FD(this, fid);
    if (fd == -1) {
        //檔案描述符已關閉
        JNU_ThrowIOException(env, "Stream Closed");
        nread = -1;
    } else {
        //讀取指定位元組的資料
        nread = IO_Read(fd, buf, len);
        if (nread > 0) {
            //讀取成功,将資料寫入到bytes數組中
            (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);
        } else if (nread == -1) {
            //read函數讀取失敗
            JNU_ThrowIOExceptionWithLastError(env, "Read error");
        } else { /* EOF */
            nread = -1; //讀取到流末尾
        }
    }

    if (buf != stackBuf) {
        free(buf);//釋放緩存
    }
    return nread;
}

ssize_t
handleRead(FD fd, void *buf, jint len)
{
    ssize_t result;
    //調用read函數讀取位元組資料,buf用于儲存資料,len表示讀取的位元組數
    //result用于儲存read函數的調用結果,即實際讀取的位元組數,如果為0,則表示已經到達流末尾了
    RESTARTABLE(read(fd, buf, len), result);
    return result;
}

#define IS_NULL(obj) ((obj) == NULL)

static int
outOfBounds(JNIEnv *env, jint off, jint len, jbyteArray array) {
    return ((off < 0) ||
            (len < 0) ||
            //len超過了array中剩餘可用空間
            ((*env)->GetArrayLength(env, array) - off < len));
}
           

5、skip0 / available0

      skip0的核心是用于擷取和操作檔案讀寫位置的lseek函數,available0的實作會區分fd的類型,如果是Socket等檔案描述符,則使用ioctl函數擷取接受緩沖區中的位元組數,如果是正常的檔案,則使用lseek函數擷取剩餘的未讀取的位元組數。

JNIEXPORT jlong JNICALL
Java_java_io_FileInputStream_skip0(JNIEnv *env, jobject this, jlong toSkip) {
    jlong cur = jlong_zero;
    jlong end = jlong_zero;
    
    //擷取目前檔案關聯的檔案描述符
    FD fd = GET_FD(this, fis_fd);
    if (fd == -1) {
        //檔案描述符已關閉,抛出異常
        JNU_ThrowIOException (env, "Stream Closed");
        return 0;
    }
    //IO_Lseek通過宏定義指向lseek64函數
    //擷取目前檔案的讀寫位置
    if ((cur = IO_Lseek(fd, (jlong)0, (jint)SEEK_CUR)) == -1) {
        //調用函數失敗,抛出異常
        JNU_ThrowIOExceptionWithLastError(env, "Seek error");
    //将目前讀寫位置向後移動toSkip位元組,傳回目前的位元組位置    
    } else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1) {
        JNU_ThrowIOExceptionWithLastError(env, "Seek error");
    }
    //傳回實際的跳過位元組數,因為跳過的位元組數可能超過檔案末尾
    return (end - cur);
}

JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_available0(JNIEnv *env, jobject this) {
    jlong ret;
     //擷取目前檔案關聯的檔案描述符
    FD fd = GET_FD(this, fis_fd);
    if (fd == -1) {
        //檔案描述符已關閉,抛出異常
        JNU_ThrowIOException (env, "Stream Closed");
        return 0;
    }
    //IO_Available通過宏定義指向handleAvailable方法
    if (IO_Available(fd, &ret)) {
        if (ret > INT_MAX) {
            //傳回值超過int的最大值,則将其置為int的最大值
            ret = (jlong) INT_MAX;
        } else if (ret < 0) {
            ret = 0;
        }
        //long轉換成int類型
        return jlong_to_jint(ret);
    }
    //調用C函數失敗,抛出異常
    JNU_ThrowIOExceptionWithLastError(env, NULL);
    return 0;
}

jint
handleAvailable(FD fd, jlong *pbytes)
{
    int mode;
    struct stat64 buf64;
    jlong size = -1, current = -1;

    int result;
    //調用fstat64函數擷取檔案描述符的屬性
    RESTARTABLE(fstat64(fd, &buf64), result);
    if (result != -1) {
        mode = buf64.st_mode;
        if (S_ISCHR(mode) || S_ISFIFO(mode) || S_ISSOCK(mode)) {
            int n;
            int result;
            //ioctl函數是對IO管道進行管理的函數,FIONREAD是該函數支援的一個cmd,表示擷取接受資料緩沖區中的位元組數
            //result用于儲存函數執行的結果,n用于儲存cmd執行的結果
            RESTARTABLE(ioctl(fd, FIONREAD, &n), result);
            if (result >= 0) {
                //執行成功,可用位元組數就是n
                *pbytes = n;
                return 1;
            }
        } else if (S_ISREG(mode)) {
            //如果是正常的檔案,則size是檔案大小
            size = buf64.st_size;
        }
    }
    
    //擷取目前的檔案讀寫位置
    if ((current = lseek64(fd, 0, SEEK_CUR)) == -1) {
        //擷取失敗,則傳回0
        return 0;
    }

    if (size < current) {
        //如果size小于目前讀寫位置,說明實際的檔案大小不止size
        //則擷取檔案末尾對應的位置
        if ((size = lseek64(fd, 0, SEEK_END)) == -1)
            return 0;
        //上一步操作成功會将檔案讀寫位置移動到檔案末尾,此處将其恢複至原來的位置    
        else if (lseek64(fd, current, SEEK_SET) == -1)
            return 0;
    }
    //計算剩餘的流資料大小
    *pbytes = size - current;
    return 1;
}
           

三、FileDescriptor

1、定義

     FileDescriptor表示一個檔案描述符,具體可以是一個打開的檔案或者Socket或者位元組序列,主要用在FileInputStream和FileOutputStream中,應用程式不應該直接建立FileDescriptor執行個體。Window下和Linux下FileDescriptor稍有差異,我們關注Linux下的實作,該類的源碼在jdk\src\solaris\classes\java\io目錄下,其包含的屬性如下:

//open函數傳回的檔案描述符
    private int fd;

    private Closeable parent;
    private List<Closeable> otherParents;
    private boolean closed;
           

 構造函數的實作如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

該類包含了3個static final常量,分别對應到System類的in,out和err常量,如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

重點關注以下方法的實作。

2、attach / closeAll

      attach方法用于儲存從目前FileDescriptor執行個體中讀取資料的Closeable執行個體引用,友善在closeAll方法中一次的将所有從該FileDescriptor執行個體中讀取資料的Closeable執行個體都關閉掉。

//将某個FileInputStream執行個體同目前fd綁定,即存在多個FileInputStream同時從一個fd中讀取資料
//主要為下面的closeAll服務
synchronized void attach(Closeable c) {
        if (parent == null) {
            // first caller gets to do this
            parent = c;
        } else if (otherParents == null) {
            //初始化otherParents
            otherParents = new ArrayList<>();
            otherParents.add(parent);
            otherParents.add(c);
        } else {
            otherParents.add(c);
        }
    }

//将多個從目前fd中讀取資料的Closeable執行個體關閉
@SuppressWarnings("try")
synchronized void closeAll(Closeable releaser) throws IOException {
        if (!closed) {
            closed = true;
            IOException ioe = null;
            try (Closeable c = releaser) {
                if (otherParents != null) {
                    for (Closeable referent : otherParents) {
                        try {
                            //調用close方法關閉流
                            referent.close();
                        } catch(IOException x) {
                            if (ioe == null) {
                                ioe = x;
                            } else {
                                //儲存異常資訊
                                ioe.addSuppressed(x);
                            }
                        }
                    }
                }
            } catch(IOException ex) {
                /*
                 * If releaser close() throws IOException
                 * add other exceptions as suppressed.
                 */
                if (ioe != null)
                    ex.addSuppressed(ioe);
                ioe = ex;
            } finally {
                if (ioe != null) //抛出異常
                    throw ioe;
            }
        }
    }
           

3、initIDs / sync

      initIDs是本地方法,在static代碼塊中調用,如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

該方法用來初始化FileDescriptor類中fd屬性的引用ID,其實作如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

sync也是一個本地方法,其實作如下:

JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_sync(JNIEnv *env, jobject this) {
    FD fd = THIS_FD(this);
    //IO_Sync通過宏定義指向fsync函數,該函數會将高速緩存中的發生修改的塊緩沖區和檔案屬性的修改強制重新整理到磁盤中
    //會阻塞目前線程直到重新整理動作完成
    if (IO_Sync(fd) == -1) {
        //調用fsync失敗,抛出異常
        JNU_ThrowByName(env, "java/io/SyncFailedException", "sync failed");
    }
}

#define THIS_FD(obj) (*env)->GetIntField(env, obj, IO_fd_fdID)
           

四、OutputStream

     OutputStream也是一個抽象類,該類定義的方法如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

子類必須實作的抽象方法隻有一個,write(int)方法,用于寫入單個位元組,與InputStream中的無參read方法對應;flush和close方法是空實作,另外兩個重載的write方法都是依賴第一個write方法實作,如下:

public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

public void write(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if ((off < 0) || (off > b.length) || (len < 0) ||
                   ((off + len) > b.length) || ((off + len) < 0)) {
            //數組越界       
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        //周遊位元組數組,逐一寫入
        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }
           

 五、FileOutputStream

1、定義

      FileOutputStream繼承自OutputStream,包含的屬性如下:

/**
     * 檔案描述符
     */
    private final FileDescriptor fd;

    /**
     * 是否寫入到檔案後面,預設為false,會檔案中原來的内容清空
     */
    private final boolean append;

    /**
     * 關聯的FileChannel
     */
    private FileChannel channel;

    /**
     * 檔案路徑
     */
    private final String path;
    
    //流關閉用到的鎖
    private final Object closeLock = new Object();
    //是否關閉
    private volatile boolean closed = false;
           

 其構造方法如下:

public FileOutputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null, false);
    }

public FileOutputStream(String name, boolean append)
        throws FileNotFoundException
    {
        this(name != null ? new File(name) : null, append);
    }

public FileOutputStream(File file) throws FileNotFoundException {
        this(file, false);
    }

public FileOutputStream(File file, boolean append)
        throws FileNotFoundException
    {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            //檢查是否通路權限
            security.checkWrite(name);
        }
        if (name == null) {
            throw new NullPointerException();
        }
        if (file.isInvalid()) { //檢查路徑是否合法
            throw new FileNotFoundException("Invalid file path");
        }
        //初始化檔案描述符
        this.fd = new FileDescriptor();
        //将目前FileOutputStream執行個體注冊到fd中
        fd.attach(this);
        this.append = append;
        this.path = name;
        //打開檔案描述符
        open(name, append);
    }

private void open(String name, boolean append)
        throws FileNotFoundException {
        open0(name, append);
    }
           

同FileInputStream,write和close相關方法的實作都封裝在本地方法中,如下:

public void write(int b) throws IOException {
        //本地方法
        write(b, append);
    }

public void write(byte b[]) throws IOException {
        //本地方法
        writeBytes(b, 0, b.length, append);
    }

public void write(byte b[], int off, int len) throws IOException {
        //本地方法
        writeBytes(b, off, len, append);
    }

public void close() throws IOException {
        synchronized (closeLock) { //擷取鎖
            if (closed) {
                return; //已關閉則直接傳回
            }
            closed = true;//未關閉,将close的置為true
        }

        if (channel != null) {
            channel.close();
        }
        //關閉fd
        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();//本地方法
           }
        });
    }
           

注意FileOutputStream并沒有改寫父類OutputStream中空實作的flush方法,如果希望将檔案修改顯示的重新整理到磁盤,需要顯示調用FileDescriptor的sync方法,底層是調用fsync函數。下面逐一說明各本地方法的實作細節,源碼在OpenJDK jdk\src\solaris\native\java\io\FileOutputStream_md.c中。

2、initIDs / open0 / close0

      initIDs本地方法是在靜态代碼塊中執行的,如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

該方法用于初始化 FileOutputStream中fd字段的引用ID,如下:

Java8 FileInputStream / FileDescriptor / FileOutputStream 源碼解析

 open0 和 close0 的實作跟中open0 和 close0方法的實作完全一樣,這裡不再詳細展開,如下:

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_open0(JNIEnv *env, jobject this,
                                    jstring path, jboolean append) {
    //FileInputStream中調用fileOpen方法時,傳遞的flag是O_RDONLY                                 
    fileOpen(env, this, path, fos_fd,
             O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_close0(JNIEnv *env, jobject this) {
    fileClose(env, this, fos_fd);
}
           

3、write / writeBytes

        這兩個方法的核心都是寫入指定位元組數的write函數,如下:

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_write(JNIEnv *env, jobject this, jint byte, jboolean append) {
    writeSingle(env, this, byte, append, fos_fd);
}

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_writeBytes(JNIEnv *env,
    jobject this, jbyteArray bytes, jint off, jint len, jboolean append) {
    writeBytes(env, this, bytes, off, len, append, fos_fd);
}

void
writeSingle(JNIEnv *env, jobject this, jint byte, jboolean append, jfieldID fid) {
    // Discard the 24 high-order bits of byte. See OutputStream#write(int)
    char c = (char) byte;
    jint n;
    //擷取關聯的fd
    FD fd = GET_FD(this, fid);
    if (fd == -1) {
        JNU_ThrowIOException(env, "Stream Closed");
        return;
    }
    //IO_Append和IO_Write 都通過宏指向handleWrite函數
    if (append == JNI_TRUE) {
        n = IO_Append(fd, &c, 1);
    } else {
        n = IO_Write(fd, &c, 1);
    }
    if (n == -1) {
        JNU_ThrowIOExceptionWithLastError(env, "Write error");
    }
}

void
writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,
           jint off, jint len, jboolean append, jfieldID fid)
{
    jint n;
    //BUF_SIZE的值是8192
    char stackBuf[BUF_SIZE];
    char *buf = NULL;
    FD fd;

    if (IS_NULL(bytes)) { //位元組數組為null,抛出異常
        JNU_ThrowNullPointerException(env, NULL);
        return;
    }

    if (outOfBounds(env, off, len, bytes)) {//數組越界
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
        return;
    }

    if (len == 0) {
        return;
    } else if (len > BUF_SIZE) {
        //超過BUF_SIZE,則另外配置設定記憶體
        buf = malloc(len);
        if (buf == NULL) {
            //記憶體配置設定失敗,抛出異常
            JNU_ThrowOutOfMemoryError(env, NULL);
            return;
        }
    } else {
        buf = stackBuf;
    }
    
    //将bytes數組中的資料拷貝到buf中
    (*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf);

    if (!(*env)->ExceptionOccurred(env)) {
        //上一步的拷貝執行成功
        off = 0;
        while (len > 0) {
            fd = GET_FD(this, fid);
            if (fd == -1) {
                JNU_ThrowIOException(env, "Stream Closed");
                break;
            }
            //IO_Append和IO_Write 都通過宏指向handleWrite函數
            //buf+off把指針往前移動off個
            if (append == JNI_TRUE) {
                //會一次性最多寫入len個,實際因為多種原因可能沒有寫入len個就傳回了
                //n表示寫入的位元組數
                n = IO_Append(fd, buf+off, len);
            } else {
                n = IO_Write(fd, buf+off, len);
            }
            if (n == -1) {
                //寫入失敗,抛出異常
                JNU_ThrowIOExceptionWithLastError(env, "Write error");
                break;
            }
            //如果執行成功,n等于實際寫入的位元組數
            off += n;
            len -= n;
        }
    }
    if (buf != stackBuf) {
        free(buf); //釋放單獨配置設定的記憶體
    }
}

ssize_t
handleWrite(FD fd, const void *buf, jint len)
{
    ssize_t result;
    //調用write函數寫入指定的位元組數
    RESTARTABLE(write(fd, buf, len), result);
    return result;
}
           

繼續閱讀