拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SSM项目Jackson依赖引入与SpringMVC JSON序列化问题排查

做 SSM 项目的人迟早都得跟 Jackson 的依赖引入打交道。SpringMVC 的 ResponseBody 和 RequestBody 底层用的默认 JSON 框架就是 Jackson你只要在 pom.xml 里漏加 jackson-databind或者用了版本对不上的旧包接口返回时大概率会冒出 No serializer found 或者 HttpMessageNotWritableException 这类报错而且这种问题第一次遇到时很容易一头雾水代码看着没毛病编译也过偏偏一调用就异常。这篇我就结合自己搭 SSM 项目的实际经历把 Jackson 的坐标选择、版本管理、SpringMVC 整合配置、常见报错排查讲清楚顺便聊聊大家都在搜的 Jackson 和 Fastjson 该怎么选。适合正在搭 SSM 环境的新人也适合项目里被 JSON 序列化问题困扰的同学做对照排查。1. 为什么 SSM 项目里 Jackson 是绕不开的依赖1.1 SpringMVC 的 JSON 转换链路是怎么走的先搞清楚 Jackson 在整个 SSM 项目里处于什么位置。SSM 说的是 Spring、SpringMVC、MyBatis 三件套前端发来的 JSON 请求体要变成 Java 对象后端返回的 Java 对象要变成 JSON 响应这两条路都绕不开 SpringMVC 的 HttpMessageConverter 机制。RequestBody 会找合适的 converter 把请求体反序列化成参数对象ResponseBody 则把方法返回值交给 converter 序列化成响应体。SpringMVC 默认注册的 JSON 转换器就是 MappingJackson2HttpMessageConverter它内部持有一个 com.fasterxml.jackson.databind.ObjectMapper 实例序列化和反序列化的核心动作都由 ObjectMapper 完成。所以 Jackson 在 SSM 项目里不是可选增强库而是 SpringMVC JSON 能力的地基。Spring 官方逻辑是只要 classpath 里存在 Jackson 2.xmvc:annotation-driven 就会自动装配这个转换器反过来如果缺了 JacksonSpringMVC 找不到合适的 message converter接口直接抛异常前端要么看到 500要么拿到一个空响应体。1.2 少了 Jackson 到底会看到什么现象我在帮同事排查问题时见过不少典型的缺依赖现场。最常见的场景是 controller 返回一个 Map 或者实体对象前端一调用就报 HttpMessageNotWritableException: No converter found for return value of type...。也有另一种情况项目里引了一个老掉牙的 org.codehaus.jackson或者从网上复制了一段 Gson 依赖虽然项目能启动但 ResponseBody 返回的 JSON 格式完全不对日期序列化成一大串数字JsonFormat 注解不生效隐藏字段莫名其妙出现在响应里。这些现象背后其实是同一件事SpringMVC 没有找到可用的 JSON converter。所以我一直建议给 SSM 项目引入 Jackson 不要看成加一个工具库而应该看成把 SpringMVC 的 JSON 基础设施一次装好。理解了这层关系再去选坐标、定版本、做配置每一步都能想清楚为什么而不是从网上看到一段配置就复制粘贴。2. Jackson 依赖引入的实操细节坐标、版本与冲突处理2.1 Maven 坐标怎么选三个核心包的关系Jackson 2.x 的 Maven 坐标很多人只记住了 jackson-databind其实完整体系由三个核心包组成。jackson-core 提供流式 API包括 JsonParser 和 JsonGenerator是整个体系最底层的流处理引擎jackson-annotations 负责注解也就是 JsonProperty、JsonFormat、JsonIgnoreProperties 这些我们日常写在实体上的东西jackson-databind 提供 ObjectMapper是高层数据绑定 API同时传递依赖了前面两个包。实操中绝大多数项目只需要显式引入 jackson-databind 一个依赖就够了。这里我想做个类比Python 同学都知道用 pip 引入依赖包Maven 坐标就是 Java 世界里的依赖引入方式把 jar 包从中央仓库拉下来装进 classpath。但 Maven 比 pip 多了一层传递依赖机制databind 会自动把 core 和 annotations 一起带进来。有一类特殊情况要留意如果你在 pom 里把 core、annotations、databind 三个包都显式列出来而且版本不一致Maven 的最近优先原则会让不同版本的类混在同一套 classpath 里轻则注解不生效重则 NoSuchMethodError。所以我的原则是能只引 databind就绝不多手引另外两个。2.2 版本号怎么定别盲目追新版本选择是 Jackson 踩坑的重灾区。以 2.15.x 为例它要求 JDK 8 起步如果项目跑在 JDK 17 上我更建议大家用 2.14 以上的版本兼容性更稳。选版本至少看三个维度第一看 Spring 版本Spring 4.x 时代配 Jackson 2.x 问题不大Spring 5.3.x 的官方测试一般基于 Jackson 2.12 以上第二看项目里有没有其他依赖传递引入 Jackson如果有公共依赖锁定了 2.9.x你直接写 2.15.2 可能根本拉不进高版本第三看项目是否使用 Java 8 的 LocalDate、LocalDateTime如果用还需要补一个 jackson-datatype-jsr310 模块否则反序列化这类时间类型会直接抛异常。我习惯统一用 properties 管理版本号。先声明jackson.version2.15.2/jackson.version然后再用 jackson-bom 作为 BOM 导入。BOM 的全称是 Bill of Materials类似一张官方打包好的版本清单引入它之后 databind、core、annotations、jsr310 这些模块的版本就不需要你一个一个人肉对齐了Maven 会按 BOM 里列出的版本统一解析。这对多模块的 SSM 工程意义很大因为它把版本对不齐这类问题从根源上消灭掉了。properties jackson.version2.15.2/jackson.version /properties dependencyManagement dependency groupIdcom.fasterxml.jackson/groupId artifactIdjackson-bom/artifactId version${jackson.version}/version typepom/type scopeimport/scope /dependency /dependencyManagement2.3 依赖冲突排查dependency:tree 是救命工具版本冲突在 SSM 这种依赖链超长的项目里很常见。比如说项目里引了某个第三方 SDK它内部传递引入 org.codehaus.jackson:jackson-mapper-asl这是 Jackson 1.x 的老坐标包名是 org.codehaus.jackson而 2.x 的包名是 com.fasterxml.jackson.core两类包共存通常没事因为代码层面使用的类完全不同。真正的麻烦是同一个坐标被解析成多个版本比如两个依赖分别引入了 jackson-databind 2.9.8 和 2.15.2最终生效的版本取决于 Maven 的依赖树解析规则而结果往往不是你期望的那个。排查冲突最直接的办法是运行mvn dependency:tree -Dincludescom.fasterxml.jackson.core输出里会清楚列出 jackson-databind 被哪些依赖引入、最终解析到哪个版本。如果你发现间接依赖锁定了旧版而业务代码需要新版 API就在你直接声明的依赖里保持高版本最近优先原则会保障你的直接声明生效。如果某个 SDK 强行把旧版拉进来还导致启动时报 NoSuchMethodError就用 exclusion 把它从 SDK 的传递依赖中剔除。注意 exclusion 要精确到你需要排除的那个坐标别把整个 SDK 依赖排掉否则可能把 SDK 的其他能力一起废掉。我见过有人图省事直接排除整个依赖树结果系统里一堆 ClassNotFoundException回头排查更痛苦。3. SpringMVC 整合 Jackson 的配置要点3.1 mvc:annotation-driven 其实帮你做完了大部分事一般 SSM 项目的 spring-mvc.xml 里只写了mvc:annotation-driven/一行ResponseBody 就能正常返回 JSON原因我在前面说过SpringMVC 检测到 classpath 有 Jackson 2.x会自动注册 MappingJackson2HttpMessageConverter。这步默认配置能满足 80% 的日常需求但实测下来有几个点很容易让前端同事抓狂。日期被序列化成长长的时间戳数字值为 null 的字段被原样输出Long 类型的雪花 ID 发给浏览器后精度丢失。这些问题表面看是 JSON 不规范根子其实都在 ObjectMapper 的全局设置上需要你自己动手改。3.2 自定义 ObjectMapperXML 和 Java Config 两种姿势要改全局序列化规则核心思路是替换 SpringMVC 默认持有的 ObjectMapper。XML 风格的老 SSM 项目可以这样改bean idobjectMapper classcom.fasterxml.jackson.databind.ObjectMapper property namedateFormat valueyyyy-MM-dd HH:mm:ss/ /bean mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper refobjectMapper/ /bean /mvc:message-converters /mvc:annotation-driven如果项目已经切到 Java Config就注册一个 Bean思路完全一致Bean public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper; }这里有个容易被忽略的点你一旦自定义了 ObjectMapperSpringMVC 就不会再用默认实例所以你在里面做的任何配置都会变成全项目生效。比如 FAIL_ON_UNKNOWN_PROPERTIES 这个配置默认情况下 Jackson 2.x 在反序列化时遇到实体里不存在的字段会直接抛 UnrecognizedPropertyException接口返回 400。我建议对于开放接口、前端字段经常变动的系统把它关掉能让联调顺畅很多当然代价是后端对多余字段不再敏感有没有被恶意传参要自己在日志里关注团队需要约定好。3.3 日期格式与 JSR310 模块LocalDateTime 的正确姿势日期序列化是另一个高频需求。建议实体字段上使用双保险全局配好 dateFormat字段上再用 JsonFormat(pattern yyyy-MM-dd HH:mm:ss) 做兜底覆盖。注意全局 dateFormat 只对 java.util.Date 类型生效对 Java 8 的 LocalDate、LocalDateTime 不生效。要让这些新时间类型正常工作先引入 jackson-datatype-jsr310 依赖然后在 ObjectMapper 上注册 JavaTimeModulemapper.registerModule(new JavaTimeModule());注册之后 LocalDateTime 默认按 ISO 8601 格式输出例如 2024-06-18T10:30:00中间那个 T 在不少业务场景里看着很别扭。要输出成业务常用的 yyyy-MM-dd HH:mm:ss还是在字段或 getter 上配 JsonFormat 最直接因为字段级的注解优先级高于全局设置。另外如果你真的想保留时间戳输出需要配置 WRITE_DATES_AS_TIMESTAMPS但我在实际项目中很少见到这种需求统一输出字符串对前端反而是最友好的。Long 精度丢失的问题也值得多说两句。Java 的 Long 最大值 19 位超过 JavaScript 的 Number 安全整数范围2 的 53 次方雪花 ID 发给浏览器后末几位可能悄悄变成 0。常见解法是序列化时把这类 Long 字段转成 String可以给字段配 JsonSerialize(using ToStringSerializer.class)也可以按字段类型统一注册自定义序列化器。如果只是个别 ID 字段用注解最省事不用动全局配置。4. Jackson 与 Fastjson 之争热词背后怎么选4.1 功能与 API 差异Jackson 和 Fastjson 哪个好是很多人在搜索引擎里反复问的问题要回答它得先搞清楚两者的差异。Fastjson 的 API 确实简单JSON.toJSONString(obj) 一行序列化JSON.parseObject(json, Xxx.class) 一行反序列化对新手来说几乎没有学习成本。Jackson 则要先创建 ObjectMapper 实例再调用 writeValueAsString 和 readValue光看这两行代码Fastjson 的上手速度明显更胜一筹。但 Jackson 在注解体系、模块化扩展、Spring 生态集成这三个方面的优势是 Fastjson 传统版本比不了的。Spring Boot 默认 JSON 库就是 JacksonSpringMVC 的 starter 预置的 message converter 也是围绕 Jackson 设计的。你如果非要在 SSM 项目里换成 Fastjson需要自己注册 FastJsonHttpMessageConverter而且 Fastjson 的 JSONField 和 Jackson 的 JsonProperty、JsonIgnoreProperties 是两套完全不同的注解体系团队里一旦有人混用代码里到处都是风格冲突交接和排查都费劲。4.2 安全性与维护性Fastjson 这些年的名声比较复杂。它被讨论最多的不是性能而是反复出现的反序列化安全漏洞。2019 年到 2022 年期间Fastjson 的 autotype 机制被多次爆出可被利用执行任意代码官方多次发布安全公告并建议升级到特定版本、开启 safeMode。对于内部管理系统这可能还在可控范围但项目如果要过安全评审或者等保测试安全扫描报告里出现 Fastjson 字样基本一律要求整改。我这些年已经把新项目的默认 JSON 库全部定为 Jackson理由不是性能而是维护心态Spring 官方的跟进节奏、社区的讨论体量、漏洞响应速度Jackson 都要稳得多。4.3 迁移成本应该怎么算老项目如果已经在用 Fastjson迁移不是改几行序列化代码那么简单。常见障碍有三个Fastjson 的 JSONObject 被当作业务对象在 Service 层传来传去、实体上到处是 JSONField 注解、部分单测对 JSON 字段顺序敏感。硬迁移的改动面可能很大容易引发回归。我的建议是分两步走。先把有外部请求入口的核心接口切换成 Jackson并在安全上完成兜底同时在公共模块里封装一个 JsonUtil 工具类只暴露 toJsonString、parseObject、parseArray 三个方法内部实现从 Fastjson 换成 Jackson。这样业务代码的调用点改动最小后续如果决定全面切换也只需要改工具类内部逻辑。这是个成本很低但收益明确的过渡方案我见过好几个团队都是这么平稳切过去的。简单对照一下两者维度JacksonFastjsonSpring 生态集成默认支持自动装配需手动配置 converterAPI 上手难度需要 ObjectMapper 实例静态方法直接调用历史安全表现默认配置相对稳定多次 autotype 安全公告模块化扩展Module 体系丰富扩展模块较少维护活跃度持续迭代社区活跃旧版维护节奏慢fastjson2 重启5. 常见问题与排查技巧实录5.1 高频报错速查表JSON 报错在 SSM 项目里太常见了我把这几年见过的高频报错整理成一张速查表先对着找方向报错信息典型原因处理建议No converter found for return value of type...项目缺 Jackson 依赖或返回值类型没有对应 converter检查 pom 是否引入 jackson-databindNo serializer found for class ... and no properties discovered实体没有可访问的 getter或对象是代理对象查 getter 命名处理 MyBatis 懒加载代理UnrecognizedPropertyException请求里的字段在实体中不存在关 FAIL_ON_UNKNOWN_PROPERTIES 或补字段InvalidDefinitionException: Java 8 date/time type not supported缺少 JSR310 模块引入 jackson-datatype-jsr310 并注册 JavaTimeModuleNoSuchMethodError依赖树里同一坐标出现多个版本dependency:tree 排查并统一版本Could not write JSON: Infinite recursion对象存在循环引用用 JsonBackReference 或 DTO 拆分5.2 展开说说三个高频案例第一个是 MyBatis 懒加载导致的序列化异常。实体 A 关联实体 BJackson 序列化 A 时会触发 B 的 getter一旦该 getter 触发了懒加载 SQL而 SqlSession 已经关闭就会抛出 LazyInitializationException。这个异常被包装在序列化失败里看堆栈很容易被误导。处理思路有三条关闭懒加载、用 JsonIgnoreProperties 忽略不需要的关联属性、把实体转成 DTO 再输出。在 SSM 项目里我大多数情况选第三种。原因很简单实体是持久化模型JSON 是接口协议模型两者本来就不应该完全重叠用 DTO 隔离不仅能解决懒加载问题还能避免把持久化层字段意外暴露给前端。第二个是中文乱码。MappingJackson2HttpMessageConverter 默认对 application/json 使用 UTF-8 编码但如果你在配置里自己 new 了一个 converter 却没设置 supportedMediaTypes某些容器环境下中文内容就会乱码。遇到中文乱码问题先确认有没有自定义 converter再看 web.xml 里有没有配 CharacterEncodingFilter别一上来就在 ObjectMapper 上瞎调编码设置方向错了事倍功半。第三个是空值输出。默认情况下 Jackson 会把 null 字段也序列化出去响应体里可能出现一堆 field: null。前端拿到 null 之后还要写判空逻辑接口文档看起来也乱。如果团队约定null 不输出在 ObjectMapper 上配 setSerializationInclusion(JsonInclude.Include.NON_NULL) 就能解决。但要提醒一点这个配置会让 null 字段直接消失而不是变成 null如果前端业务逻辑依赖字段是否存在来判断状态配置前必须前后端对好几遍否则线上容易出隐蔽 bug。5.3 排查思路先分清序列化还是反序列化最后分享一个我屡试不爽的定位方法。遇到 JSON 报错第一反应不是去翻 Spring 配置而是先判断问题出在序列化Java 对象变成 JSON还是反序列化JSON 变成 Java 对象。序列化问题大概率跟返回值实体、getter、循环引用、日期类型有关反序列化问题大概率跟请求体字段名、构造函数、setter、未知字段处理有关。判断清楚之后直接脱离 SpringMVC 写一个几十行的 main 方法用 ObjectMapper 单独复现一次序列化或反序列化过程。这个办法能快速排除到底是 Jackson 本身的问题还是 Spring 集成的问题不用反复重启 web 容器效率高很多。我实测下来90% 的序列化问题都能用这个方法独立复现出来之后要改实体还是改配置方向就非常明确。最后再分享一个我在团队里强制执行的小习惯所有 Maven 模块的 Jackson 版本都由顶层 parent 用 properties 统一声明子模块一律不写版本号。这样升级 Jackson 时只改一个地方然后跑一遍全量测试就能评估影响面。我见过几个项目因为各模块各写各的版本升级时一脸懵最后花了一整个下午用 dependency:tree 逐个模块对齐版本才把问题收干净。前人的教训摆在眼前版本这件事真不用等踩坑了才去治理。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门