gson 将属性添加到JSON而无需在内存中阅读的最佳方法

6kkfgxo0  于 2022-11-06  发布在  其他
关注(0)|答案(3)|浏览(200)

我有一个很大的JSON和复杂的文件(~ 100 MB)。我需要在不阅读内存的情况下向其添加属性。我无法找到一个选项,在不读取内存中的整个内容的情况下追加到JSON。
我找不到任何合适的例子。
我能想到的最好方法是使用StreamReader(s)和StreamReader(s)将最后一个}替换为"key":"value"}
JSON是通过遗留应用程序读取的,我们正在将其集成到Web应用程序中,因此这将成为一个瓶颈。

eni9jsuy

eni9jsuy1#

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

cnjp1d6j

cnjp1d6j2#

我考虑的是从一个输入流到一个输出流的全功能流(因此从一开始就一个令牌一个令牌地阅读),但是既然你提到你可以访问一个文件,@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);
        }
    }

}

工作原理:附加写入程序首先尝试从给定文件的最后开始跳过可能的空格,然后尝试检测终止字符(}],具体取决于文档),然后它尝试检测最后一个值,并在其后追加新值(可能带有空格)。之后一旦找到书写位置,它只是为给定的文件描述符创建一个缓冲的读取器,终止字符被写入文件输出流,然后基础流也被关闭。
下面是一行10个附加的示例结果(测试在适当的基准测试方面写得很差,但在这里已经足够好了),输入文件大小大约是24 MB(比你的文件大三倍):

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

每次运行你的解决方案大约需要500 ms(总共10次运行大约需要5s)。还要注意的是,你的解决方案每次运行都会创建一个新的大文件,因此会非常消耗资源并且不是最优的(并且随着输入文件大小的增加会变得更慢),而使用RandomAccessFile和一些低级解析确实很快,并且只在最后 * 附加 * 必要的数据。
再次感谢Carson的伟大想法!

zbsbpyhn

zbsbpyhn3#

在@fluffy和@卡森的答案周围创建一个 Package 器。
第一个
和测试用例

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

    }

相关问题