作为一名写了八年 Java 的“老油条”,JSON 解析几乎是日常开发中绕不开的坎——从刚入行时用 JSONObject 手动 get 字符串的笨拙(还记得第一次解析嵌套 JSON 写了三层 getJSONObject 吗?),到现在封装通用工具类应对复杂场景(泛型、大文件、自定义格式一个不落),踩过的坑能编一本《JSON 排坑手册》。

这篇文章不聊虚的,纯实战视角:哪些业务场景最常遇到 JSON 解析?不同库该怎么选才能少踩坑?核心代码如何写得既稳定又优雅?甚至会告诉你“为什么有些坑别人踩过你还会踩”——毕竟,经验的价值不是避免踩坑,而是踩坑后能快速爬出来。

一、先聊透:哪些业务场景会高频用到 JSON 解析?

八年里,我在电商、金融、政务系统都待过,JSON 解析的场景总结下来就三类,几乎覆盖 80% 的业务需求。每类场景都有对应的“坑点预警”,都是真金白银踩出来的教训。

1. 配置文件解析:从“硬编码”到“动态配置”的必经之路

早期项目爱用 XML 做配置(比如 Spring 的 applicationContext.xml),后来基本被 JSON 取代——原因很简单:轻量(同样的配置,JSON 比 XML 少 30% 冗余字符)、易读(前后端都能看懂)、修改无需重启(动态加载配置时 JSON 解析更高效)。

高频业务案例
  • 系统初始化参数:电商系统的支付渠道配置(payment-config.json),包含不同支付方式的 API 地址、超时时间、鉴权密钥,启动时加载到内存,避免硬编码改完要重新部署。
    示例 JSON 结构:
    {
      "paymentChannels": [
        {
          "channelCode": "ALIPAY",
          "apiUrl": "https://openapi.alipay.com/gateway.do",
          "timeoutMs": 3000,
          "appId": "20240801000001",
          "privateKey": "MIIEvQIBADANBgkqhkiG9w0BAQE..."
        },
        {
          "channelCode": "WECHAT",
          "apiUrl": "https://api.mch.weixin.qq.com/pay/unifiedorder",
          "timeoutMs": 5000,
          "mchId": "1234567890"
        }
      ]
    }
    
  • 规则引擎动态规则:金融风控系统的“交易拦截规则”(risk-rule.json),比如“单日单笔金额>10万且非常用设备,触发二次验证”,业务人员可在后台修改规则,系统实时加载解析。
  • 多环境配置隔离:开发/测试/生产环境的数据库地址、Redis 配置不同,用 config-dev.json/config-prod.json 区分,打包时通过环境变量选择对应的配置文件。
坑点预警
  • 配置文件路径问题:新手常写绝对路径(D:/config/payment.json),部署到 Linux 服务器直接报错,正确做法是从 classpath 读取(用 ClassLoader.getResourceAsStream)。
  • 配置变更未生效:修改配置文件后没触发重新解析,需要加“配置监听”(比如用 WatchService 监控文件变化,变更后重新加载)。

2. 接口数据交互:前后端、跨系统数据流转的“通用语言”

这是 JSON 解析最频繁的场景——几乎所有接口都绕不开“接收 JSON → 解析为对象”或“对象 → 生成 JSON 返回”。

高频业务案例
  • 消费第三方接口:调用高德地图 API 获取地址解析结果,返回的 JSON 包含省、市、区、街道等嵌套字段,需要解析为 AddressResult 对象才能用。
    示例返回 JSON:
    {
      "status": "1",
      "regeocode": {
        "addressComponent": {
          "province": "北京市",
          "city": "北京市",
          "district": "朝阳区",
          "township": "望京街道"
        },
        "formatted_address": "北京市朝阳区望京街道望京SOHO"
      }
    }
    
  • 接收前端复杂表单:电商订单提交时,前端传的 JSON 包含“订单基本信息+商品列表+收货地址+优惠券”,是多层嵌套结构(Order 包含 List<OrderItem>Address)。
  • 批量数据导入/导出:运营需要批量导入 1000 条商品信息,上传 JSON 文件后解析为 List<Product>;用户需要导出自己的订单记录,后端将 List<Order> 生成 JSON 文件供下载。
坑点预警
  • 字段名不匹配:前端传 user_id,Java 类用 userId,没配置映射直接解析为 null(新手常犯)。
  • 嵌套泛型解析失败:比如解析 List<Map<String, User>>,直接用 List.class 会导致泛型擦除,拿到的是 List<Map> 而非 List<Map<String, User>>
  • 第三方接口返回格式不规范:比如同个字段有时是 String(“100”)有时是 Integer(100),解析时会报 JsonParseException

3. 日志文件分析:分布式系统排障的“关键钥匙”

分布式系统中,日志不再是简单的文本(比如 System.out.println),而是结构化的 JSON 格式——方便用 ELK 栈(Elasticsearch + Logstash + Kibana)收集、解析、可视化,快速定位问题。

高频业务案例
  • 用户行为埋点日志:APP 埋点日志(user-behavior-20240819.json)记录用户点击、页面停留、跳转行为,离线分析时需要解析出“用户 ID+行为类型+时间戳+页面路径”,统计用户转化率。
    示例日志 JSON:
    {
      "userId": "10086",
      "behaviorType": "CLICK",
      "timestamp": 1724006400000,
      "pagePath": "/order/pay",
      "deviceId": "android_123456",
      "extInfo": {
        "buttonName": "确认支付",
        "stayTimeMs": 500
      }
    }
    
  • 系统运行监控日志:微服务的 JVM 监控日志,包含堆内存使用、GC 次数、接口响应时间,解析后存入 Prometheus 做监控告警。
  • 链路追踪日志:分布式链路追踪(如 SkyWalking)的日志,用 JSON 记录“TraceID+SpanID+服务名+调用耗时”,通过解析还原整个调用链路,定位慢接口。
坑点预警
  • 大文件 OOM:1GB 的日志文件直接加载到内存解析,瞬间触发 OutOfMemoryError(我见过同事这么干,服务器直接挂了)。
  • 日志格式不统一:部分日志因代码异常导致 JSON 格式错乱(比如少个逗号),解析时会中断整个文件处理,需要跳过脏数据。

二、解析思路:八年经验告诉你——选对库,少走一半弯路

很多新手纠结“哪个 JSON 库最好”,其实实战中常用的就三个:Jackson、Gson、Fastjson。八年经验总结:优先用 Jackson,其次 Gson,避坑 Fastjson——不是说 Fastjson 完全不能用,而是它踩过的坑太多,非必要不选。

1. 三大主流库对比:从性能、功能、坑点三维度评估

维度 Jackson Gson Fastjson
性能 强(解析 10 万条数据约 80ms) 中(解析 10 万条数据约 120ms) 中(早期快,复杂场景易卡顿)
Spring 集成 无缝集成(@RequestBody 底层实现) 需要额外配置 需要额外配置
复杂场景支持 优(泛型、继承、嵌套都稳定) 良(泛型解析需手动处理) 差(复杂对象易解析错乱,如 Integer 转 Long)
安全漏洞 少(2.15.x 后无高危漏洞) 少(谷歌维护,更新及时) 多(历史漏洞 20+,维护不及时)
配置灵活性 高(日期、null 值、字段映射可自定义) 中(配置较简洁,复杂需求需扩展) 中(配置多但部分场景不生效)
八年踩坑次数 5 次(多为配置问题) 8 次(泛型解析坑较多) 20+ 次(类型转换、漏洞、格式错乱)
为什么优先选 Jackson?
  • 无依赖冲突:Spring Boot 默认集成 Jackson,不用额外引入其他库,避免“Jackson+Fastjson 共存导致的类冲突”(见过项目因这俩库冲突,JSON 解析时而正常时而报错)。
  • 复杂场景更稳定:对“泛型嵌套”(如 List<Map<String, User>>)、“继承关系”(如父类 Animal 子类 Dog)的解析支持,Jackson 比其他两个库稳定太多。
  • 可配置性拉满:日期格式化、null 值处理、敏感字段过滤、字段名映射,几乎所有奇葩需求都能通过配置解决(比如 JSON 里的 0/1 映射到 Java 的 boolean)。

2. 核心解析思路:三步法+避坑细节

无论用哪个库,解析 JSON 文件的核心思路都一样:读文件 → 转字符串 → 映射为对象。但这三步里藏着很多“隐形坑”,八年经验帮你把坑填上:

第一步:读文件——避免路径和编码坑
  • 路径问题:不要写绝对路径(如 C:/config.json),用 classpath 读取(适用于 Maven/Gradle 项目):
    // 正确:从 classpath 读取资源(src/main/resources 下的文件)
    InputStream inputStream = JsonFileUtils.class.getClassLoader().getResourceAsStream("payment-config.json");
    // 错误:绝对路径,部署到 Linux 会报错
    File file = new File("C:/config/payment-config.json");
    
  • 编码问题:JSON 文件若含中文,需指定编码为 UTF-8(避免乱码),尤其是 Windows 下生成的文件可能带 BOM 头(),需要先去除:
    // 读取时指定编码,去除 BOM 头
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
    String line;
    StringBuilder sb = new StringBuilder();
    while ((line = reader.readLine()) != null) {
        // 去除 UTF-8 BOM 头(若存在)
        sb.append(line.replace("\uFEFF", ""));
    }
    
第二步:转字符串——大文件别全加载到内存
  • 小文件(<10MB):可以直接转成字符串后解析(简单高效)。
  • 大文件(>100MB):绝对不能全加载到字符串,必须用流式解析(边读边解析,内存占用可控)。
第三步:映射为对象——解决类型和格式坑
  • 类型匹配:JSON 中的 number 类型(如 100),Java 用 Long 接收比 Integer 更安全(避免 JSON 中数字超出 Integer 范围导致溢出)。
  • 日期格式:必须显式指定时区(timezone = "GMT+8"),否则默认 UTC 时区会差 8 小时(比如 JSON 里的“2024-08-01 00:30:00”解析后变成“2024-07-31 16:30:00”,订单日期统计直接错一天)。
  • 未知字段:JSON 中若有 Java 类没有的字段,解析时会报 UnrecognizedPropertyException,需配置忽略未知字段:
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    

三、核心代码:从基础到进阶,附八年实战工具类

以 Jackson 为例(Spring 生态默认,最常用),从“基础解析”到“复杂场景”,再到“通用工具类封装”,每段代码都带“避坑注释”——这些都是我在项目中反复验证过的稳定写法。

1. 基础依赖配置:选对版本很重要

Jackson 由三个核心模块组成:jackson-core(核心API)、jackson-databind(数据绑定)、jackson-annotations(注解支持)。Maven 只需引入 jackson-databind(会自动依赖另外两个):

<!-- Maven 依赖:用 2.15.x 及以上版本(修复了旧版本的安全漏洞) -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.1</version> <!-- 最新稳定版,避免 2.10.x 以下的漏洞 -->
</dependency>

<!-- 可选:若需要解析 YAML/CSV(也能处理 JSON),加这个模块 -->
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-yaml</artifactId>
    <version>2.16.1</version>
</dependency>

Gradle 配置:

implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1'

2. 基础解析:JSON 文件 → Java 对象(最常用场景)

假设我们有一个“用户配置文件”(user-config.json),需要解析为 UserConfig 对象。

步骤 1:定义 Java 实体类(用 Lombok 简化代码)
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Date;
import java.util.List;

@Data // 自动生成 getter/setter/toString,减少模板代码
@JsonIgnoreProperties(ignoreUnknown = true) // 忽略 JSON 中未知的字段(避免解析失败)
public class UserConfig {
    // 字段名映射:JSON 中的 user_id → Java 的 userId(避免字段名不匹配)
    @JsonProperty("user_id")
    private Long userId;

    private String username;

    // 解析 JSON 数组为 List
    private List<String> roles;

    // 日期格式化:显式指定格式和时区(避免 UTC 时差问题)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    // 嵌套对象:用内部类定义(逻辑更紧凑)
    private Address address;

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class Address {
        private String province;
        private String city;
        // JSON 中有 street 字段则解析,没有则为 null(不报错)
        private String street;
    }
}
步骤 2:基础解析代码(小文件适用)
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public class JsonFileUtils {
    // 单例 ObjectMapper:这个对象很重,重复创建会导致性能问题(亲测 QPS 差 3 倍)
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final Logger log = LoggerFactory.getLogger(JsonFileUtils.class);

    static {
        // 全局配置:解决常见问题
        OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 忽略未知字段
        OBJECT_MAPPER.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, false); // 空数组不解析为 null
        OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); // 允许空对象序列化
    }

    /**
     * 解析 JSON 文件为 Java 对象(小文件:<10MB)
     * @param filePath 文件路径(支持 classpath 路径或绝对路径)
     * @param clazz 目标类
     * @return 解析后的对象
     */
    public static <T> T parseJsonFile(String filePath, Class<T> clazz) {
        // 1. 处理文件路径:优先从 classpath 读取,不存在则按绝对路径处理
        File file;
        InputStream inputStream = JsonFileUtils.class.getClassLoader().getResourceAsStream(filePath);
        if (inputStream != null) {
            // 从 classpath 读取(适用于 Maven 项目的 resources 目录)
            try {
                return OBJECT_MAPPER.readValue(inputStream, clazz);
            } catch (IOException e) {
                log.error("解析 classpath 下的 JSON 文件失败,路径:{},目标类:{}", filePath, clazz.getName(), e);
                throw new RuntimeException("JSON 解析异常", e);
            } finally {
                // 关闭流:避免资源泄漏(新手常忘)
                try {
                    if (inputStream != null) inputStream.close();
                } catch (IOException e) {
                    log.error("关闭输入流失败", e);
                }
            }
        } else {
            // 按绝对路径处理
            file = new File(filePath);
            if (!file.exists()) {
                log.error("JSON 文件不存在,路径:{}", filePath);
                throw new RuntimeException("JSON 文件不存在");
            }
            try {
                return OBJECT_MAPPER.readValue(file, clazz);
            } catch (IOException e) {
                log.error("解析绝对路径的 JSON 文件失败,路径:{},目标类:{}", filePath, clazz.getName(), e);
                throw new RuntimeException("JSON 解析异常", e);
            }
        }
    }

    // 调用示例
    public static void main(String[] args) {
        // 解析 classpath 下的 user-config.json
        UserConfig userConfig = parseJsonFile("user-config.json", UserConfig.class);
        log.info("解析结果:用户名={},省份={}", userConfig.getUsername(), userConfig.getAddress().getProvince());
    }
}

3. 进阶场景:解决 90% 复杂业务需求

(1)解析泛型对象(如 List、Map)

业务中常遇到 JSON 数组(如 user-list.json),需要解析为 List<UserConfig>。直接用 List.class 会导致泛型擦除,必须用 TypeReference 指定泛型类型。

import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;
import java.util.Map;

/**
 * 解析泛型对象(如 List<UserConfig>、Map<String, UserConfig>)
 * @param filePath 文件路径
 * @param typeReference 泛型类型引用
 * @return 泛型对象
 */
public static <T> T parseJsonFileWithGeneric(String filePath, TypeReference<T> typeReference) {
    InputStream inputStream = JsonFileUtils.class.getClassLoader().getResourceAsStream(filePath);
    if (inputStream == null) {
        log.error("泛型解析:JSON 文件不存在,路径:{}", filePath);
        throw new RuntimeException("JSON 文件不存在");
    }
    try {
        return OBJECT_MAPPER.readValue(inputStream, typeReference);
    } catch (IOException e) {
        log.error("解析泛型 JSON 文件失败,路径:{},类型:{}", filePath, typeReference.getType(), e);
        throw new RuntimeException("JSON 泛型解析异常", e);
    } finally {
        try {
            inputStream.close();
        } catch (IOException e) {
            log.error("关闭泛型解析流失败", e);
        }
    }
}

// 调用示例 1:解析为 List<UserConfig>
List<UserConfig> userList = parseJsonFileWithGeneric(
    "user-list.json",
    new TypeReference<List<UserConfig>>() {} // 匿名内部类指定泛型
);

// 调用示例 2:解析为 Map<String, UserConfig>(key 为 userId 的字符串形式)
Map<String, UserConfig> userMap = parseJsonFileWithGeneric(
    "user-map.json",
    new TypeReference<Map<String, UserConfig>>() {}
);
(2)大文件流式解析(避免 OOM)

对于 100MB+ 的大文件(如日志文件),必须用流式解析——逐行读取 JSON 节点,处理完一个释放一个,内存占用稳定在 200MB 以内。

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.File;

/**
 * 流式解析大 JSON 文件(适用于 >100MB 的文件,如日志、批量数据)
 * @param filePath 文件路径
 * @param handler 数据处理器(处理单个 JSON 节点)
 */
public static void parseLargeJsonFile(String filePath, LargeJsonHandler handler) {
    File file = new File(filePath);
    if (!file.exists()) {
        log.error("大文件解析:文件不存在,路径:{}", filePath);
        throw new RuntimeException("JSON 文件不存在");
    }
    // 用 try-with-resources 自动关闭 JsonParser(避免资源泄漏)
    try (JsonParser parser = OBJECT_MAPPER.getFactory().createParser(file)) {
        parser.setCodec(OBJECT_MAPPER);
        // 第一步:判断是否为数组开头(大文件常是 JSON 数组格式)
        if (parser.nextToken() != JsonToken.START_ARRAY) {
            log.error("大文件解析:JSON 格式错误,不是数组");
            throw new RuntimeException("大文件 JSON 格式错误");
        }
        // 第二步:逐节点解析(循环直到数组结束)
        while (parser.nextToken() != JsonToken.END_ARRAY) {
            // 读取单个 JSON 节点(不加载整个数组到内存)
            JsonNode node = parser.readValueAsTree();
            // 调用处理器处理单个节点(如写入数据库、统计数据)
            handler.handle(node);
            // 可选:每处理 1000 条数据,手动触发 GC(减少内存峰值)
            if (handler.getProcessedCount() % 1000 == 0) {
                System.gc();
            }
        }
        log.info("大文件解析完成,共处理 {} 条数据", handler.getProcessedCount());
    } catch (IOException e) {
        log.error("大文件解析失败,路径:{}", filePath, e);
        throw new RuntimeException("大文件 JSON 解析异常", e);
    }
}

// 数据处理器接口(解耦处理逻辑)
public interface LargeJsonHandler {
    void handle(JsonNode node);
    int getProcessedCount();
}

// 调用示例:解析用户行为日志文件
public static void main(String[] args) {
    // 实现处理器:统计每个用户的点击次数
    LargeJsonHandler handler = new LargeJsonHandler() {
        private int processedCount = 0;
        private Map<String, Integer> clickCountMap = new HashMap<>();

        @Override
        public void handle(JsonNode node) {
            try {
                // 解析单个日志节点
                String userId = node.get("userId").asText();
                String behaviorType = node.get("behaviorType").asText();
                // 统计点击行为
                if ("CLICK".equals(behaviorType)) {
                    clickCountMap.put(userId, clickCountMap.getOrDefault(userId, 0) + 1);
                }
                processedCount++;
            } catch (Exception e) {
                // 跳过脏数据(避免一条错误日志中断整个解析)
                log.error("处理大文件节点失败,节点内容:{}", node.toString(), e);
            }
        }

        @Override
        public int getProcessedCount() {
            return processedCount;
        }
    };

    // 解析 500MB 的用户行为日志
    parseLargeJsonFile("user-behavior-20240819.json", handler);
}
(3)自定义序列化/反序列化(解决特殊格式)

业务中常遇到“JSON 格式特殊,无法直接映射”的场景,比如:

  • JSON 中的 0/1 映射到 Java 的 boolean(0=false,1=true);
  • 敏感字段(如手机号)在 JSON 中是加密的,解析时需要解密;
  • 枚举类型在 JSON 中是字符串(如 “ALIPAY”),需要映射到 Java 枚举(PaymentChannel.ALIPAY)。

以“加密用户名解密”为例,自定义反序列化器:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.util.Base64;

// 1. 自定义反序列化器:解密 JSON 中的加密用户名
public class DecryptUsernameDeserializer extends JsonDeserializer<String> {
    // 加密密钥(实际项目中从配置文件读取,不要硬编码)
    private static final String SECRET_KEY = "1234567890abcdef";

    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String encryptedUsername = p.getValueAsString();
        if (encryptedUsername == null || encryptedUsername.isEmpty()) {
            return null;
        }
        // 解密逻辑(AES 示例,实际按业务加密算法调整)
        try {
            SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedUsername));
            return new String(decryptedBytes);
        } catch (Exception e) {
            log.error("解密用户名失败,加密值:{}", encryptedUsername, e);
            throw new IOException("用户名解密失败", e);
        }
    }
}

// 2. 在实体类中使用自定义反序列化器
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
    private Long userId;

    // 对 username 字段使用自定义反序列化器
    @JsonDeserialize(using = DecryptUsernameDeserializer.class)
    private String username;

    private String phone;
}

// 3. 解析示例(JSON 中的 username 是加密字符串)
User user = parseJsonFile("encrypted-user.json", User.class);
log.info("解密后的用户名:{}", user.getUsername()); // 输出解密后的明文
(4)反向场景:Java 对象 → JSON 文件(生成 JSON)

除了解析,生成 JSON 文件也很常用(如导出数据)。需要注意“格式化输出”“排除 null 字段”“敏感字段脱敏”。

import java.io.FileWriter;
import java.io.IOException;

/**
 * 将 Java 对象生成 JSON 文件
 * @param obj 要生成 JSON 的对象
 * @param filePath 输出文件路径
 * @param prettyPrint 是否格式化输出(true:带缩进,易读;false:压缩,体积小)
 */
public static <T> void generateJsonFile(T obj, String filePath, boolean prettyPrint) {
    File file = new File(filePath);
    // 确保父目录存在(避免文件路径不存在导致报错)
    if (!file.getParentFile().exists()) {
        boolean mkdirs = file.getParentFile().mkdirs();
        if (!mkdirs) {
            log.error("创建 JSON 文件父目录失败,路径:{}", file.getParent());
            throw new RuntimeException("创建父目录失败");
        }
    }
    try (FileWriter writer = new FileWriter(file)) {
        if (prettyPrint) {
            // 格式化输出(带缩进,适合人工阅读)
            OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(writer, obj);
        } else {
            // 压缩输出(无缩进,体积小,适合接口传输)
            OBJECT_MAPPER.writeValue(writer, obj);
        }
        log.info("JSON 文件生成成功,路径:{}", filePath);
    } catch (IOException e) {
        log.error("生成 JSON 文件失败,路径:{},对象类型:{}", filePath, obj.getClass().getName(), e);
        throw new RuntimeException("JSON 生成异常", e);
    }
}

// 调用示例:导出用户列表为 JSON 文件(格式化输出)
List<UserConfig> userList = new ArrayList<>();
userList.add(new UserConfig(1L, "张三", List.of("admin")));
generateJsonFile(userList, "exported-user-list.json", true);

4. 八年踩坑总结:这些细节能让你少走 3 年弯路

  1. 别用 Fastjson 的 JSON.parseObject
    踩坑经历:之前做金融项目,用 Fastjson 解析订单数据,JSON 中的 Integer 字段(如 orderId: 123)被解析成 Long,存入数据库时因类型不匹配报错,排查了 2 小时才发现是 Fastjson 的类型转换问题。后来换成 Jackson,再也没出现过。

  2. 日期格式化必须显式指定时区
    没加 timezone = "GMT+8" 时,Jackson 默认用 UTC 时区,解析“2024-08-01 00:30:00”会变成“2024-07-31 16:30:00”,导致订单日期统计错误(凌晨下单的订单被归到前一天)。解决方案:所有日期字段加 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

  3. 工具类一定要加详细日志
    解析失败时,没日志等于“盲人摸象”。至少要打印:文件路径、目标类、异常栈。比如:

    log.error("解析 JSON 文件失败,路径:{},目标类:{}", filePath, clazz.getName(), e);
    

    之前有个同事没加日志,解析失败后只能一遍遍试,浪费了一下午。

  4. 大文件坚决用流式解析
    500MB 的日志文件,直接用 readValue 加载,内存瞬间飙到 1.5G,触发 OOM 导致服务器重启。换成流式解析后,内存稳定在 200MB 以内,处理速度还快了 20%。

  5. 避免在循环中创建 ObjectMapper
    ObjectMapper 是重量级对象,创建时会加载大量配置和类信息。之前项目中有人在循环里创建 ObjectMapper,QPS 从 1000 降到 300,改成单例后恢复正常。

  6. @JsonProperty 显式映射字段名
    就算现在 JSON 字段和 Java 字段一致(如 username),也建议加 @JsonProperty("username")——万一后续前端改字段名(如 user_name),只需改注解,不用改 Java 字段名,减少代码改动量。

  7. 处理 JSON 中的 null 和空数组
    JSON 中的 null 字段,Jackson 默认解析为 Java 的 null,容易触发 NPE。建议配置:

    // 将 null 解析为空字符串(针对 String 字段)
    OBJECT_MAPPER.getDeserializationConfig().withHandler(new NullStringHandler());
    // 将空数组解析为空 List(避免 null)
    OBJECT_MAPPER.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, false);
    

四、最后:解析 JSON 的本质是什么?

八年开发越久越觉得:JSON 解析看似是“技术活”,其实考验的是对业务的理解。

  • 配置文件解析,要考虑“可扩展性”——比如新增支付渠道时,JSON 结构不变,只需加一个 channel 节点,解析代码不用改;
  • 接口数据解析,要考虑“兼容性”——比如前端新增字段,后端解析时不能报错,需用 @JsonIgnoreProperties(ignoreUnknown = true)
  • 日志文件解析,要考虑“性能”——大文件不能加载到内存,脏数据要跳过,保证解析不中断。

选择 Jackson 还是 Gson,用基础解析还是流式解析,这些都是手段。最终目的是让数据“稳定、高效”地在系统间流动——就像老木匠选刨子,不是看刨子多贵,而是看能不能刨出光滑的木板。

如果你也踩过 JSON 解析的奇葩坑(比如 JSON 里的字段时而 camelCase 时而 snake_case),欢迎在评论区交流——毕竟,踩坑的尽头是共鸣。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐