处理大型 JSON 行协议(JSON Lines / NDJSON)数据时,Java 中 BufferedReader 配合 Jackson 的逐行解析方案,是很多开发者会优先考虑的做法。核心思路很简单:文件里每一行是一个独立的 JSON 对象,不能把整个文件当成一个大 JSON 一次性解析,而要按行切割后再逐个处理。

要理解,这种逐行解析方案有一个关键前提:数据格式必须严格遵循 JSON Lines 规范,即每一行都是一个完整的 JSON 对象,行与行之间没有逗号分隔,也不需要用数组包裹。
第一步,确认文件格式是不是 JSON Lines
数据格式是基础,如果格式不正确,所有工作都是徒劳。一个典型的 JSON Lines 文件示例如下:
{ "id": 1, "name": "Alice"}{"id": 2, "name": "Bob"}{"id": 3, "name": "Charlie"}
正确示例:每行都是独立且合法的 JSON 对象
{"user":"u1","event":"login","ts":1712345678}
{"user":"u2","event":"logout","ts":1712345689}
{"user":"u3","event":"click","ts":1712345695}
错误示例:整体是 JSON 数组或包含跨行未闭合的对象
[{"id":1},{"id":2}] 或跨行未闭合的 JSON —— 这种格式显然无法使用逐行解析。
用 BufferedReader + Jackson 逐行解析
确认格式无误后,在实际开发中,最推荐的做法是使用 Jackson 的 ObjectMapper。它支持将单个 JSON 字符串直接解析成 Java 对象,整个过程分四步走:
- 使用
Files.newBufferedReader()或new BufferedReader(new FileReader())打开文件 - 循环调用
readLine()逐行读取,并自动跳过空行和纯空白行 - 对每一行非空字符串,调用
ObjectMapper.readValue(line, YourClass.class)进行解析 - 捕获
JsonProcessingException异常以处理格式错误的行(例如记录日志并跳过)
一段高效的代码可以这样写:
ObjectMapper mapper = new ObjectMapper();
try (BufferedReader reader = Files.newBufferedReader(Paths.get("data.jsonl"))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
try {
MyEvent event = mapper.readValue(line, MyEvent.class);
// 处理 event...
} catch (JsonProcessingException e) {
System.err.println("Invalid JSON line: " + line);
}
}
}
处理超大文件时的注意事项
逐行读取方式对内存非常友好,但仍有几个细节会影响稳定性,需要特别留意:
- 设置合适的缓冲区大小:默认的 8KB 缓冲区通常足够;但如果单行内容极长(例如嵌套深、字段多),建议显式指定更大的缓冲区,以避免
readLine()内部频繁扩容带来的性能开销。 - 避免累积行内容:切勿使用
StringBuilder拼接所有行,这相当于将整个文件加载到内存,失去了流式处理的意义。 - 及时释放资源:务必使用 try-with-resources 语句确保
BufferedReader正确关闭,防止文件句柄泄漏。 - 考虑反序列化性能:如果每行数据结构固定,可以预编译
ObjectReader(例如mapper.readerFor(MyEvent.class))来提升解析速度。
替代方案:Gson 或自定义轻量解析
如果你的项目已经使用了 Gson,同样可以轻松实现逐行解析:
Gson gson = new Gson(); MyEvent event = gson.fromJson(line, MyEvent.class);
另一种常见场景是:如果只需要提取个别字段(例如只取 "id" 和 "ts"),而不想引入完整的 POJO 类,可以使用 Jackson 的 JsonNode 或 Gson 的 JsonObject 进行轻量解析:
JsonNode node = mapper.readTree(line);
long id = node.get("id").asLong();
String user = node.get("user").asText();
这种方案省去了编写类定义的过程,灵活性极高,特别适合 ETL(数据提取、转换、加载)或日志分析类场景。
