首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Jackson Streaming API 實戰:高性能 JSON 讀寫的零拷貝之道

     編輯:Java 相關知識

為什麼選擇 Streaming API?

Jackson 提供三種 JSON 處理模式:

  • Tree Model (JsonNode):整棵樹載入內存,靈活但記憶體佔用大。
  • Data Binding (ObjectMapper):POJO 自動映射,開發效率高,但需完整讀入內存。
  • Streaming API (JsonParser/JsonGenerator):事件驅動、單向流式、常數內存,適合超大 JSON、高吞吐、低延遲場景。

本文聚焦 Streaming API 核心用法與實戰技巧。


一、寫入:JsonGenerator 詳解

1. 創建與配置

JsonFactory factory = new JsonFactory();
// 可選配置
factory.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); // 手動關閉流
factory.configure(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS, true); // 長整數防精度丟失

// 輸出目標:File, OutputStream, Writer
JsonGenerator gen = factory.createJsonGenerator(new File("output.json"), JsonEncoding.UTF8);
// 或者 pretty print
gen.setPrettyPrinter(new DefaultPrettyPrinter());

2. 核心寫入方法

方法用途對應 JSON
writeStartObject() / writeEndObject()對象開始/結束{ ... }
writeStartArray() / writeEndArray()數組開始/結束[ ... ]
writeFieldName(String)寫入鍵名"name":
writeStringField(String, String)鍵值對快捷寫入"k":"v"
writeNumberField(String, int/long/double)數值字段"age":21
writeBooleanField(String, boolean)布爾字段"vip":true
writeNullField(String)空值字段"k":null
writeString/Number/Boolean/Null()直接寫值(需在數組或根層級)值

3. 完整寫入範例

public void writeStudent(JsonGenerator gen) throws IOException {
    gen.writeStartObject();                    // {
    gen.writeStringField("name", "張三");       //   "name": "張三"
    gen.writeNumberField("age", 20);           //   "age": 20
    gen.writeBooleanField("active", true);     //   "active": true
    
    gen.writeFieldName("courses");             //   "courses":
    gen.writeStartArray();                     //   [
    gen.writeString("數學");                    //     "數學"
    gen.writeString("英語");                    //     "英語"
    gen.writeEndArray();                       //   ]
    
    gen.writeFieldName("address");             //   "address":
    gen.writeStartObject();                    //   {
    gen.writeStringField("city", "北京");       //     "city": "北京"
    gen.writeEndObject();                      //   }
    gen.writeEndObject();                      // }
}

關鍵點:嚴格遵循「開始-內容-結束」配對,否則拋出 JsonGenerationException。調用 gen.close() 或 gen.flush() 確保緩衝區寫入。


二、讀取:JsonParser 詳解

1. 核心事件驅動模型

JsonParser.nextToken() 返回下一個 JsonToken 枚舉值:

START_OBJECT ('{'), END_OBJECT ('}'),
START_ARRAY ('['), END_ARRAY (']'),
FIELD_NAME (鍵名), VALUE_STRING, VALUE_NUMBER_INT, VALUE_NUMBER_FLOAT,
VALUE_TRUE, VALUE_FALSE, VALUE_NULL, VALUE_EMBEDDED_OBJECT

2. 遍歷解析模板

JsonParser parser = factory.createJsonParser(new File("input.json"));
while (parser.nextToken() != JsonToken.END_OBJECT) { // 根對象結束停止
    JsonToken token = parser.currentToken();
    if (token == JsonToken.FIELD_NAME) {
        String field = parser.currentName();
        parser.nextToken(); // 移動到值
        switch (field) {
            case "name": 
                String name = parser.getText(); break;
            case "age":
                int age = parser.getIntValue(); break;
            case "active":
                boolean active = parser.getBooleanValue(); break;
            case "courses":
                List courses = new ArrayList<>();
                while (parser.nextToken() != JsonToken.END_ARRAY) {
                    courses.add(parser.getText());
                }
                break;
            case "address":
                // 嵌套對象遞歸或繼續循環
                while (parser.nextToken() != JsonToken.END_OBJECT) {
                    if (parser.currentToken() == JsonToken.FIELD_NAME) {
                        String subField = parser.currentName();
                        parser.nextToken();
                        if ("city".equals(subField)) city = parser.getText();
                    }
                }
                break;
        }
    }
}

3. 進階技巧:跳過不關心的子樹

if ("hugeArray".equals(field)) {
    parser.skipChildren(); // 直接跳過整個數組/對象,極大提升性能
}

4. 數值類型安全獲取

// 避免精度丟失
BigDecimal price = parser.getDecimalValue(); // 推薦金額用
long id = parser.getLongValue();
double ratio = parser.getDoubleValue();

三、混合模式:Streaming + Data Binding

大數組中每個元素映射為 POJO,既保持流式內存優勢,又享受自動映射便利:

// JSON: { "users": [ {...}, {...}, ... ] }
parser.nextToken(); // START_OBJECT
while (parser.nextToken() != JsonToken.END_OBJECT) {
    if (parser.currentToken() == JsonToken.FIELD_NAME && "users".equals(parser.currentName())) {
        parser.nextToken(); // START_ARRAY
        ObjectMapper mapper = new ObjectMapper(); // 可復用單例
        while (parser.nextToken() != JsonToken.END_ARRAY) {
            // 關鍵:readValue 會消費當前 token 並自動定位到下一個元素起始
            User user = mapper.readValue(parser, User.class);
            processUser(user); // 即時處理,無需全量載入
        }
    }
}

四、性能優化與最佳實踐

場景建議
高頻序列化JsonFactory、ObjectMapper 為線程安全單例複用
大量數值開啟 WRITE_NUMBERS_AS_STRINGS 避免 JS 端精度丟失
網絡傳輸直接寫入 ServletOutputStream,避免中間 ByteArray
異步非阻塞配合 Netty/Servlet 3.1+ AsyncContext,邊解析邊處理
錯誤恢復捕獲 JsonParseException 記錄位置 parser.getTokenLocation()

五、對比總結

維度Tree ModelData BindingStreaming API
內存佔用O(N) 全樹O(N) 對象圖O(1) 常數
編程難度低最低高(手動狀態機)
靈活度最高中(受註解約束)高(完全控制)
適用場景小配置、動態結構常規業務 API超大文件、高吞吐、嵌入式

結語

Jackson Streaming API 是處理海量 JSON 的利器。掌握 JsonGenerator 的「寫入配對」與 JsonParser 的「事件驅動遍歷」,並善用 skipChildren() 与混合模式,即可在內存受限環境下實現極致性能。下次面對 GB 級 JSON 日志、流式 RPC 響應時,請優先考慮它。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved