如何优化Spring应用中大JSON字符串转JSONObject的性能?
兄弟,你这问题我之前碰过好多次——单请求解析3-5秒还能接受,并行8-10个直接干到60秒,核心原因就是内存过载导致频繁Full GC,再加上你现在的解析流程本身就不够高效。给你几个针对性的优化方案,按优先级从高到低排:
你现在用RestTemplate.exchange(..., String.class)会把整个200MB的JSON一次性加载到内存里,变成一个巨大的String对象。并行8个的话,光是这些字符串就占了1.6GB内存,直接把堆内存撑爆,GC疯狂跑,这就是耗时暴涨的罪魁祸首。
正确的做法是让RestTemplate直接返回输入流,用Jackson流式解析,避免一次性加载整个响应:
// 自定义ResponseExtractor拿到响应的输入流 ResponseExtractor<InputStream> streamExtractor = response -> { InputStream is = response.getBody(); // 别自己关流,RestTemplate会自动处理 return is; }; // 用execute方法获取流,而不是exchange转String InputStream responseStream = restTemplate.execute( yourUrl, HttpMethod.POST, request -> request.getBody().write(postEntity.getBody()), // 这里按你的请求体调整 streamExtractor ); // 用Jackson直接从流解析成JsonNode(比org.json的JSONObject快N倍) JsonNode jsonNode = objectMapper.readTree(responseStream);
流式解析只会把当前处理的JSON节点加载到内存,内存占用直接从200MB降到几MB,并行时的GC压力瞬间就没了。
org.json的JSONObject是个老古董了,它的解析逻辑是基于字符串逐字符扫描,效率极低。而Jackson的JsonNode是原生的树模型,基于字节流解析,性能差了至少一个数量级。
如果业务代码里一定要用JSONObject,可以把JsonNode转过去,但尽量直接用JsonNode——它的API和JSONObject几乎一样:
// JsonNode取值和JSONObject完全类似 String username = jsonNode.get("username").asText(); int age = jsonNode.get("age").asInt(); List<JsonNode> items = jsonNode.findValues("items");
实在要转的话,也别直接转成字符串再解析(那又回到了内存问题),可以用Jackson的ObjectMapper直接转:
// 直接从JsonNode转成org.json的JSONObject(避免生成大字符串) JSONObject jsonObject = objectMapper.convertValue(jsonNode, JSONObject.class);
ObjectMapper是线程安全的,绝对不要每次请求都new一个——每次初始化都会创建一堆序列化器/反序列化器,浪费CPU。把它做成Spring的Bean,全局复用:
@Bean public ObjectMapper objectMapper() { return new ObjectMapper() // 忽略未知字段,减少反射查找的开销 .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // 禁用不必要的日期自动检测,提升解析速度 .configure(MapperFeature.AUTO_DETECT_CREATORS, true) .configure(MapperFeature.AUTO_DETECT_FIELDS, true) // 启用快速失败,遇到JSON语法错误立刻抛出,不用解析完整个文档 .configure(JsonParser.Feature.FAIL_ON_READING_DUP_TREE_KEY, true) // 如果用Java 8+时间API,一定要注册这个模块,避免Jackson用反射处理日期 .registerModule(new JavaTimeModule()); }
并行时如果线程数太多,会导致上下文切换频繁,反而降低性能。对于IO密集型的请求(你的场景是网络请求+解析),线程池的核心线程数建议设为CPU核心数*2,比如4核CPU就设8个核心线程:
ExecutorService executor = new ThreadPoolExecutor( 8, // 核心线程数 16, // 最大线程数 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), // 任务队列 new ThreadFactoryBuilder().setNameFormat("json-parser-%d").build() );
另外,RestTemplate的默认连接池太小,并行请求时会排队等待连接。换成HttpComponentsClientHttpRequestFactory,配置合理的连接数:
HttpClient httpClient = HttpClientBuilder.create() .setMaxConnTotal(20) // 全局最大连接数 .setMaxConnPerRoute(10) // 每个目标服务器的最大连接数 .build(); RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
如果服务器那边能配合,把200MB的大JSON拆成多个小响应(比如按数据分片、分页),每个请求只返回部分数据,这样客户端的内存压力会骤减,并行处理的效率能提升好几倍。比如按ID范围分10个请求,每个返回20MB数据,解析速度会快很多。
按这个顺序优化,并行8-10个请求的耗时应该能降到20秒以内,甚至接近单请求时间的8-10倍(正常并行的开销)。
内容的提问来源于stack exchange,提问作者Prog_G

