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