Java 向JSON添加属性而不在内存中读取的最佳方法

Java 向JSON添加属性而不在内存中读取的最佳方法,java,spring,jackson,gson,Java,Spring,Jackson,Gson,我有一个大的JSON和复杂的文件(~100MB)。我需要在不读取内存的情况下向其添加属性。如果不读取内存中的全部内容,我无法找到附加到JSON的选项 我找不到合适的例子 我能想到的最好办法是使用StreamReader(s)和StreamReader(s)将最后一个}替换为“键”:“值”} JSON是通过遗留应用程序读取的,我们正在将其集成到web应用程序中,因此这将成为一个瓶颈。如果您不愿意解析数据,我认为您应该使用Java.io.RandomAccessFile。该包将允许您查找到文件的末尾

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

我找不到合适的例子

我能想到的最好办法是使用
StreamReader
(s)和
StreamReader
(s)将最后一个
}
替换为
“键”:“值”}


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

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

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

公共最终类JsonAppender
扩展编写器{
私人最终缓冲写作者;
专用最终字符终止符;
私有布尔值isAboutToWrite=true;
私有JsonAppender(最终缓冲写入程序写入程序、最终字符终止程序){
this.writer=writer;
这个。终止符=终止符;
}
公共静态写入程序附件结束(最终随机访问文件随机访问文件)
抛出IOException{
long pos=randomAccessFile.length()-1;
字符终止符='\u0000';
外部空白:
对于(;位置>=0;位置--){
randomAccessFile.seek(pos);
final char ch=(char)randomAccessFile.readByte();
开关(ch){
//@formatter:off
案例“”:案例“\r”:案例“\n”:案例“\t”:
//@formatter:on
继续;
//@formatter:off
案例']':案例'}':
//@formatter:on
终止符=ch;
打破外部空白;
违约:
抛出新IOException(“意外的”+ch+”位于“+pos”);
}
}
如果(位置<0){
抛出新IOException(“未找到对象或数组开始”);
}
内部空白:
对于(位置-=1;位置>=0;位置--){
randomAccessFile.seek(pos);
final char ch=(char)randomAccessFile.readByte();
开关(ch){
//@formatter:off
案例“”:案例“\r”:案例“\n”:案例“\t”:
//@formatter:on
继续;
//@formatter:off
案例“}”:案例“]”:
案例“\”:
案例“0”:案例“1”:案例“2”:案例“3”:案例“4”:案例“5”:案例“6”:案例“7”:案例“8”:案例“9”:
案例“e”://适用于true和false
案例“l”://表示null
//@formatter:on
打破内部空白;
违约:
抛出新IOException(“意外的”+ch+”位于“+pos”);
}
}
返回新的JsonAppender(新的BufferedWriter(新的OutputStreamWriter(新的FileOutputStream(randomAccessFile.getFD())),终止符);
}
@凌驾
公共无效写入(最终字符[]缓冲区、最终整型偏移量、最终整型长度)
抛出IOException{
如果(isAboutToWrite){
isAboutToWrite=false;
writer.write(',');
}
writer.write(缓冲区、偏移量、长度);
}
@凌驾
公共图书馆
抛出IOException{
writer.flush();
}
@凌驾
公众假期结束()
抛出IOException{
writer.write(终结者);
writer.close();
}
}
以及一个示例测试(假设该测试在一个秒表友好的环境中运行,如IntelliJ IDEA——请参阅下一步):

公共最终类JsonAppenderTest{
@重复测试(10)
公共无效测试结束()
抛出IOException{
try(final RandomAccessFile RandomAccessFile=新的RandomAccessFile(大的JSON路径,“rw”);
final Writer Writer=JsonAppender.appendAtEnd(随机访问文件)){
最后一个字符串json=newJSONObject(ImmutableMap.of(“foo”,“bar”))).toString();
copy(新的StringReader(json),writer);
}
}
}
工作原理:追加编写器尝试跳过可能的空白,首先从给定文件的末尾开始,然后尝试检测终止字符(
}
]
,取决于文档),然后尝试检测最后一个值,以便在之后追加新值(带可能的空白)。之后,一旦找到写入位置,它只会为给定的文件描述符创建一个缓冲读取器,该文件描述符已配置为在该位置写入。一旦关闭写入程序,终止字符将写入文件输出流,然后基础流也将关闭

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

  • 第一次测试运行约30..40ms(预热JVM等)
  • 从第2次开始每次测试运行约1..2ms
    
    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();
            }
    
    
        }