【问题标题】:Best way to add a property to JSON without reading in memory在不读取内存的情况下向 JSON 添加属性的最佳方法
【发布时间】:2021-01-11 13:36:27
【问题描述】:

我有一个大型 JSON 和复杂文件 (~100MB)。我需要在不读取内存的情况下为其添加一个属性。如果不读取内存中的全部内容,我无法找到附加到 JSON 的选项。

我找不到任何合适的例子。

我能想到的最好的办法是使用StreamReader(s) 和StreamReader(s) 将最后一个} 替换为"key":"value"}

JSON 是通过遗留应用程序读取的,我们正在将其集成到 Web 应用程序中,因此这正在成为一个瓶颈。

【问题讨论】:

  • 您已经实施了什么来实现这一目标?

标签: java spring jackson gson


【解决方案1】:

如果你不愿意解析数据,我认为你应该使用Java.io.RandomAccessFile。该包将允许您查找文件的末尾(或末尾前的 1),并写入新数据。请记住,作者会覆盖字符串,它不会插入它们,因此假设您要添加"key": property,,您需要记住插入,"key": property}

【讨论】:

  • 谢谢,但java.io.RandomAccessFile 会很慢。 ObjectMapper 会比 RandomAccessFile 相对快。
  • DataInputStreamDataOutputStream 将比 RandomAccessFile 快得多。但是,我使用 BufferedReaderBufferedWriter 创建了一个临时文件并在我有一个临时文件后移动。
  • @Mansoor 不,RandomAccessFile 不会很慢,因为它允许从最后开始寻找大文件,并且还提供了一种使用特定位置的文件句柄打开文件输出流的方法; 并不重要输入文件有多大。 ObjectMapper 会更慢,因为它是面向输入的,并且它至少需要逐个解析令牌,并且它还需要在同一个文件中的源文件和目标文件(其中大部分工作是复制令牌,而不是附加) .
  • @Carson 我希望我能再次支持您的答案,因为我非常感谢通过打开不需要解析整个文件的 RandomAccessFile 实例来解析给定文件末尾的想法.
【解决方案2】:

我正在考虑从输入流到输出流的全功能流式传输(因此从一开始就按令牌读取和写入令牌),但是既然您提到您可以访问文件,@Carson 的建议确实比我最初的想法好。我只是进一步发展了这个想法:

public final class JsonAppender
        extends Writer {

    private final BufferedWriter writer;
    private final char terminator;

    private boolean isAboutToWrite = true;

    private JsonAppender(final BufferedWriter writer, final char terminator) {
        this.writer = writer;
        this.terminator = terminator;
    }

    public static Writer appendAtEnd(final RandomAccessFile randomAccessFile)
            throws IOException {
        long pos = randomAccessFile.length() - 1;
        char terminator = '\u0000';
        outer_whitespace:
        for ( ; pos >= 0; pos-- ) {
            randomAccessFile.seek(pos);
            final char ch = (char) randomAccessFile.readByte();
            switch ( ch ) {
// @formatter:off
            case ' ': case '\r': case '\n': case '\t':
// @formatter:on
                continue;
// @formatter:off
            case ']': case '}':
// @formatter:on
                terminator = ch;
                break outer_whitespace;
            default:
                throw new IOException("Unexpected " + ch + " at " + pos);
            }
        }
        if ( pos < 0 ) {
            throw new IOException("No object or array begin found");
        }
        inner_whitespace:
        for ( pos -= 1; pos >= 0; pos-- ) {
            randomAccessFile.seek(pos);
            final char ch = (char) randomAccessFile.readByte();
            switch ( ch ) {
// @formatter:off
            case ' ': case '\r': case '\n': case '\t':
// @formatter:on
                continue;
// @formatter:off
            case '}': case ']':
            case '\"':
            case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
            case 'e': // for both true and false
            case 'l': // for null
// @formatter:on
                break inner_whitespace;
            default:
                throw new IOException("Unexpected " + ch + " at " + pos);
            }
        }
        return new JsonAppender(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(randomAccessFile.getFD()))), terminator);
    }

    @Override
    public void write(final char[] buffer, final int offset, final int length)
            throws IOException {
        if ( isAboutToWrite ) {
            isAboutToWrite = false;
            writer.write(',');
        }
        writer.write(buffer, offset, length);
    }

    @Override
    public void flush()
            throws IOException {
        writer.flush();
    }

    @Override
    public void close()
            throws IOException {
        writer.write(terminator);
        writer.close();
    }

}

还有一个示例测试(假设测试在秒表友好的环境中运行,例如 IntelliJ IDEA -- 见下文):

public final class JsonAppenderTest {

    @RepeatedTest(10)
    public void testAppendAtEnd()
            throws IOException {
        try ( final RandomAccessFile randomAccessFile = new RandomAccessFile(LARGE_JSON_PATH, "rw");
                final Writer writer = JsonAppender.appendAtEnd(randomAccessFile) ) {
            final String json = new JSONObject(ImmutableMap.of("foo", "bar")).toString();
            CharStreams.copy(new StringReader(json), writer);
        }
    }

}

它是如何工作的:附加写入器首先从给定文件的末尾开始尝试跳过可能的空格,然后尝试检测终止字符(}],取决于文档) ,然后它尝试检测最后一个值以在之后附加一个新值(可能有空格)。之后,一旦找到写入位置,它只会为已配置为在该位置写入的给定文件描述符创建一个缓冲读取器。并且一旦 writer 关闭,终止字符将写入文件输出流,然后底层流也将关闭。

这里是连续 10 次附加的示例结果(测试在适当的基准测试方面写得很差,但这在这里已经足够好了),输入文件大小约为 24 MB(比你的大三倍):

  • ~30..40ms 用于第一次测试运行(预热 JVM 等)
  • 从第 2 次到第 10 次每次测试运行约 1..2 毫秒
  • 总共 10 次测试运行约 40 毫秒

运行您的解决方案每次测试运行大约产生约 500 毫秒(总共 10 次测试运行大约需要 5 秒)。另请注意,您的解决方案在每次运行时都会创建一个新的大文件,因此非常耗费资源且不是最优的(随着输入文件大小的增长甚至会更慢),而使用 RandomAccessFile 进行一些低级解析非常快且仅追加必要的数据到最后。

再次感谢 Carson 的好主意!

【讨论】:

  • 谢谢毛毛。我很欣赏这些努力。尽管我很喜欢 Carson 的想法,但它并不适合我的直接用例。正如您正确提到的,对于成熟的流媒体解决方案,RandomAccessFile 会更好。
  • 我非常清楚在每个 addInt 上创建一个副本。但我会执行一次这个操作。所以,它有效。但我正在制作一个库,所以我会重新构建它。
  • 您是否有机会验证形成的 JSON 是否正确?
  • {{"foo":1},{"foo":1},{"foo":1},{"foo":1},{"foo":1},{"foo":1},{"foo":1},{"foo":1},{"foo":1},{"foo":1}} 这是它生成的 JSON。
  • @Mansoor 另外,如果您要编写对象属性,请使用write 方法编写属性名称和值。为简单起见,上面的示例仅用于数组。 appender 是愚蠢的,它只检测从哪里开始写入过程,然后与您的addInt 类似地工作。编写一个好的向后解析器显然很耗时,而且显然超出了问题的范围。
【解决方案3】:

围绕@fluffy 和@Carson 的答案创建一个包装器。


public class JsonStream extends Writer {
    private final BufferedWriter writer;
    private final char terminator;

    private boolean isAboutToWrite = true;

    private JsonStream(final BufferedWriter writer, final char terminator) {
        this.writer = writer;
        this.terminator = terminator;
    }

    public static Writer appendAtEnd(final RandomAccessFile randomAccessFile)
            throws IOException {
        long pos = randomAccessFile.length() - 1;
        char terminator = '\u0000';
        outer_whitespace:
        for ( ; pos >= 0; pos-- ) {
            randomAccessFile.seek(pos);
            final char ch = (char) randomAccessFile.readByte();
            switch ( ch ) {
// @formatter:off
                case ' ': case '\r': case '\n': case '\t':
// @formatter:on
                    continue;
// @formatter:off
                case ']': case '}':
// @formatter:on
                    terminator = ch;
                    break outer_whitespace;
                default:
                    throw new IOException("Unexpected " + ch + " at " + pos);
            }
        }
        if ( pos < 0 ) {
            throw new IOException("No object or array begin found");
        }
        inner_whitespace:
        for ( pos -= 1; pos >= 0; pos-- ) {
            randomAccessFile.seek(pos);
            final char ch = (char) randomAccessFile.readByte();
            switch ( ch ) {
// @formatter:off
                case ' ': case '\r': case '\n': case '\t':
// @formatter:on
                    continue;
// @formatter:off
                case '}': case ']':
                case '\"':
                case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
                case 'e': // for both true and false
                case 'l': // for null
// @formatter:on
                    break inner_whitespace;
                default:
                    throw new IOException("Unexpected " + ch + " at " + pos);
            }
        }
        return new JsonStream(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(randomAccessFile.getFD()))), terminator);
    }

    @Override
    public void write(final char[] buffer, final int offset, final int length)
            throws IOException {
        if ( isAboutToWrite ) {
            isAboutToWrite = false;
            writer.write(',');
        }
        writer.write(buffer, offset, length);
    }

    @Override
    public void flush()
            throws IOException {
        writer.flush();
    }

    @Override
    public void close()
            throws IOException {
        writer.write(terminator);
        writer.close();
    }

}



public class JsonStreamAppender extends Writer {

    final File file;
    final Map<String,Object> map;
    final Writer writer;
    public JsonStreamAppender(File file) throws IOException {
        this.file=file;
        map=new LinkedHashMap<>();
        final RandomAccessFile randomAccessFile = new RandomAccessFile(file.getAbsolutePath(), "rw");
        writer= JsonStream.appendAtEnd(randomAccessFile);
    }

    public void add(String key,int value) throws IOException {
        map.put(key,value);
        this.append();
    }



    private void append() throws IOException {
        final String json = new JSONObject(ImmutableMap.copyOf(map)).toString();
        CharStreams.copy(new StringReader(json.substring(1,json.length()-1)), writer);
    }

    /**
     * Writes a portion of an array of characters.
     *
     * @param cbuf Array of characters
     * @param off  Offset from which to start writing characters
     * @param len  Number of characters to write
     * @throws IOException If an I/O error occurs
     */
    @Override
    public void write(char[] cbuf, int off, int len) throws IOException {
        writer.write(cbuf,off,len);
    }

    /**
     * Flushes the stream.  If the stream has saved any characters from the
     * various write() methods in a buffer, write them immediately to their
     * intended destination.  Then, if that destination is another character or
     * byte stream, flush it.  Thus one flush() invocation will flush all the
     * buffers in a chain of Writers and OutputStreams.
     *
     * <p> If the intended destination of this stream is an abstraction provided
     * by the underlying operating system, for example a file, then flushing the
     * stream guarantees only that bytes previously written to the stream are
     * passed to the operating system for writing; it does not guarantee that
     * they are actually written to a physical device such as a disk drive.
     *
     * @throws IOException If an I/O error occurs
     */
    @Override
    public void flush() throws IOException {
        writer.flush();
    }

    /**
     * Closes the stream, flushing it first. Once the stream has been closed,
     * further write() or flush() invocations will cause an IOException to be
     * thrown. Closing a previously closed stream has no effect.
     *
     * @throws IOException If an I/O error occurs
     */
    @Override
    public void close() throws IOException {
        writer.close();
    }
}


测试用例

    @RepeatedTest(10)
    public void testAppendAtEndAppender(){
        try(JsonStreamAppender jsonStreamAppender=new JsonStreamAppender(new File(LARGE_JSON_PATH))){
            jsonStreamAppender.add("operationalLife",-1);
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

【讨论】:

    猜你喜欢
    • 2012-09-01
    • 2017-12-14
    • 1970-01-01
    • 2015-09-13
    • 1970-01-01
    • 1970-01-01
    • 2023-04-06
    • 2013-01-20
    相关资源
    最近更新 更多