【Exception】fastjson的toJSONString()导致OOM

线上运行的代码出现OOM问题,经排查发现是由于Fastjson在转换对象为JSON字符串时引发的。Fastjson在处理大量数据时会分配大量内存,可能导致内存溢出。考虑到其安全性和性能问题,建议改用Jackson的writeValueAsString()方法来替代,以避免类似问题。此外,文章还强调了在选择JSON库时应考虑其稳定性和性能。

现象

线上代码跑一段时间后出现了OOM的问题,经过日志的检查,发现异常出现在fastjson的toJSONString()导致的OOM

解决

将toJSONString()方法改成jackson的转字符串方法

ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(obj);

原因

fastjson在转成json字符串的时候会分配一个byte[],当数据量很大的时候,需要分配很大的内存,当分配的内存超过堆内存时,会出现OOM

fastjson确实好用,但是因为之前频繁出现安全问题,加上这次发现的OOM问题,最好还是别用fastjon了,虽然它的api确实简单好用

public static <T> List<T> parseFileData(String typeName, String corepath, String filename, Class<T> clazz) { log.info("【{}】开始解析文件:{}", typeName, filename); String pathname = LOCAL_BASE_PATH + corepath + filename; // 返回集合 List<T> list = new ArrayList<>(); // 读取文件 File file = new File(pathname); // 读取标识 boolean flag = false; // 读取数据 List<Map<String, String>> lineDataList = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new FileReader(file))) { // 数据行 String line; // 字段名称数组 String[] fieldNameArr = new String[0]; // 字段值数组 String[] fieldValueArr; while ((line = br.readLine()) != null) { // 字段数组 String[] fieldArr = line.split(SPLIT_STR , -1); // 读取列名行 if ("op_type".equals(fieldArr[0])) { fieldNameArr = fieldArr; continue; } // 数据行 Map<String, String> lineDataMap = new HashMap<>(fieldNameArr.length); fieldValueArr = fieldArr; for (int i = 0; i < fieldNameArr.length; i++) { lineDataMap.put(fieldNameArr[i], fieldValueArr[i]); } lineDataList.add(lineDataMap); } flag = true; } catch (Exception e) { log.error("【{}】读取文件FileReader异常!", typeName, e); } if (flag) { Vector<T> vector = new Vector<>(); ParserForUtil pfu = new ParserForUtil(); /* 解析读取数据 */ pfu.parserSet(lineDataList, lineDataMap -> { String dataJson = JSON.toJSONString(lineDataMap); T dataObject = JSON.parseObject(dataJson, clazz); vector.add(dataObject); }); list = new ArrayList<>(vector); log.info("【{}】成功解析文件:{}", typeName, filename); } return list; }这个方法什么情况会出现读取的行数缺失
最新发布
11-04
package com.jkt.sjzy.data.batch; import com.jkt.sjzy.data.batch.runner.FetchFromApiRunner; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.scheduling.annotation.EnableScheduling; import java.util.Arrays; /** * Hello world! *git branch -M master */ @SpringBootApplication @EnableScheduling @MapperScan(basePackages = {"com.jkt.sjzy.data.batch.mapper"}) public class BatchDataApplication { public static void main( String[] args ) { ConfigurableApplicationContext context = SpringApplication.run(BatchDataApplication.class, args); FetchFromApiRunner runner = context.getBean(FetchFromApiRunner.class); boolean isFirstExecution = Arrays.asList(args).contains("--first-execution"); runner.fetchFromApi(isFirstExecution); System.exit(0); } } package com.jkt.sjzy.data.batch.handler.savemode; import cn.hutool.core.date.DateUtil; import cn.hutool.db.PageResult; import com.alibaba.fastjson2.JSONObject; import com.jkt.sjzy.data.batch.domain.entities.DataApiConfig; import com.jkt.sjzy.data.batch.domain.entities.DataApiTokenConfig; import com.jkt.sjzy.data.batch.enums.ApiType; import com.jkt.sjzy.data.batch.handler.fetch.FetchDataFactory; import com.jkt.sjzy.data.batch.handler.fetch.IFetchDataHandler; import com.jkt.sjzy.data.batch.handler.token.ITokenHandler; import com.jkt.sjzy.data.batch.handler.token.TokenHandlerFactory; import com.jkt.sjzy.data.batch.handler.write.IWriteDataHandler; import okhttp3.Headers; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; /** * @Author: wangruiming * @Date: 2025/6/26 17:26 */ public abstract class SaveDataTemplate implements ISaveDataHandler { @Override public void saveData(DataApiTokenConfig tokenConfig, DataApiConfig apiConfig, ApiType apiType, String beginDate, String endDate) { IWriteDataHandler writeDataHandler = preStep(apiConfig, apiType); IFetchDataHandler<?> init = FetchDataFactory.init(apiConfig.getFetchDataClazz()); init.fetchData(apiType, tokenConfig, apiConfig, writeDataHandler, beginDate, endDate); } /** * 前置操作步骤 * * @param apiConfig * @param apiType */ public abstract IWriteDataHandler preStep(DataApiConfig apiConfig, ApiType apiType); } package com.jkt.sjzy.data.batch.handler.fetch; import java.lang.reflect.InvocationTargetException; /** * @Author: wangruiming * @Date: 2025/6/26 17:52 */ public class FetchDataFactory { public static IFetchDataHandler<?> init(String clazz){ try { Class<?> aClass = Class.forName(clazz); return (IFetchDataHandler<?>) aClass.getConstructor().newInstance(); } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { throw new RuntimeException(e); } } } package com.jkt.sjzy.data.batch.handler.fetch; import com.jkt.sjzy.data.batch.domain.entities.DataApiConfig; import com.jkt.sjzy.data.batch.domain.entities.DataApiTokenConfig; import com.jkt.sjzy.data.batch.enums.ApiType; import com.jkt.sjzy.data.batch.handler.write.IWriteDataHandler; /** * @Author: wangruiming * @Date: 2025/6/26 17:50 */ public interface IFetchDataHandler<T> { void fetchData(ApiType apiType, DataApiTokenConfig tokenConfig, DataApiConfig config, IWriteDataHandler handler, String beginDate, String endDate); } package com.jkt.sjzy.data.batch.handler.fetch.cck; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.google.common.util.concurrent.RateLimiter; import com.jkt.sjzy.data.batch.domain.dao.cck.CckPageResult; import com.jkt.sjzy.data.batch.domain.entities.DataApiConfig; import com.jkt.sjzy.data.batch.domain.entities.DataApiTokenConfig; import com.jkt.sjzy.data.batch.domain.entities.JobLastRunningLog; import com.jkt.sjzy.data.batch.enums.ApiType; import com.jkt.sjzy.data.batch.exception.TokenExpiredException; import com.jkt.sjzy.data.batch.handler.fetch.IFetchDataHandler; import com.jkt.sjzy.data.batch.handler.token.ITokenHandler; import com.jkt.sjzy.data.batch.handler.token.TokenHandlerFactory; import com.jkt.sjzy.data.batch.handler.write.IWriteDataHandler; import com.jkt.sjzy.data.batch.service.JobLastRunningLogService; import com.jkt.sjzy.data.batch.service.ServiceRegistry; import com.jkt.sjzy.data.batch.util.JsonUtil; import com.jkt.sjzy.data.batch.util.RequestUtil; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import okhttp3.Headers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.core.env.Environment; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import static com.alibaba.fastjson2.JSONObject.parseObject; import static com.jkt.sjzy.data.batch.constant.Constant.*; /** * @Author: wangruiming * @Date: 2025/6/26 17:53 */ @Slf4j public class CckFetchDataHandler<T> implements IFetchDataHandler<T> { private final Environment environment; private final JobLastRunningLogService jobLastRunningLogService; public CckFetchDataHandler(Environment environment, JobLastRunningLogService jobLastRunningLogService) { this.environment = environment; this.jobLastRunningLogService = jobLastRunningLogService; } @Override public void fetchData(ApiType apiType, DataApiTokenConfig tokenConfig, DataApiConfig config, IWriteDataHandler handler, String beginDate, String endDate) { Set<String> idSet = new HashSet<>(); String timeRangeType = getTimeRangeType(); LocalDateTime now = LocalDateTime.now(); int pageNum = 1; //刷新token ITokenHandler tokenHandler = TokenHandlerFactory.getTokenHandler(tokenConfig); if (!tokenHandler.validateToken()) { tokenHandler.refreshToken(); } // 处理分页接口 if (config.getPaged()) { // String beginDate = "2019-01-01"; // String endDate = DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd"); while (true) { List<Object> allData = new ArrayList<>(); CckPageResult pageResult = fetchPagedData(apiType, config, tokenHandler, pageNum, beginDate, endDate); if (pageResult == null || pageResult.getResources().isEmpty()) { break; } for (JSONObject data : pageResult.getResources()) { String id = extractId(data, apiType); if (id != null && idSet.add(id)) { T bean = processDataToBean(data, config, apiType); if (bean != null) { allData.add(bean); } } data = null; } handler.writeData(allData); if (pageNum >= pageResult.getTotalPageNo()) { break; } pageNum++; } } // 处理单日接口 else { LocalDate startDate = LocalDate.of(2025, 1, 1); LocalDate endDateObj = parseDate(endDate); LocalDate beginDateObj = parseDate(beginDate); if (beginDateObj.isBefore(startDate)) { beginDateObj = startDate; } // Iterator<LocalDate> dateIterator = startDate.datesUntil(endDate.plusDays(1)).iterator(); List<LocalDate> allDates = beginDateObj.datesUntil(endDateObj.plusDays(1)).collect(Collectors.toList()); int threadCount = Math.min(Runtime.getRuntime().availableProcessors() * 2, 10); ExecutorService executor = Executors.newFixedThreadPool(threadCount); Semaphore semaphore = new Semaphore(5); // 限制最大并发请求数 RateLimiter rateLimiter = RateLimiter.create(5.0); // 每秒最多5个请求 BlockingQueue<T> resultQueue = new LinkedBlockingQueue<>(); CountDownLatch latch = new CountDownLatch(allDates.size()); // while (dateIterator.hasNext()) { // 提交所有日期的请求任务 for (LocalDate date : allDates) { executor.submit(() -> { try { semaphore.acquire(); rateLimiter.acquire(); // LocalDate currentDate = dateIterator.next(); String dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 获取单日数据 JSONObject data = fetchSingleDayData(apiType, tokenHandler, config, dateStr); if (data != null) { String id = extractId(data, apiType); if (id != null && idSet.add(id)) { T bean = processDataToBean(data, config, apiType); if (bean != null) { resultQueue.put(bean); } data = null; } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { log.error("处理日期 {} 时出错", date, e); } finally { semaphore.release(); latch.countDown(); } }); } try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("等待任务完成时被中断", e); } finally { executor.shutdown(); } List<Object> allData = new ArrayList<>(resultQueue); handler.writeData(allData); } jobLastRunningLogService.saveOrUpdate(new JobLastRunningLog(DigestUtil.md5Hex(config.getTargetTable()), apiType.getDesc(), now, (long) idSet.size())); } /** * 将JSON数据转换为对应的Bean对象 */ @SuppressWarnings("unchecked") private <T> T processDataToBean(JSONObject data, DataApiConfig apiConfig, ApiType apiType) { if (data == null) { return null; } // 转换为小写下划线命名 try { Class<?> clazz = Class.forName(DAO_CLAZZ_PATH.formatted(apiConfig.getProjectCode()) + apiType.getBeanClazz()); JsonUtil.jsonToUnderLineKey(data); return (T) JSONObject.parseObject(data.toJSONString(), clazz); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } /** * 获取单日数据 */ private JSONObject fetchSingleDayData(ApiType apiType, ITokenHandler tokenHandler, DataApiConfig config, String date) { AtomicInteger retryCount = new AtomicInteger(0); while (retryCount.getAndIncrement() < MAX_PAGE_RETRIES) { try { String url = config.getUrl() + "?date=" + date; Headers headers = Headers.of("x-token", tokenHandler.getToken()); String responseBody = RequestUtil.requestRaw("GET", url, headers, ""); return parseSingleResult(responseBody, apiType); } catch (TokenExpiredException e) { tokenHandler.refreshToken(); } catch (Exception e) { if (retryCount.get() < MAX_PAGE_RETRIES) { tokenHandler.sleepWithBackoff(retryCount.get()); } } } return null; } /** * 解析单日结果 */ private JSONObject parseSingleResult(String response, ApiType apiType) { if (StrUtil.isBlank(response)) { return null; } try { JSONObject responseObj = parseObject(response); if (responseObj == null) { return null; } // 检查响应状态码 int ret = responseObj.getIntValue("ret"); if (ret != 0) { log.warn("{}接口返回非成功状态: {}", apiType.getDesc(), ret); return null; } // 解析嵌套的data结构 if (responseObj.containsKey("data")) { JSONObject outerData = responseObj.getJSONObject("data"); if (outerData != null && outerData.containsKey("data")) { return outerData.getJSONObject("data"); } } } catch (Exception e) { log.error("解析{}单日数据失败: {}", apiType.getDesc(), e.getMessage()); } return null; } /** * 获取分页数据 */ private CckPageResult fetchPagedData(ApiType apiType, DataApiConfig config, ITokenHandler tokenHandler, int pageNum, String beginDate, String endDate) { AtomicInteger retryCount = new AtomicInteger(0); while (retryCount.getAndIncrement() < MAX_PAGE_RETRIES) { try { String url = config.getUrl() + "?beginDate=" + beginDate + "&pageNo=" + pageNum + "&pageSize=" + PAGE_SIZE + "&endDate=" + endDate; Headers headers = Headers.of("x-token", tokenHandler.getToken()); String responseBody = RequestUtil.requestRaw("GET", url, headers, ""); return parsePageResult(responseBody, apiType); } catch (TokenExpiredException e) { tokenHandler.refreshToken(); } catch (Exception e) { tokenHandler.sleepWithBackoff(retryCount.get()); } } return null; } /** * 从数据中提取唯一标识 */ private String extractId(JSONObject data, ApiType apiType) { if (data == null) { return null; } // 根据不同接口类型提取不同的ID字段 switch (apiType) { case ACTIVITY: return data.getString("ID"); case EVENT: return data.getString("ID"); case TRAINING_VIDEO: return data.getString("ID"); case TRAINING_TEXT: return data.getString("ID"); case EVENT_INFO: return data.getString("DATE"); case PASSENGER_INFO: return data.getString("DATETIME"); case ACTIVITY_INFO: return data.getString("DATE"); default: return data.getString("ID"); } } //日期格式参数化 private static LocalDate parseDate(String dateStr) { return dateStr.contains("T") ? LocalDateTime.parse(dateStr).toLocalDate() : LocalDate.parse(dateStr); } private String getTimeRangeType() { return environment.getProperty("data.fetch.timeRangeType", "NORMAL"); } /** * 解析分页结果 */ private CckPageResult parsePageResult(String response, ApiType apiType) { if (StrUtil.isBlank(response)) { return new CckPageResult(new ArrayList<>(), 0, 1); } List<JSONObject> dataList = new ArrayList<>(); int totalCount = 0; int totalPage = 1; try { JSONObject responseObj = parseObject(response); if (responseObj == null) { return new CckPageResult(dataList, totalCount, totalPage); } // 嵌套的data.data层 JSONObject dataObj = responseObj.getJSONObject("data").getJSONObject("data"); totalCount = dataObj.getIntValue("count"); int pageSize = dataObj.getIntValue("pageSize"); totalPage = (totalCount + pageSize - 1) / pageSize; JSONArray dataArray = dataObj.getJSONArray("list"); if (dataArray != null) { for (int i = 0; i < dataArray.size(); i++) { JSONObject data = dataArray.getJSONObject(i); if (data != null) { dataList.add(data); } } } } catch (Exception e) { log.error("解析{}分页数据失败: {}", apiType.getDesc(), e.getMessage()); } return new CckPageResult(dataList, totalCount, totalPage); } }
07-09
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值