SpringMVC中JSON处理:Jackson与FastJson整合实战对比 前后端分离的项目做多了你大概率会有一种感受所有联调扯皮一半以上都出在JSON格式问题上。前端说日期格式不对后端说字段名对不上中间还有一堆反序列化异常等着你。Jackson和FastJson是Java后端最常用的两个JSON处理库一个稳一个快各有各的用户群。这篇文章我直接从实际使用角度出发把两个库的入门用法、常用配置以及整合SpringMVC的完整路径梳理一遍。内容不绕弯子该给代码给代码该给配置给配置适合刚接触SpringMVC的Java开发者也适合已经在项目里用了、但想系统对比一下选型的同学。1. 先搞清楚SpringMVC为什么需要专门聊JSON处理1.1 JSON在前后端交互中的核心地位现在的Web项目几乎没有不做前后端数据交互的。后端把数据装成JSON通过接口吐给前端前端把参数装成JSON通过请求体传给后端。JSON本身只是一种数据格式规范但Java是强类型语言你不能直接把JSON字符串塞进一个业务对象里更不能把一个对象直接变回JSON字符串。这个过程需要序列化和反序列化。序列化是把Java对象转成JSON字符串反序列化是把JSON字符串转回Java对象。而这正是Jackson和FastJson这类库的核心职能。说得再直白一点你写的每一个包含RequestBody的Controller方法底层都在做反序列化每一次返回业务对象的Controller方法底层都在做序列化。这个环节如果处理不好接口联调就是灾难现场。1.2 HttpMessageConverter机制与两个库的定位SpringMVC本身并不直接绑定某个JSON库它定义了一套HttpMessageConverter消息转换器机制。请求进来时MappingJackson2HttpMessageConverter这类转换器负责把请求体里的JSON字符串变成Java对象响应出去时同一个转换器负责把Java对象变成JSON字符串。这套机制的好处是解耦。你可以用Jackson也可以用FastJson甚至可以自定义一个消息转换器。SpringMVC只负责调度具体干活的是转换器。你要做的就是把某个JSON库对应的消息转换器注册到SpringMVC的转换器列表里。那Jackson和FastJson的定位有什么区别Jackson是Spring生态的事实标准Spring Boot默认就走JacksonAPI设计严谨扩展点丰富生态成熟。FastJson的特点是API使用特别直接JSON.toJSONString()一下就把对象序列化完了性能在不少基准测试里也表现不错。很多国内团队的老项目里都能看到FastJson的影子。选型这件事没有绝对的谁好谁坏更多看项目现状和团队约束。后面我会专门给一个对比表。2. Jackson快速上手核心API与常用配置2.1 从ObjectMapper开始序列化与反序列化Jackson的核心类是ObjectMapper。所有序列化和反序列化操作都围绕它展开。先看最基本的用法ObjectMapper objectMapper new ObjectMapper(); // 序列化Java对象 → JSON字符串 String json objectMapper.writeValueAsString(user); // 反序列化JSON字符串 → Java对象 User user objectMapper.readValue(json, User.class);这看起来很简单实际使用时有一个很容易被忽略的点ObjectMapper是有状态的很多全局配置都挂在它上面。如果每次都用new ObjectMapper()创建新实例配置会丢失性能也会有损耗。正确做法是把它作为Spring容器里的单例Bean来管理需要的时候直接注入。另一个高频场景是泛型集合的反序列化。比如ListUser直接写readValue(json, List.class)拿到的其实是ListLinkedHashMap后续取元素强转User时会出问题。这种情况必须用TypeReferenceListUser users objectMapper.readValue( json, new TypeReferenceListUser() {} );TypeReference的工作原理是在匿名内部类里拿到完整的泛型类型信息Jackson根据这个类型信息反序列化出正确的ListUser。2.2 注解驱动JsonProperty、JsonIgnore与JsonFormat注解是Jackson最实用的功能之一。通过注解你可以在不改实体类结构的前提下控制序列化结果。public class User { private Long id; // 指定序列化到JSON时的字段名 JsonProperty(user_name) private String name; // 这个字段完全不参与序列化和反序列化 JsonIgnore private String password; // 日期格式化必须指定时区否则可能差8小时 JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private Date createTime; // 省略 getter/setter }JsonProperty最常见的应用场景是当Java字段名是驼峰风格、前端需要下划线风格时通过这个注解做映射就不用把整个工程的命名风格改掉。JsonIgnore则用于敏感字段比如密码、token这类不应该暴露给前端的数据。JsonFormat是日期处理的救星。Java里Date序列化后默认是一串时间戳数字前端拿到根本没法直接展示。加上这个注解后输出就是2025-01-15 10:30:00这种可读格式。注意timezone属性经常被忽略结果导致时间差了8个小时这种问题排查起来特别头疼不如一开始就写上。2.3 全局配置解决null、未知字段和日期格式注解解决的是局部问题全局问题得靠ObjectMapper的统一配置。我一般会手动调整几个配置项否则很容易踩坑。ObjectMapper objectMapper new ObjectMapper(); // 反序列化时遇到JSON里有、Java类里没有的字段默认是抛异常的。 // 前端多传了字段后端整个接口直接报错这不是我们想要的行为。 objectMapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false ); // 序列化时值为null的字段默认会输出field: null。 // 有些前端不喜欢这种结构可以统一不输出。 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 全局日期格式 objectMapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); // Java 8的LocalDateTime需要额外注册JavaTimeModule objectMapper.registerModule(new JavaTimeModule());这里单独说一下JavaTimeModule。如果你用LocalDateTime而不是DateJackson默认反序列化会报错因为JDK 8的时间类型不是传统JavaBean。Spring Boot的自动配置会帮你注册这个模块但你要是自己new了一个ObjectMapper就很容易忽略这一步。我在刚接手一个老项目时就遇到过类似的问题自己创建ObjectMapper没注册JavaTimeModule接口返回LocalDateTime时直接异常。3. FastJson快速上手API风格与两库差异3.1 一栈式APIJSON类的直接调用FastJson的核心入口是JSON这个静态类API风格比Jackson更直接。不需要先创建ObjectMapper这种东西直接调用静态方法就完事了。// 序列化 String json JSON.toJSONString(user); // 反序列化基础对象 User user JSON.parseObject(json, User.class); // 反序列化泛型集合 ListUser users JSON.parseArray(json, User.class); // 更灵活的方式TypeReference ListUser users2 JSON.parseObject(json, new TypeReferenceListUser() {});com.alibaba.fastjson.JSONObject也是FastJson的一大特色可以直接从JSON字符串中提取某个字段而不必完整反序列化成实体类JSONObject obj JSON.parseObject(json); Long userId obj.getLong(id);这种用法在写一些灵活接口时很方便比如你要从上游接口返回里抽取几个字段又不想为这个响应单独建一个DTO。需要区分的是FastJson有1.x和2.x两个大版本。2.x的包名变成了com.alibaba.fastjson2核心类同样是JSON但底层完全重写了。如果你的项目是从0开始直接用fastjson2就好老项目继续用com.alibaba.fastjson也没问题只是要注意依赖别引错。3.2 JSONField与常用SerializerFeatureFastJson对应的核心注解是JSONField一个注解覆盖了字段名映射、忽略字段、日期格式化等需求。public class User { private Long id; JSONField(name user_name) private String name; JSONField(serialize false) private String password; JSONField(format yyyy-MM-dd HH:mm:ss) private Date createTime; // 省略 getter/setter }对比Jackson的写法你会发现思路非常接近name用来重命名serialize false对应JsonIgnoreformat对应日期格式。这就是为什么说这两个库只要你掌握了一个另一个入门成本极低。序列化时如果想调整输出行为可以给JSON.toJSONString()传入SerializerFeature枚举参数String json JSON.toJSONString(user, SerializerFeature.WriteMapNullValue);WriteMapNullValue表示null值字段也要输出。这正好和Jackson的Include.NON_NULL是相反的操作两个库默认行为其实不一样。Jackson默认输出null字段FastJson默认不输出null字段。这在联调时是个坑因为前端可能用略显不同的方式来判空。后面排查章节我会专门讲。3.3 两库对比与选型建议这里放一个我实际使用后整理的对比表方便快速了解差异。对比维度JacksonFastJsonAPI风格需要先创建ObjectMapper面向对象风格JSON静态类直接调用上手更快Spring生态集成Spring Boot默认自动配置完善需要手动注册消息转换器注解功能JsonProperty、JsonIgnore、JsonFormatJSONField一个顶三个null字段处理默认输出null默认不输出nullJava 8时间类型需要JavaTimeModule内置支持泛型反序列化用TypeReference用TypeReference或parseArray维护活跃度社区活跃版本迭代稳定2.x版本重写后更新较快典型适用场景Spring Boot项目、对生态依赖重的场景老项目接入、API风格偏好直接、性能敏感场景选型建议上如果是新项目且已经用了Spring Boot那直接用Jackson就好因为Spring Boot默认就依赖它再引FastJson反而多一套转换器要管理。如果是老项目里面已经遍布JSON.toJSONString这类调用那就不要纠结了继续用FastJson同时注意版本升级到维护活跃的版本。4. 实战整合SpringMVC从配置到接口验证4.1 传统SpringMVC工程中的消息转换器配置前面说过SpringMVC通过HttpMessageConverter处理JSON。整合的核心就是把这个转换器注册到SpringMVC容器里。先看一个传统SSM风格工程的XML配置。假设你已经有了SpringMVC的基础配置要启用JSON转换在spring-mvc.xml里加上!-- JSON参数和响应 -- mvc:annotation-driven mvc:message-converters register-defaultstrue bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper bean classorg.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean property namedateFormat valueyyyy-MM-dd HH:mm:ss/ property namefeaturesToDisable array util:constant static-fieldcom.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES/ /array /property /bean /property /bean /mvc:message-converters /mvc:annotation-drivenmvc:annotation-driven是SpringMVC注解驱动的开关message-converters可以自定义转换器列表。很多人不知道的是只要classpath里有JacksonSpringMVC的annotation-driven就会自动注册MappingJackson2HttpMessageConverter所以最小配置其实什么都不用做。上面这段配置是为了自定义ObjectMapper的行为才需要显式声明。4.2 整杰ACKson最省事的方案如果项目里已经引入了jackson-databind依赖整合Jackson几乎不需要做什么额外的事情。Spring Boot项目更省心自动配置已经搞定了一切。你可以在application.yml里做简单的配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 default-property-inclusion: non_null deserialization: fail-on-unknown-properties: false这里有个经验spring.jackson.date-format只对java.util.Date生效对LocalDateTime是不起作用的。如果你用的是LocalDateTime需要单独注册序列化器或者使用Jackson2ObjectMapperBuilderCustomizer来自定义。Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); return builder - { builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter)); builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter)); }; }这样LocalDateTime类型字段序列化和反序列化都会走你指定的格式。这个方法在实际项目里很有用建议保存下来。4.3 整杰 FastJson手动注册的完整步骤FastJson不像Jackson那样被Spring Boot自动配置所以核心步骤是注册FastJsonHttpMessageConverter并把它加入SpringMVC的转换器列表。实现WebMvcConfigurer的configureMessageConverters方法是最常见的做法Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void configureMessageConverters(ListHttpMessageConverter? converters) { FastJsonHttpMessageConverter converter new FastJsonHttpMessageConverter(); FastJsonConfig config new FastJsonConfig(); // 全局日期格式 config.setDateFormat(yyyy-MM-dd HH:mm:ss); // 序列化特性配置 config.setSerializerFeatures( SerializerFeature.WriteMapNullValue ); converter.setFastJsonConfig(config); // 关键必须设置支持的MediaType否则接口返回406/415 ListMediaType mediaTypes new ArrayList(); mediaTypes.add(MediaType.APPLICATION_JSON); converter.setSupportedMediaTypes(mediaTypes); converters.add(converter); } }如果在Spring Boot项目里用FastJson还需要注意一个细节Spring Boot已经自动注册了一堆转换器你再往集合里加FastJson两个JSON转换器会同时存在。Spring是按转换器顺序匹配的如果Jackson排前面请求参数就可能被Jackson处理掉FastJson配置的规则就不会生效。这也是为什么有些项目整合FastJson后发现注解不起作用。要让FastJson成为默认转换器比较稳的做法是重新定义HttpMessageConverters这个BeanBean public HttpMessageConverters fastJsonHttpMessageConverters() { FastJsonHttpMessageConverter converter new FastJsonHttpMessageConverter(); FastJsonConfig config new FastJsonConfig(); config.setDateFormat(yyyy-MM-dd HH:mm:ss); config.setSerializerFeatures(SerializerFeature.WriteMapNullValue); converter.setFastJsonConfig(config); ListMediaType mediaTypes new ArrayList(); mediaTypes.add(MediaType.APPLICATION_JSON); converter.setSupportedMediaTypes(mediaTypes); return new HttpMessageConverters(converter); }如果你的Spring Boot版本较旧1.x年代甚至有专门的fastjson-spring-boot-starter可用直接依赖加引入、配置类都不用写。新版本项目我更推荐手动注册的方式依赖更少出现问题也好排查。4.4 实测验证写一个接口看生效情况配置完成后用一个带日期和枚举字段的接口快速验证是否生效。假设Controller长这样RestController RequestMapping(/api/user) public class UserController { PostMapping(/save) public User save(RequestBody User user) { return user; } }保存接口直接回显接收到的对象。请求时发送{ user_name: 张三, createTime: 2025-02-10 12:00:00 }如果返回结果中user_name和createTime格式都正常说明反序列化和序列化都工作在预期状态。如果createTime变成时间戳或者user_name变成name那就是消息转换器没有正确生效可以顺藤摸瓜检查配置。5. 高频问题排查速查日期、循环引用、415错误5.1 日期格式总是不对日期问题是JSON处理里出现频率最高的问题没有之一。常见表现有三种反序列化时报InvalidFormatException序列化后变成一串数字时间戳或者时间时区相差8小时。如果是Date类型检查三件事第一ObjectMapper是否设置了dateFormat第二JsonFormat注解里的pattern写没写对第三时区是否设置为GMT8。前两个解决格式问题第三个解决时差问题。如果是LocalDateTimespring.jackson.date-format根本管不到。必须走Jackson2ObjectMapperBuilderCustomizer注册序列化和反序列化器或者直接在字段上加注解。FastJson对LocalDateTime的内置支持其实比Jackson好但用JSONField(format yyyy-MM-dd HH:mm:ss)仍然是更明确的做法。5.2 循环引用序列化异常两个对象互相引用在遍历序列化时就会无限递归最终栈溢出。比如订单里有用户用户里又挂了订单列表。FastJson默认开启循环引用检测遇到这种结构会产生{$ref:$.data.user}这种引用令牌而不是真正递归展开。这个令牌对普通前端很不友好因为它根本不知道这是什么。如果业务上确实出现了循环引用我建议优先从设计上拆开比如在DTO层面去掉双向引用而不是依赖序列化库的兜底机制。Jackson对循环引用的默认行为是报JsonMappingException: Infinite recursion。解决办法是使用JsonIgnore打断一边的引用或者适当地用JsonIdentityInfo处理同一对象多次出现的情况。从实际经验看JsonIgnore最简单也最不容易出幺蛾子。注意不要把循环引用问题简单理解为加一个SerializerFeature.DisableCircularReferenceDetect就万事大吉。关闭检测后如果还存在真实循环序列化还是会无限递归直到栈溢出。先处理引用结构再谈特性配置。5.3 返回状态码415与JSON内容缺失接口报415 Unsupported Media Type多半是消息转换器没有匹配到请求体的Content-Type。假如你POST请求时带的是application/json;charsetUTF-8但转换器支持的MediaType里只写了application/jsonSpringMVC可能在匹配时出问题。所以在4.3节里我特意加了converter.setSupportedMediaTypes(...)这一行很多网上的示例代码都没有这一步实测下来就栽在这里。另一个低频现象是接口返回200但JSON里凡是为null的字段全部消失。这不是bug如果你用的是FastJson这正是它的默认行为。如果前端需要固定数据结构通过SerializerFeature.WriteMapNullValue把null字段补上即可如果用的是Jackson只想输出非null字段则设置Include.NON_NULL。两个库在这一点上方向完全相反读代码时别被绕进去。5.4 一个更隐蔽的坑多个转换器并存时的优先级在同时引入Jackson和FastJson的项目里配置类非常容易出现“改了不生效”的假象。比如你明明为FastJson设置了WriteMapNullValue但实际返回结果里null字段还是没了。原因大概率是Jackson转换器排在FastJson前面并且先被Spring选中处理了响应。Spring按转换器的声明顺序和兼容性进行匹配一旦Jackson能够处理application/jsonFastJson就没有机会参与。排查方式很简单在configureMessageConverters方法里打印一下转换器列表的先后顺序或者直接在统一的响应封装里放一个明显的调试字段看它走的是哪个转换器的序列化逻辑。确定之后再做取舍要么移除Jackson依赖要么用Primary和控制注册顺序来保证FastJson优先。这里我要多说一句假如你的项目已经因为历史依赖不得不同时存在两个JSON库那么至少在代码层面要统一入口不要一会儿用ObjectMapper一会儿用JSON.toJSONString否则同一个对象的序列化行为在不同接口里可能都不一致。我个人在实际操作中的体会是JSON库本身只是工具真正的坑往往出现在“默认行为”上。Jackson和FastJson的很多差异点比如null字段输出、未知字段处理、循环引用策略都属于那种平时记不住、线上踩一脚才知道疼的细节。与其等到联调阶段手忙脚乱不如在项目初始化时就把消息转换器的配置和字段风格统一好。还有一个建议如果你的团队同时维护多个服务最好在代码规范里明确规定一个主JSON库并把它封装成一个公共依赖避免每个服务各自为政。这样后面接手的同事不用在两种API之间来回切换光这一点就值回配置成本了。