使用Base64编码器和InputStreamReader时出现问题

7

我在数据库中有一些CLOB列,需要将Base64编码的二进制文件放入其中。

由于这些文件可能很大,因此我需要进行流式处理,不能一次性读取整个文件。

我正在使用org.apache.commons.codec.binary.Base64InputStream进行编码,但是遇到了问题。我的代码基本上是这样的:

FileInputStream fis = new FileInputStream(file);
Base64InputStream b64is = new Base64InputStream(fis, true, -1, null);
BufferedReader reader = new BufferedReader(new InputStreamReader(b64is));

preparedStatement.setCharacterStream(1, reader);

运行以上代码时,在更新执行期间,我会得到以下错误之一:java.io.IOException: Underlying input stream returned zero bytes,它是在InputStreamReader代码深处抛出的。
为什么会出现这种情况呢?在我看来,reader应该尝试从base 64流中读取,该流将从文件流中读取,一切都应该很顺利。
3个回答

14

看起来这是 Base64InputStream 中的一个bug。你的调用是正确的。

你应该向Apache commons codec项目报告此问题。

简单测试案例:

import java.io.*;
import org.apache.commons.codec.binary.Base64InputStream;

class tmp {
  public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream(args[0]);
    Base64InputStream b64is = new Base64InputStream(fis, true, -1, null);

    while (true) {
      byte[] c = new byte[1024];
      int n = b64is.read(c);
      if (n < 0) break;
      if (n == 0) throw new IOException("returned 0!");
      for (int i = 0; i < n; i++) {
        System.out.print((char)c[i]);
      }
    }
  }
}

InputStreamread(byte[]) 调用不允许返回 0。但在任何长度为三的倍数的文件上,它会返回 0。


1
是的,你说得对。这是Base64InputStream中的一个错误。+1为确认此错误的测试用例。 - BalusC
2
报告一下:https://issues.apache.org/jira/browse/CODEC-101 话说,我仍然在想这个巧合,我的测试文件确实是3个字节的倍数长:o) - BalusC
1
哇,谢谢你的确认,我必须说我很惊讶我发现了这样一个错误(虽然是无意中)。 - karoberts

4
有趣的是,我在这里做了一些测试,无论流的来源如何,当你使用InputStreamReader读取Base64InputStream时,它确实会抛出异常,但是当你将其作为二进制流读取时,它却可以完美地工作。正如Trashgod所提到的,Base64编码是帧结构的。InputStreamReader实际上应该在Base64InputStream上再次调用flush(),以查看是否还有更多数据返回。

我没有看到其他修复此问题的方法,除了实现自己的Base64InputStreamReaderBase64Reader这实际上是一个bug,请参见Keith的答案。

作为解决方法,您还可以将其存储在DB中的BLOB而不是CLOB中,并使用PreparedStatement#setBinaryStream()。无论它被存储为二进制数据还是其他内容都没有关系。您不希望具有如此大的Base64数据可索引或可搜索。


更新:由于这不是一个选择,让Apache Commons Codec的人修复我报告的Base64InputStream错误CODEC-101可能需要一些时间,您可以考虑使用另一个第三方Base64 API。我在这里找到了一个here(公共领域,所以您可以随意使用它,甚至放在自己的包中),我在这里测试过,它运行良好。
InputStream base64 = new Base64.InputStream(input, Base64.ENCODE);

更新2:commons codec的开发者很快修复了这个问题。

Index: src/java/org/apache/commons/codec/binary/Base64InputStream.java
===================================================================
--- src/java/org/apache/commons/codec/binary/Base64InputStream.java (revision 950817)
+++ src/java/org/apache/commons/codec/binary/Base64InputStream.java (working copy)
@@ -145,21 +145,41 @@
         } else if (len == 0) {
             return 0;
         } else {
-            if (!base64.hasData()) {
-                byte[] buf = new byte[doEncode ? 4096 : 8192];
-                int c = in.read(buf);
-                // A little optimization to avoid System.arraycopy()
-                // when possible.
-                if (c > 0 && b.length == len) {
-                    base64.setInitialBuffer(b, offset, len);
+            int readLen = 0;
+            /*
+             Rationale for while-loop on (readLen == 0):
+             -----
+             Base64.readResults() usually returns > 0 or EOF (-1).  In the
+             rare case where it returns 0, we just keep trying.
+
+             This is essentially an undocumented contract for InputStream
+             implementors that want their code to work properly with
+             java.io.InputStreamReader, since the latter hates it when
+             InputStream.read(byte[]) returns a zero.  Unfortunately our
+             readResults() call must return 0 if a large amount of the data
+             being decoded was non-base64, so this while-loop enables proper
+             interop with InputStreamReader for that scenario.
+             -----
+             This is a fix for CODEC-101
+            */
+            while (readLen == 0) {
+                if (!base64.hasData()) {
+                    byte[] buf = new byte[doEncode ? 4096 : 8192];
+                    int c = in.read(buf);
+                    // A little optimization to avoid System.arraycopy()
+                    // when possible.
+                    if (c > 0 && b.length == len) {
+                        base64.setInitialBuffer(b, offset, len);
+                    }
+                    if (doEncode) {
+                        base64.encode(buf, 0, c);
+                    } else {
+                        base64.decode(buf, 0, c);
+                    }
                 }
-                if (doEncode) {
-                    base64.encode(buf, 0, c);
-                } else {
-                    base64.decode(buf, 0, c);
-                }
+                readLen = base64.readResults(b, offset, len);
             }
-            return base64.readResults(b, offset, len);
+            return readLen;
         }
     }

我在这里尝试过,它运行良好。


很遗憾,我不能使用BLOB,因为有时其中的数据将是文本。 - karoberts

0
"为了获得最高效率,请考虑将InputStreamReader包装在BufferedReader中。例如:"
BufferedReader in = new BufferedReader(new InputStreamReader(b64is));

补充说明:由于Base64填充到4个字符的倍数,因此请验证源是否被截断。可能需要使用flush()

也许这样更有效率,但它并没有解决问题。 - karoberts
你的流是否有被截断的可能性?如果我没记错的话,base64 是有框架的。 - trashgod
问题已更新。您能详细说明一下“base64被框定”的意思吗?流直接来自文件。 - karoberts
编码流必须填充为“4个字符的整数倍”,以便解码最后一个字节;如果流被截断,这将是一个问题。参考文献如上所述。 - trashgod
@trashgod - "上面引用的参考资料在哪里?" - Stephen C
@Stephen C: "一个4个字符的整数倍"—Base64 http://en.wikipedia.org/wiki/Base64 - trashgod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接