about 4 years ago

這次看一個比較小的東西,就是Java 8終於將Base64編解碼器內建到java.util套件中了。Base64雖然會增加實際傳輸資料的長度,但在只用文字的網路協定中傳輸binary資料時常常用到(例如:MIME email),所以一直到Java 8才內建確實讓人意外,在Java 8之前,需要Base64編解碼器,都需要透過第三方函式庫,例如Apache Commons Codec

還記得之前某個工作內容是當使用者透過瀏覽器上傳A檔案到X系統時,因某些原故,X系統不提供檔案儲放的功能,實際上是將該檔案A放到另一個有提供檔案內容檢索的Y系統;另一個類似的情況是,當使用者透過瀏覽器在X系統下載B檔案(內容是機器生成的)時,該檔案B會放到Y系統的另一個資料夾中備查。X系統與Y系統都是標準的RESTful Web Service,當時,X和Y系統間檔案內容就是先以Base64編碼後,以文字的方式夾帶在JSON中透過HTTP傳輸。當時用的Base64編解碼器正是Apache Commons Codec。

既然Java 8內建Base64編解碼器,那就來寫點程式試用看看吧!不過個人的習慣,對於從輸入讀取資料放到輸出這種事,喜歡使用Apache Commons IOIOUils.copy(InputStream, OutputStream)函式,不過如果沒有使用Apache Commons IO,也可以自己寫一個(如Code List 1),之後就可以不用再寫while loop做資料讀取複製的程式了。

Code List 1 - Copy data from the input stream to the output stream
public static void copy(InputStream input, OutputStream output) throws IOException {
    byte[] buffer = new byte[4096];
    int length = 0;
    while((length = input.read(buffer)) != -1) {
        output.write(buffer, 0, length);
    }
}

有了輔助函式後,就先使用編碼器,將一個InputStream的資料用Base64編碼器編碼後放到指定的OutputStream,Code List 2中,encode(InputStream, OutputStream, Base64.Encoder)函式接受三個參數,第一個是代表資料的來源,第二個是代表編碼後資料的目的地,這兩個蠻容易理解的,那第三個呢?為什麼還要指定編碼器?難道Base64編碼有不同種類嗎?是的,Base64在不同通訊協定中有些變形,所以Java 8提供三種Base64的邊解碼器,第一種是基本版(分別用Base64.getEncoder()Base64.getDecoder()取得邊解碼器),只用0-9a-zA-Z+/=字元編碼且內容不換行;第二種針對網址和檔名修改的版本(用Base64.getUrlEncoder()Base64.getUrlDecoder()取得邊解碼器),由於+/符號在網址中有特殊用途,某些檔案系統也不允許/作為檔名,所以第二種用-(減號)取代+,用_(底線)取代/;第三種針對MIME調整的版本(用Base64.getMimeEncoder()Base64.getMimeDecoder()取得邊解碼器),使用的編碼字元和第一種一樣,但每輸出76個字元會加上一組\r\n換行。假設第一種是比較常用的情況,是可以如Code List 2那樣寫一個無第三參數的版本方便使用。

Code List 2 - Encode the data from the input stream with Base64 encoder
public static void encode(InputStream input, OutputStream output, Base64.Encoder encoder) {
    try(OutputStream encodedOutput = encoder.wrap(output)) {
        copy(input, encodedOutput);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void encode(InputStream input, OutputStream output) {
    encode(input, output, Base64.getEncoder());
}

編碼後就可以用解碼器進行解碼的動作,Code List 3中decode(InputStream, OutputStream, Base64.Decoder)同樣接受三個參數,第一個參數是待解碼的資料來源,第二個是解碼後的資料目的地,第三個是解碼器。剛提到的三種不同邊解碼器是無法混用的,所以使用第一種編碼器編碼的內容一定要用第一種解碼器來進行解碼。同樣,也可以提共一個無第三參數的版本方便使用,接下來的範例程式都可以用同樣的方法提供一個預先使用第一種邊解碼器的版本。

Code List 3 - Decode the data from the input stream with Base64 decoder
public static void decode(InputStream input, OutputStream output, Base64.Decoder decoder) {
    try(InputStream decodedInput = decoder.wrap(input)) {
        copy(decodedInput, output);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void decode(InputStream input, OutputStream output) {
    decode(input, output, Base64.getDecoder());
}

有了從InputStream讀取資料進行編碼或解碼動作後放到OutputStream的函式後,就可以來點不一樣的變形了,例如像Code List 4,可以指定輸入的檔案和輸出的檔案,利用FileInputStreamFileOutputStream將檔案包裝成串流來使用,這時就可以使用Code List 2和Code List 3提供的函示來進行編解碼的動作。

Code List 4 - Encode/Decode the content the source file to the target file
public static void encode(File source, File target, Base64.Encoder encoder) {
    try(InputStream input = new FileInputStream(source);
        OutputStream output = new FileOutputStream(target)) {
        encode(input, output, encoder);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

public static void decode(File source, File target, Base64.Decoder decoder) {
    try(InputStream input = new FileInputStream(source);
        OutputStream output = new FileOutputStream(target)) {
        decode(input, output, decoder);
    } catch(IOException e) {
        e.printStackTrace();
    }
}

當然,很多時候編解碼後的內容並不是要放到檔案中,以剛剛提到的例子,檔案內容會先被編碼,然後當成JSON的一部分,這時會希望有類似Code List 5的程式,將編解碼後的內容當成字串,這時可以用ByteArrayOutputStream暫時存放邊解碼後的內容,然後使用toString(String)將結果以字串輸出。

Code List 5 - Encode/Deocde the file content as a string
public static String encode(File source, Base64.Encoder encoder) {
    String result = null;
    try(InputStream input = new FileInputStream(source);
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        encode(input, output, encoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

public static String decode(File source, Base64.Decoder decoder) {
    String result = null;
    try(InputStream input = new FileInputStream(source);
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        decode(input, output, decoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

同樣,需要被編解碼的資料來源不一定是從檔案來,可能就是一個字串,這時Code List 6的函式就幫得上忙了,先用getBytes()取得字串的位元組陣列,接著用ByteArrayInputStream將位元組陣列包裝成輸入串流,然後用同樣的方法,對輸入的字串進行編解碼的動作。

Code List 6 - Encode/Decode the string
public static String encode(String source, Base64.Encoder encoder) {
        String result = null;
        try(InputStream input = new ByteArrayInputStream(source.getBytes());
            ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            encode(input, output, encoder);
            result = output.toString("UTF-8");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

public static String decode(String source, Base64.Decoder decoder) {
    String result = null;
    try(InputStream input = new ByteArrayInputStream(source.getBytes());
        ByteArrayOutputStream output = new ByteArrayOutputStream()) {
        decode(input, output, decoder);
        result = output.toString("UTF-8");
    } catch (IOException e) {
        e.printStackTrace();
    }
    return result;
}

Java 8有了Base64編解碼器,方便不少,不過Apache Commons Codec提供更多常用的編解碼器,其實是更方便的,但如果你的應用程式中只需要Base64編解碼器,在有Java 8的環境中確實不需要將Apache Commons Codec和專案一起打包,對~前提是要有Java 8的環境,變成用不用內建的Base64編解碼器又是一種抉擇了。

猜謎活動:試著解開這個檔案的內容(提示是這檔案是一個PNG圖檔)。

← Java 8 初探 - Default Methods Quick Glance of Java 8 - Base64 →