Gson解析json时出现Expected a string but was BEGIN_ARRAY异常,由此发现Gson和FastJson区别

本文通过一个实例对比了Gson与Fastjson在解析JSON数据时的不同表现,特别是当期望的数据类型为字符串而实际类型为数组时,Gson会抛出异常,而Fastjson则能正确解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

用Gson解析一段json,想把一个属性定义成String,来接收所有的数据类型,拿到解析出来的String再做处理,结果遇到这个异常. 

这个异常大家应该都遇到过,就是我们定义的对象的某个属性是string类型的,但json中确是Array类型的.

 这是因为:Gson中,关键字后面出现""引起来的内容将会被只认为是STRING,“{}”只被认为是类,“[]”只被认为是List,这个几乎是强制性的。

 就是说如果你的实体预计是获取String的变量,但是关键字后面对应的却出现了“{”或“[”,那么这个转换将被认为是错误的,抛出异常。

想到以前用的FastJson好像是不存在这种问题的,于是做了测试.

@Test
    public void jsonTest(){
        // json文件
        File file=new File("C://data.json");
        // 文件转String
        String data=readToString(file.getAbsolutePath());

        // fastjson解析
        useFastJson(data);
        // Gson解析
        useGson(data);
    }

    /**
     * 用fastjson解析
     * @param data
     */
    private void useFastJson(String data){
        Article article= JSON.parseObject(data, Article.class);
        System.out.println(article.getDetail());
    }

    /**
     * 用Gosn 解析
     * @param data
     */
    private void useGson(String data){
        Article article= new Gson().fromJson(data,Article.class);
        System.out.println(article.getDetail());
    }

/**
     * 文件中获取String
     * @param fileName
     * @return
     */
    public static String readToString(String fileName) {
        String encoding = "UTF-8";
        File file = new File(fileName);
        Long filelength = file.length();
        byte[] filecontent = new byte[filelength.intValue()];
        try {
            FileInputStream in = new FileInputStream(file);
            in.read(filecontent);
            in.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            return new String(filecontent, encoding);
        } catch (UnsupportedEncodingException e) {
            System.err.println("The OS does not support " + encoding);
            e.printStackTrace();
            return null;
        }
    }
结果:
[{"catalog":"环境搭建","article":[{"read":1,"column":9,"id":2,"title":"IntelliJIDEA环境搭建","slug":"static/kotlin_article/0_1.html"},{"read":1,"column":9,"id":3,"title":"Eclipse环境搭建","slug":"static/kotlin_article/0_2.html"},{"read":1,"column":9,"id":4,"title":"使用命令行编译","slug":"static/kotlin_article/0_3.html"},{"read":1,"column":9,"id":5,"title":"Android环境搭建","slug":"static/kotlin_article/0_4.html"}]}]

com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 2 column 16 path $.detailcom.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 2 column 16 path $.detailat com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:224)at com.google.gson.Gson.fromJson(Gson.java:888)at com.google.gson.Gson.fromJson(Gson.java:853)at com.google.gson.Gson.fromJson(Gson.java:802)at com.google.gson.Gson.fromJson(Gson.java:774)at per.wsj.myapplication.ExampleUnitTest.useGson(ExampleUnitTest.java:60)at per.wsj.myapplication.ExampleUnitTest.jsonTest(ExampleUnitTest.java:43)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)

果然Fastjson是可以的,好坑啊 

 

 

 

<think>我们面对的问题是:在使用`ObjectMapper`解析JSON字符串,出现高CPU内存占用,并且长间无响应。根据引用[1]引用[2]的背景,这种情况可能发生在高并发或大数据量的场景下,尤其是在私有化部署环境中,当处理大JSON数据,传统的解析方式(如一次性加载整个JSON到内存)会导致内存溢出或CPU过载。 ### 问题分析 1. **内存占用高**:一次性将整个JSON字符串加载到内存中,如果JSON非常大(例如几百MB或GB级),将直接导致JVM堆内存压力增大,甚至触发Full GC,进而引起长间停顿。 2. **CPU占用高**:解析JSON解析器需要构建完整的对象树,这个过程涉及大量的对象创建、类型转换树遍历操作,会持续消耗CPU资源。 3. **长间无响应**:由于内存CPU的过度消耗,系统可能因为频繁的垃圾回收(尤其是Full GC)而出现长间停顿,或者解析过程本身耗过长。 ### 解决方案 针对上述问题,我们可以采取以下策略: #### 1. 使用流式解析(Streaming API) 流式解析(如Jackson的`JsonParser`)逐令牌(token)处理JSON,不需要一次性将整个文档加载到内存中,从而显著降低内存占用。同,由于不需要构建完整的对象树,CPU消耗也会降低。 **示例代码:** ```java import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import java.io.StringReader; public void parseLargeJson(String jsonStr) throws Exception { JsonFactory factory = new JsonFactory(); try (JsonParser parser = factory.createParser(new StringReader(jsonStr))) { JsonToken token; while ((token = parser.nextToken()) != null) { // 根据token类型处理 if (token == JsonToken.FIELD_NAME) { String fieldName = parser.getCurrentName(); token = parser.nextToken(); // 移动到该字段的值 if ("BD_FORMID".equals(fieldName)) { if (token == JsonToken.VALUE_STRING) { String bdFormId = parser.getText(); // 进行验证 if (bdFormId == null) { throw new BusinessException("BD_FORMID不能为空"); } } } // 其他字段处理... } } } } ``` **优点**:内存占用低,适合处理大JSON。 **缺点**:需要手动处理每个字段,代码复杂度高。 #### 2. 使用增量绑定(Incremental Binding) 如果只需要部分数据,可以使用Jackson的`JsonParser`结合`ObjectMapper`进行部分绑定。例如,只解析出`BD_FORMID`字段,然后跳过不需要的部分。 **示例代码:** ```java JsonFactory factory = new JsonFactory(); try (JsonParser parser = factory.createParser(jsonStr)) { // 移动到整个对象的开始 if (parser.nextToken() != JsonToken.START_OBJECT) { throw new IllegalStateException("Expected an object"); } while (parser.nextToken() != JsonToken.END_OBJECT) { String fieldName = parser.getCurrentName(); parser.nextToken(); // 移动到字段值 if ("BD_FORMID".equals(fieldName)) { String bdFormId = parser.getText(); // 验证BD_FORMID if (bdFormId == null) { throw new BusinessException("BD_FORMID不能为空"); } } else { // 跳过不需要的字段值 parser.skipChildren(); } } } ``` **优点**:可以只解析关心的字段,减少处理量。 **缺点**:仍然需要手动控制解析流程。 #### 3. 异步解析JSON解析任务放入单独的线程池中处理,避免阻塞主线程(如HTTP请求线程)。这样即使解析,也不会导致整个服务无响应。 **示例代码:** ```java // 使用CompletableFuture进行异步解析 CompletableFuture<JSONObject> future = CompletableFuture.supplyAsync(() -> { try { return objectMapper.readValue(jsonStr, JSONObject.class); } catch (JsonProcessingException e) { throw new BusinessException("解析失败"); } }, executor); // 使用自定义的线程池 // 然后可以通过future.get()获取结果(注意设置超)或使用回调 ``` **注意**:异步解析并没有减少解析本身的资源消耗,但可以避免阻塞,提高系统的整体吞吐量。同,要控制并发解析任务的数量,避免线程池内存过载。 #### 4. 优化JVM配置 根据引用[1],对于大内存JVM应用(堆内存≥100GB),需要特别关注垃圾回收的配置。例如: - 使用G1垃圾回收器,并设置合适的Region大小。 - 调整新生代老年代的比例,避免频繁Full GC。 - 设置合理的堆大小,避免内存溢出。 #### 5. 使用更高效的JSON库 考虑使用性能更高、内存占用更低的JSON库,例如: - **Jackson**:流式API树模型都很高效。 - **Fastjson2**:Fastjson的升级版,性能有所提升,但需注意稳定性。 - **Jsoniter**:号称速度非常快,且支持流式解析。 ### 总结 针对大JSON解析导致的高CPU内存占用问题,推荐采用流式解析(如Jackson Streaming API)或增量绑定,并结合异步处理来避免阻塞。同,根据应用场景调整JVM参数,选择高效的JSON解析库。在生产环境中,建议对JSON大小进行限制,避免单个请求消耗过多资源。 ### 相关问题 1. 如何用Jackson流式API处理嵌套复杂的JSON结构? 2. 在异步解析JSON,如何避免线程池资源耗尽? 3. 有哪些JVM参数可以优化大JSON解析的垃圾回收?</think>### 解析JSON高CPU/内存占用问题分析与解决方案 #### 问题诊断 使用`ObjectMapper`解析JSON出现高资源占用的核心原因: 1. **全量加载**:`parseObject()`会将整个JSON加载到内存构建对象树,大JSON直接耗尽内存[^1] 2. **对象创建开销**:每个JSON节点都创建Java对象,产生大量临对象增加GC压力 3. **阻塞式解析**:同步解析阻塞请求线程,导致线程堆积[^2] 4. **嵌套结构**:深层嵌套JSON导致递归解析消耗大量栈空间 --- ### 解决方案 #### 1. 流式解析(推荐) 使用Jackson的`JsonParser`逐令牌处理,内存恒定: ```java public JSONObject streamParse(String jsonStr) throws IOException { JsonFactory factory = new JsonFactory(); try (JsonParser parser = factory.createParser(jsonStr)) { JsonToken token; while ((token = parser.nextToken()) != null) { if (token == JsonToken.FIELD_NAME) { String field = parser.getCurrentName(); if ("BD_FORMID".equals(field)) { parser.nextToken(); String value = parser.getText(); // 字段验证逻辑 } // 其他字段处理... } } } } ``` **优势**:内存占用$O(1)$,支持GB级JSON[^3] #### 2. 增量解析 结合`JsonParser``ObjectMapper`实现部分绑定: ```java JsonParser parser = new JsonFactory().createParser(jsonStr); MyDTO dto = null; while (parser.nextToken() != JsonToken.END_OBJECT) { String field = parser.getCurrentName(); if ("criticalField".equals(field)) { dto = objectMapper.readValue(parser, MyDTO.class); break; // 获取关键字段后退出 } } ``` #### 3. 异步处理 分离解析与业务逻辑,避免阻塞请求线程: ```java CompletableFuture.supplyAsync(() -> { try { return objectMapper.readValue(jsonStr, MyDTO.class); } catch (JsonProcessingException e) { throw new RuntimeException(e); } }, executor).thenAccept(this::processDTO); ``` #### 4. 内存优化配置 ```properties # JVM参数调整(适合≥32GB堆) -Xms24g -Xmx24g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=35 ``` #### 5. 替代JSON库比较 | 库 | 内存效率 | 流式支持 | 适用场景 | |------------|---------|---------|-----------------| | Jackson | ★★★★☆ | ✅ | 通用大JSON处理 | | Jsoniter | ★★★★★ | ✅ | 极致性能场景 | | Gson | ★★☆☆☆ | ❌ | 小JSON简单解析 | --- ### 最佳实践组合 1. **预处理检查**: ```java if (jsonStr.length() > 10_000_000) { // 10MB阈值 return streamParse(jsonStr); // 切换流式解析 } ``` 2. **内存监控**:集成Micrometer监控堆内存使用 3. **请求限流**:使用Guava RateLimiter控制大请求并发 ```java RateLimiter limiter = RateLimiter.create(5); // 5req/s limiter.acquire(); ``` > **关键提示**:对于持续增长的JSON数据,建议采用Redis或MongoDB替代直接解析[^3],其内置的二进制JSON处理效率提升$3\times$以上。 --- ### 相关问题 1. 如何监控Jackson解析过程中的内存使用变化? 2. 使用Redis处理大JSON有哪些最佳实践? 3. 流式解析在处理嵌套JSON数组有哪些注意事项? 4. 如何为JSON解析任务配置合适的线程池参数? [^1]: 大内存JVM应用的内存溢出问题诊断方案 [^2]: 高并发场景下服务器资源紧张导致响应延迟 [^3]: NoSQL数据库在处理JSON数据的性能优势
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值