【问题标题】:Why do popular Java Base64 encoding libraries use OutputStreams for Encoding and InputStreams for encoding?为什么流行的 Java Base64 编码库使用 OutputStreams 进行编码,使用 InputStreams 进行编码?
【发布时间】:2020-02-07 21:29:01
【问题描述】:

我一直在尝试解决 Java 程序中的内存问题,我们将整个文件加载到内存中,对其进行 base64 编码,然后将其用作发布请求中的表单参数。这是由于文件太大而导致OOME。

我正在研究一种解决方案,我可以通过 base64 编码器将文件流式传输到 Http Post 请求的请求正文中。我在所有流行的编码库(Guava、java.util.Base64、android.util.Base64 和 org.apache.batik.util)中注意到的常见模式之一是 if 库支持使用 Streams 进行编码,编码总是通过 OutputStream 完成,而解码总是通过 InputStream 完成。

我很难找到/确定这些决定背后的原因。鉴于这些流行且编写良好的库中的许多都与此 api 设计保持一致,我认为这是有原因的。 将这些解码器之一调整为 InputStream 或接受 InputStream 似乎并不难,但我想知道这些编码器以这种方式设计是否有有效的架构原因。

为什么普通库都是通过 OuputStream 进行 Base64 编码,通过 InputStream 进行 Base64 解码?

支持我的声明的示例:

java.util.Base64
 - Base64.Decoder.wrap(InputStream stream)
 - Base64.Encoder.wrap(OutputStream stream)

android.util.Base64
 - Base64InputStream  // An InputStream that does Base64 decoding on the data read through it.
 - Base64OutputStream // An OutputStream that does Base64 encoding

google.common.io.BaseEncoding
 - decodingStream(Reader reader)
 - encodingStream(Writer writer)

org.apache.batik.util
 - Base64DecodeStream implements InputStream
 - Base64EncodeStream implements OutputStream

【问题讨论】:

  • 出于您已经说明的确切原因:“由于文件大小过大,这是导致 OOME 的原因。”如果您流式传输数据,则无需一次将所有数据保存在内存中,尝试这样做会导致内存不足异常。简而言之,不要尝试在 RAM 中进行操作。
  • 当然,有了HTTP POST request allowing binary data,你可能想知道base 64编码/解码到底有什么意义。这是一个有趣的问题,我在早期也很挣扎,但是一旦你明白了,你就会明白,骑自行车的风格。
  • 我不完全理解这个问题。在您的情况下,您不需要这种能力;您只是用包装器包装 POST 请求的OutputStream,对吗?如果你不需要它,为什么你认为许多其他人需要它?
  • 不幸的是,我无法控制另一端的 API,它指定文件必须使用 application/x-www-form-urlencoded 内容类型作为参数进行 base64 编码。 RE:包装 POST 请求的 OutputStream。无论出于何种原因,我们项目中非常古老的 http 客户端抽象并没有公开包装OutputStream 的方法,但它确实提供了一种提供InputStream 的方法。试图迎合这个 API 是让我回答这个问题的原因。 @MaartenBodewes 的答案正是我想要确认它是不正确的方法。谢谢!
  • 您的老同事有一个InputStream 到某个来源并问自己,“现在我们如何通过 HTTP 连接发送它?”。为此使用给定的InputStream 是合乎逻辑但错误的解决方案(因为主要是处理阻塞调用)。在 Java 9 中,有(方便的)方法transferTo。它是PipedInputStream 的逻辑对应物,它减轻了程序员的缓冲/循环负担以连接两者。

标签: java encoding base64 inputstream


【解决方案1】:

嗯,是的,您可以反转它,但这是最有意义的。 Base64 用于使 二进制数据 - 由应用程序生成或操作 - 与基于文本的外部环境兼容。 所以外部总是需要base 64编码的数据,内部需要解码的二进制数据。

应用程序通常不会对 base 64 编码数据本身执行任何操作;它只需要与另一个应用程序进行二进制数据通信当需要或期望文本接口时


如果要将二进制数据导出到外部,自然会使用输出流。如果该数据需要以 base 64 编码,请确保将数据发送到编码为 base 64 的输出流。

如果您想从外部导入二进制数据,那么您将使用输入流。如果该数据以 base 64 编码,则您首先需要对其进行解码,因此请确保在将其视为二进制流之前对其进行解码。


让我们创建一些图片。假设您有一个在面向文本的环境中运行但在二进制数据上运行的应用程序。重要的部分是左侧应用程序上下文中箭头的方向。

然后你得到输入(读取调用):

{APPLICATION} <- (binary data decoding) <- (base64 decoding) <- (file input stream) <- [BASE 64 ENCODED FILE]

为此,您自然会使用输入流。

那么让我们看看输出(写调用):

{APPLICATION} -> (binary data encoding) -> (base64 encoding) -> (file output stream) -> [BASE 64 ENCODED FILE]

为此,您自然会使用输出流。

这些流可以通过将它们链接在一起相互连接,即使用一个流作为另一个流的父级。


这是一个 Java 示例。请注意,在数据类本身中创建二进制编码器/解码器有点难看;通常你会为此使用另一个类 - 我希望它足以用于演示目的。

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Base64;

public class BinaryHandlingApplication {

    /**
     * A data class that encodes to binary output, e.g. to interact with an application in another language.
     * 
     * Binary format: [32 bit int element string size][UTF-8 element string][32 bit element count]
     * The integers are signed, big endian values.
     * The UTF-8 string should not contain a BOM.
     * Note that this class doesn't know anything about files or base 64 encoding.
     */
    public static class DataClass {
        private String element;
        private int elementCount;

        public DataClass(String element) {
            this.element = element;
            this.elementCount = 1;
        }

        public String getElement() {
            return element;
        }

        public void setElementCount(int count) {
            this.elementCount = count;
        }

        public int getElementCount() {
            return elementCount;
        }

        public String toString() {
            return String.format("%s count is %d", element, elementCount);
        }

        public void save(OutputStream out) throws IOException {

            DataOutputStream dataOutputStream = new DataOutputStream(out);

            // so here we have a chain of:
            // a dataoutputstream on a base 64 encoding stream on a fileoutputstream 


            byte[] utf8EncodedString = element.getBytes(UTF_8);
            dataOutputStream.writeInt(utf8EncodedString.length);
            dataOutputStream.write(utf8EncodedString);

            dataOutputStream.writeInt(elementCount);
        }

        public void load(InputStream in) throws IOException {
            DataInputStream dataInputStream = new DataInputStream(in);

            // so here we have a chain of:
            // a datainputstream on a base 64 decoding stream on a fileinputstream 

            int utf8EncodedStringSize = dataInputStream.readInt();
            byte[] utf8EncodedString = new byte[utf8EncodedStringSize];
            dataInputStream.readFully(utf8EncodedString);
            this.element = new String(utf8EncodedString, UTF_8);

            this.elementCount = dataInputStream.readInt();
        }

    }

    /**
     * Create the a base 64 output stream to a file; the file is the text oriented
     * environment.
     */
    private static OutputStream createBase64OutputStreamToFile(String filename) throws FileNotFoundException {
        FileOutputStream textOutputStream = new FileOutputStream(filename);
        return Base64.getUrlEncoder().wrap(textOutputStream);
    }

    /**
     * Create the a base 64 input stream from a file; the file is the text oriented
     * environment.
     */
    private static InputStream createBase64InputStreamFromFile(String filename) throws FileNotFoundException {
        FileInputStream textInputStream = new FileInputStream(filename);
        return Base64.getUrlDecoder().wrap(textInputStream);
    }

    public static void main(String[] args) throws IOException {
        // this text file acts as the text oriented environment for which we need to encode
        String filename = "apples.txt";

        // create the initial class
        DataClass instance = new DataClass("them apples");
        System.out.println(instance);

        // perform some operation on the data
        int newElementCount = instance.getElementCount() + 2;
        instance.setElementCount(newElementCount);

        // write it away
        try (OutputStream out = createBase64OutputStreamToFile(filename)) {
            instance.save(out);
        }

        // read it into another instance, who cares
        DataClass changedInstance = new DataClass("Uh yeah, forgot no-parameter constructor");
        try (InputStream in = createBase64InputStreamFromFile(filename)) {
            changedInstance.load(in);
        }
        System.out.println(changedInstance);
    }
}

特别注意流的链接,当然也没有任何缓冲区随便。我使用了 URL 安全的 base 64(如果您想使用 HTTP GET 代替)。


当然,在您的情况下,您可以使用 URL 生成 HTTP POST 请求,并直接编码到检索到的 OutputStream 流,方法是包装它。这样就不需要(广泛)缓冲 base 64 编码数据。查看有关如何访问OutputStream here 的示例。

记住,如果你需要缓冲,那你就错了。

如 cmets 中所述,HTTP POST 不需要 base 64 编码,但无论如何,现在您知道如何将 base 64 直接编码为 HTTP 连接了。


java.util.Base64具体说明: 虽然 base 64 是文本,但 base64 流生成/消耗字节; 它只是假设 ASCII 编码(这对于 UTF-16 文本可能很有趣)。 我个人认为这是一个糟糕的设计决定。他们应该包装一个 ReaderWriter 代替,即使这会稍微减慢编码速度。

为了他们的辩护,各种 base 64 标准和 RFC 也犯了这个错误。

【讨论】:

  • 句子'但是,你为什么不使用OutputStream 输出和InputStream 输入?将在解码和编码的上下文中以 why 为真(输入与输出)的原因进行改进。我知道后来在一个方向上进行了尝试,但它遇到了重新措辞(“编码”和“检索”)而不是理由。 (我完全同意这个答案的观点。)
  • 这个答案让我不满意。为什么编码被认为是输出而解码被认为是输入?例如,提供Base64EncodingFilterInputStream 之类的回退/缺点是什么。
  • 提供的例子。这种编码输入流的缺点是您最终会在在您的应用程序中使用base 64。你为什么需要那个?如演示中所示,您不能将其用于链接流。
  • @MaartenBodewes:感谢更新(以及超越)。现在读起来好多了。更新了:)。
  • @MaartenBodewes 精彩的回复让我真正产生了共鸣,谢谢。不使用缓冲的建议让我想知道你的意思,对不起,这是我在 DotNet 之后使用 Java 近 20 年的第一年。由于我在一个大量使用文件/的 P8 项目中的 i/o 流本机而被烧毁。二进制输入/输出。我认为缓冲是一件好事,至少对于 http 和 Java i/o Streams 而言?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多