ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SSM项目Jackson依赖引入与JSON序列化配置实战指南

SSM项目Jackson依赖引入与JSON序列化配置实战指南 SSM 项目跑得久了总会碰到一个特别不起眼、但一旦出错就让人头疼的环节——前后端数据交互时的 JSON 序列化。我见过不少同事在 Controller 里返回一个对象前端拿到的却是 406 错误或者日期字段变成一串看不懂的时间戳排查半天才发现问题出在 Jackson 依赖没引对、没配好。这个坑说大不大但涉及的知识点却不少Maven 坐标、版本选择、SpringMVC 消息转换器、序列化配置、常见异常排查。这篇博文就以“SSM 项目中 Jackson 依赖引入”为线索把我在实际项目里踩过的坑、验证过的方案、排查过的报错一次性梳理清楚给正在做 SSM 开发或者刚接触这套框架的朋友一个可以直接抄作业的参考。1. SSM 项目为什么绕不开 Jackson1.1 先从 SSM 的请求响应链路说起SSM 是 Spring SpringMVC MyBatis 的组合三个框架各管一摊Spring 管 Bean 和事务SpringMVC 管 Web 层请求分发MyBatis 管数据库持久化。前端请求进来之后经过 DispatcherServlet 分发到对应的 Controller 方法方法返回一个 Java 对象SpringMVC 再把这个对象写给浏览器。问题就出在这最后一步——Java 对象怎么变成前端能直接用的 JSON 字符串这个转换动作在 SpringMVC 中是由 HttpMessageConverter消息转换器来完成的。框架内部维护了一批默认的消息转换器其中处理 JSON 的就是 MappingJackson2HttpMessageConverter这个类依赖 Jackson 库。也就是说如果你的 SSM 工程里没有引入 Jackson 依赖SpringMVC 就找不到能处理 JSON 的转换器Controller 返回对象时只能被迫走到其他转换器那里要么直接报 406 给前端要么返回的内容格式乱七八糟。我当时第一次搭 SSM 项目的时候图省事只引了 Spring 的 web 模块没单独加 Jackson结果所有接口全部返回 406浏览器直接显示 Not Acceptable。查了半天才发现Spring 的 web 包只是提供了转换器的抽象框架具体 JSON 实现还是要引进 Jackson 才有。1.2 Jackson 在 SSM 里到底干了什么活Jackson 的核心功能是 Java 对象和 JSON 之间的相互转换。往细了说它有三个模块jackson-core提供底层的 JSON 解析器和生成器定义了 JsonParser、JsonGenerator 这类基础 API。jackson-databind提供数据绑定能力也就是 ObjectMapper 所在的模块负责把 Java 对象序列化成 JSON以及把 JSON 反序列化成 Java 对象。jackson-annotations提供注解支持比如 JsonIgnore、JsonProperty、JsonFormat 这些注解都在这包里。这三个包是有依赖关系的databind 依赖 core 和 annotationscore 和 annotations 彼此独立。所以实际引入的时候只需要把 jackson-databind 的依赖写好Maven 会自动把另外两个拉下来。在 SSM 项目里日常使用最多的是 ObjectMapper 以及各种注解所以 databind 是引入的核心其他两个属于传递依赖。很多初学者以为“Jackson 依赖”就是在 pom 里写一个坐标完事其实坐标是第一步后面还要考虑 SpringMVC 是否识别这个转换器。在 SSM 这种基于 XML 或 JavaConfig 配置的项目里如果 spring-webmvc 的版本和 Jackson 的版本之间出现兼容问题或者消息转换器没有注册到 HandlerAdapter 上依赖引了也是白引接口照样报错。1.3 麻瓜级别的 Jackson 认知这东西没有多神秘网上有个热词叫“麻瓜记录 jackson”我看到之后觉得特别贴切——很多人在初学阶段对 Jackson 的看法就是“一个把对象转成 JSON 的库”。这个理解没错但只停留在表面。我觉得更准确的认知是Jackson 是一个高性能的 JSON 处理框架它在 Java 生态中的地位类似于 Gson、Fastjson但它和 Spring 的集成度最高也是 Spring Boot 默认的 JSON 库。如果从零开始学 SSMJackson 就是你接触到的第一个真正意义上的 Java-JSON 绑定工具。它解决的不是“能不能转 JSON”这个简单问题而是“序列化时怎么处理日期”“字段名怎么映射”“循环引用怎么避免”“空值怎么控制”这一整套完整的 JSON 处理规范。这也是为什么很多人用着用着会发现 Jackson 的配置选项远比想象中丰富。2. Jackson 还是 Fastjson先把这个纠结解决了2.1 为什么很多老项目还在纠结这个问题搜“jackson和fastjson哪个好”的人多半是接手了一个老项目发现里面既有 Jackson 又引了 Fastjson两套序列化方式混着用不知道该怎么统一。还有个原因是早年国内 Fastjson 的知名度确实高因为阿里巴巴开源中文文档齐全性能测试数据也好看很多 SSM 项目里的 JSON 工具类都直接用的 Fastjson。而 SpringMVC 天然默认支持 Jackson这就形成了“项目里有 Jackson 也有 Fastjson”的历史原因。这类问题的本质不是“哪个更好”而是“在 SSM 项目里哪个更容易和 SpringMVC 形成一致的、规范的序列化流程”。我见过最离谱的项目Controller 层用 SpringMVC 默认的 Jackson业务代码里却用 Fastjson 手写转换导致同一个对象的日期格式在接口返回和日志打印时完全不一样。这种混乱比选错库更麻烦。2.2 两个库的客观对比为了让选择更有依据我做了个对比表格都是实际开发中感受最深的几个维度对比维度JacksonFastjsonSpring 集成度SpringMVC/Spring Boot 默认支持需要自己换转换器API 易用性ObjectMapper API 稍显厚重但功能清晰JSON.parseObject 一行搞定上手快日期时间类型支持对 Java 8 时间类型LocalDate、LocalDateTime原生支持只需注册 JS R310 模块早期需要单独处理高版本才优化注解体系JsonFormat、JsonIgnore、JsonProperty 等功能全面JSONField 注解功能类似但语义不同安全维护社区活跃漏洞响应及时历史上曾爆出多个反序列化漏洞引发过争议依赖体积三个模块但都是小体积单包体积较小从性能上说两个库在常规业务场景下差距并不明显日常 CRUD 接口根本体现不出差异。真正拉开差距的是生态契合度和维护安全感。Jackson 背靠 FasterXML 社区又是 Spring 官方默认你在使用中碰到的问题Stack Overflow 上几乎都有现成答案。Fastjson 最大的优势是 API 简单直接但在 SSM 这种已经由 SpringMVC 主导序列化流程的项目里这个优势反而会因为“引入第二套序列化方式”而变成劣势。2.3 我的建议SSM 项目优先 Jackson如果你现在还没有为项目选定 JSON 库我的建议是直接选 Jackson不要犹豫。理由很简单SpringMVC 默认通过 MappingJackson2HttpMessageConverter 处理 JSON引入 Jackson 之后几乎不用额外配置。项目里所有 Controller 返回对象都走同一条序列化链路。前端传 JSON 到后端也用同一个 ObjectMapper 做反序列化技术栈统一排查问题省心。如果项目已经用了 Fastjson并且只是用来做工具类的 JSON 转换那么可以保留但建议在 Web 层把消息转换器统一为 Jackson或者在配置里明确指定使用 Fastjson 的 HttpMessageConverter不要让两种方式混着用。实际上 Fastjson 官方也提供了 fastjson-springmvc 的集成方案只是多一个配置步骤。从我接手过的项目来看统一序列化方案带来的收益远比库本身的那点性能差异要大。3. Maven 依赖引入坐标、版本、传递依赖一次说清3.1 最简引入方式在 SSM 项目的 pom.xml 里引入 Jackson 其实只需要一个依赖dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency这样写就够了因为 jackson-databind 会传递依赖 jackson-core 和 jackson-annotations。我见过有些同事把三个依赖全部手动写上虽然不报错但属于多此一举。写三个坐标的问题在于版本容易漂移——如果三个包版本不一致运行时可能出现 JsonMappingException 这类奇怪错误。所以正确做法是只引 databind其他两个让 Maven 自己决议。3.2 版本选择别用太老的版本也别盲目追新版本选择是有讲究的。我在早期项目里见过有人用 2.5.0那个版本年代久远对 Java 8 时间类型的支持不完善需要额外写自定义序列化器。后来项目升级到 Java 8 之后各种 LocalDateTime 的序列化问题全冒出来了。后来我把版本统一升级到 2.9.x才舒服了一些。现在我的建议是如果你用的是 JDK 8选 2.11.x 到 2.15.x 之间都比较稳妥。JDK 11 及以上直接选 2.15.x 或更新的 2.17.x。版本太新可能引入对 Spring 4 兼容性的问题版本太老又会缺少一些新特性和漏洞修复。SSM 项目大多是 Spring 5.x 之前的版本为了稳定起见推荐 2.13.x 或 2.15.x这两个版本在功能、性能和兼容性之间比较平衡。有个小技巧Spring 框架的版本和 Jackson 的版本是有对应关系的。Spring 5.3.x 对应的默认 Jackson 版本是 2.12.x 左右但实际上 Jackson 是向后兼容的你用了 2.15.x 也完全没有问题。唯一的硬性要求是 JDK 版本比如 Jackson 2.15 要求 JDK 8 以上。所以只要你的 JDK 不低于 8版本可以适当激进一点。3.3 顺带说一嘴Java 的依赖方式和 Python 可不一样搜热词里有个“python引入依赖方法”说明有些朋友可能刚接触完 Python 再来学 Java。这两者的依赖管理差异确实值得提一句Python 用 pip 把包安装到虚拟环境里应用运行时直接 importJava 用 Maven 或 Gradle 来声明依赖构建时 Maven 从中央仓库下载 jar 包传递依赖由 Maven 自动处理。在 Python 里你 pip install 之后需要在代码里 import在 Java 的 SSM 项目里你在 pom.xml 里引入坐标之后代码里直接 import 对应的类不用额外“安装”什么。但要注意Maven 是构建期依赖打包时如果用的是 SpringMVC 标准的 WAR 包Jackson 的 jar 会被打包进 WEB-INF/lib如果用的是 Spring Boot 那套则是全部打进一个 fat jar。SSM 项目大多数是 WAR 包方式所以依赖引入之后要注意最终打包产物里是否包含 Jackson 相关 jar。3.4 依赖冲突与传递依赖排查引入 Jackson 之后最常见的坑是依赖冲突。比如项目中某个第三方组件如 Dubbo、Zookeeper 客户端内部已经引入了低版本的 jackson-core而你又在 pom 里显式声明了一个高版本Maven 默认使用“最近依赖声明优先”的决议规则最终生效的版本不一定是你的那个。排查方式很简单在项目根目录执行mvn dependency:tree -Dincludescom.fasterxml.jackson.core这条命令会把当前工程依赖树中所有 Jackson 相关依赖打印出来你能清楚地看到哪些包传递引入了 low 版本哪些冲突了。处理冲突的常规手段是用 exclusion 排除特定传递依赖dependency groupIdorg.apache.dubbo/groupId artifactIddubbo/artifactId version2.7.15/version exclusions exclusion groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /exclusion /exclusions /dependency我在实际项目中就碰到过一次某个公司自研的 RPC 框架把 jackson-databind 锁死在 2.8.11而项目里的业务代码需要用到 2.13 才有的 LocalDateTime 支持特性。最后就是用 exclusion 把这个传递依赖从 RPC 框架中去掉再在项目层面引入一个统一的版本问题立刻消失。所以版本统一这件事最好在项目做一个统一的 dependencyManagement 管理让所有 Jackson 相关坐标都由一个入口控制版本。dependencyManagement dependencies dependency groupIdcom.fasterxml.jackson/groupId artifactIdjackson-bom/artifactId version2.15.2/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement这个 jackson-bom 是官方提供的版本管理清单引入之后你下面所有 Jackson 坐标都可以不写 versionMaven 会统一按照 BOM 中的版本来决议。这个做法是解决版本碎片化最优雅的方案。4. 引入之后的整合配置让 SpringMVC 真正用起来4.1 消息转换器的注册机制很多人以为依赖引进去了SpringMVC 就能自动处理 JSON 了。大部分情况下确实能但有一个前提配置文件里要启用注解驱动。如果 SSM 项目的 springmvc.xml 里没有配置mvc:annotation-driven /那么 SpringMVC 的默认转换器列表里可能不会注册 MappingJackson2HttpMessageConverter。这一点是新手最容易踩的坑。mvc:annotation-driven /标签干的事情很多其中一件就是自动注册 JSON 消息转换器——前提是 classpath 下能找得到 Jackson 的类。也就是说依赖引入 启用注解驱动这两者缺一不可。如果你引了依赖但没启用注解驱动接口不报错但返回的可能是乱写的内容或者直接 406。4.2 XML 配置方式SSM 项目的经典姿势传统的 SSM 项目大多是 XML 配置在 springmvc.xml 里做扩展也很简单mvc:annotation-driven mvc:message-converters bean classorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter property nameobjectMapper refjacksonObjectMapper / /bean /mvc:message-converters /mvc:annotation-driven bean idjacksonObjectMapper classorg.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean property nameserializationInclusion valueNON_NULL / property namedateFormat bean classjava.text.SimpleDateFormat constructor-arg valueyyyy-MM-dd HH:mm:ss / /bean /property /bean我建议在配置里单独定义 ObjectMapper 的 Bean而不是直接用框架内部的默认实例。原因很简单默认 ObjectMapper 的日期格式是时间戳不是你通常想要的 “yyyy-MM-dd HH:mm:ss”默认序列化时会把 null 值也输出出来生成的 JSON 就很冗长。自己定义一个 ObjectMapper Bean可以在一开始就把这些规则定好全项目共享。注意我这里用了 Jackson2ObjectMapperFactoryBean这是 Spring 提供的工厂 Bean它会在创建 ObjectMapper 的同时自动注册一些常用模块比如 Java 8 时间支持模块。如果你手写 new ObjectMapper()就得自己去注册这些模块比较繁琐。4.3 JavaConfig 配置方式现在的新项目更常见如果你不想用 XMLSSM 项目也可以用 JavaConfig。原理一样只是换个写法Configuration public class WebConfig implements WebMvcConfigurer { Bean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter converter new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper new ObjectMapper(); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); objectMapper.setTimeZone(TimeZone.getTimeZone(GMT8)); converter.setObjectMapper(objectMapper); return converter; } Override public void configureMessageConverters(ListHttpMessageConverter? converters) { converters.add(mappingJackson2HttpMessageConverter()); } }在 JavaConfig 里配置 ObjectMapper 有一个好处可以更直观地看到所有定制项不像 XML 那样需要查标签属性。有一点要提醒ObjectMapper 是线程安全的配置好之后可以全局共享不需要每个请求都 new 一个。Spring 容器里把它定义成单例 Bean 就对了。4.4 高级参数配置解决实际业务痛点用到后面你会发现Jackson 最强大的地方在于各种序列化行为的定制。以下配置是我常用且验证过稳定的方案直接在 ObjectMapper 上设置就行忽略未知字段接口演进时前端多传一个字段后端不会直接抛 UnrecognizedPropertyException。配置方式是objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)或者直接在类上标JsonIgnoreProperties(ignoreUnknown true)。空值不输出objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)字段值为 null 时不输出在 JSON 里。这个在前后端联动时尤其好用可以避免前端拿到 null 字段还要判空。日期格式化建议同时配置 dateFormat 和 timeZone否则服务器部署在海外时时间会有时区偏差。常见是objectMapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss))配合objectMapper.setTimeZone(TimeZone.getTimeZone(GMT8))。Java 8 时间类型支持如果项目用了 JDK 8 的 LocalDate/LocalDateTime需要注册 JavaTimeModule并禁用 WRITE_DATES_AS_TIMESTAMPS。Spring 的 Jackson2ObjectMapperFactoryBean 会自动注册手动 new ObjectMapper 时就要自己加上objectMapper.registerModule(new JavaTimeModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);你可以在项目里统一封装一个 JacksonConfig把这些配置集中管理。我看到一些做得规范的项目连实体类上的字段注解都做了限制前端命名习惯是下划线风格就在实体类上用JsonProperty(user_name)做映射避免为前端单独建 DTO。5. 引入后的高频异常与排查记录5.1 引入依赖之后接口仍然报 406这是我在解答新人问题时碰到最多的情况。报 406 意味着服务器无法返回客户端要求的表示形式本质上是没有找到合适的消息转换器。排查步骤按顺序来确认 pom.xml 里是否真的引入了 jackson-databind。注意检查 scope 是不是 provided 或 test如果是发布时不会带包。确认 spring 配置文件里是否启用了mvc:annotation-driven /。没有这一行注解驱动的消息转换器不会注册。确认项目的 lib 目录中最终是否有 jackson 相关的 jar 包。如果用 Maven 打 WAR 包看一下 target 目录下 WEB-INF/lib 里有没有这类的 jar。有一次我帮同事排查pom 里明明写了依赖启动也不报错但接口就是 406。最后发现他在 Maven 的 settings.xml 里配了私服镜像中央仓库的包下载失败被静默跳过了本地仓库里的 jar 是不完整的。重新拉取完就正常了。所以 406 这个错不一定是代码问题环境问题也占很大比例。5.2 NoClassDefFoundErrorJackson 类找不到如果在启动阶段出现NoClassDefFoundError: com/fasterxml/jackson/core/JsonParser这类错误说明运行时使用的不是 Maven 里声明的那套依赖。我遇到过一种典型情况项目用 Maven 构建但构建产物是放在 Tomcat 的 webapps 下而 Tomcat 的 lib 目录里有一个旧版本的 Jackson jar于是出现类冲突。另一个典型情况是依赖冲突导致 Maven 实际解析到的是一个被裁剪过的版本类定义不完整。排查这类问题的组合拳是先看mvn dependency:tree确认实际版本再看启动日志中的类加载路径最后检查部署环境的 lib 目录有没有多余的 jar。如果是 Tomcat 级别引入的旧 Jackson最简单的办法是把它从 Tomcat/lib 删掉或者在构建时用打包插件排除掉冲突夹。5.3 日期字段变成时间戳或时区错乱很多人在第一次用 Jackson 返回日期字段时发现前端拿到的不是 “2019-01-01 12:00:00”而是一个 15462 开头的长数字。这是因为 ObjectMapper 默认把 java.util.Date 序列化成 epoch 时间戳。解决方案有两种全局配置在前面的章节提过局部配置可以用实体类注解public class UserVO { JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private Date createTime; }注意 timezone 一定要写否则服务器时区不是东八区时返回时间会差 8 个小时。这个问题在部署到云服务器后特别常见——本地用 IntelliJ 自带时区没问题上服务器后时间对比总是差 8 小时排查到最后往往是忘记设置 timezone。如果你用的是 JDK 8 的 LocalDateTime那就要在 ObjectMapper 上注册 JavaTimeModule用 JsonFormat 也可以单独处理。我个人的习惯是全局配置 局部注解双保险全局把默认格式定为 “yyyy-MM-dd HH:mm:ss”遇到个别字段需要特殊格式时再用 JsonFormat 覆盖。5.4 循环引用导致 StackOverflow两个实体类互相引用比如订单里有用户对象、用户对象里又有订单列表直接用 Jackson 序列化就会无限循环最终抛出 StackOverflowError。这个坑在从数据库查询后直接返回实体对象时非常容易出现也是一开始不建议把实体类直接当 VO 返回的原因之一。常用的解决手段有三种在字段上加JsonIgnore完全忽略某个字段简单粗暴。在反序列化侧用JsonManagedReference和JsonBackReference一对注解解决父子循环引用。更深层的方案是使用JsonIdentityInfo让 Jackson 在遇到同一个对象时直接引用而不是展开序列化适合对象图中同一对象被多次引用的场景。我最推荐的是第三种思路但实际项目里最省事的还是第一种。如果你正在用 MyBatis从数据库查出来的对象往往包含大量联表查询关联出来的子对象直接序列化的话除了循环引用问题还会把不必暴露的字段全部打出去。所以我的经验是用单独的 VO 类来定义接口返回结构关联字段用简单类型或扁平化处理循环引用问题从源头就不存在了。5.5 前后端字段命名风格不一致服务端 Java 习惯用驼峰命名userName而前端很多项目风格是下划线命名user_name。如果对接的是老系统字段命名风格不统一尤其常见。处理方式是在实体类上用 JsonProperty 显式映射public class UserVO { JsonProperty(user_id) private Long userId; JsonProperty(user_name) private String userName; }这样后端代码不需要改成下划线风格Jackson 在序列化时会把 userId 输出为 user_id前端拿到的字段名就统一了。如果字段特别多还可以用类级别的JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)整个类的所有字段都自动转成下划线命名。这个方法在对接 Python 后端或老前端服务时特别管用。6. 写在最后我的体会与一个小技巧从“引入一个依赖”这个需求出发一路写到版本管理、消息转换器、配置对象、异常排查其实已经覆盖了 SSM 项目里 JSON 序列化的绝大部分知识点。我个人在实际项目里的体会是Jackson 的问题很少是“不会用”引起的更多是“没想清楚”造成的。每次报错都值得从依赖、配置、对象结构三个层面各过一遍而不是单纯改代码。这个小技巧叫“三层排查法”帮我解决过很多线上问题。最后再分享一个小技巧如果你接手了一个找不到原因的 JSON 序列化问题不要光看代码先写一个带 main 方法的测试类用本地 ObjectMapper 直接序列化那个报错的实体对象。这一步能把问题快速定位到是“实体对象结构”的问题还是“SpringMVC 配置”的问题。我多次靠这个办法几分钟内就从一堆报错日志里找到了根源。Jackson 这套工具用好了是真的省心但前提是你得先把依赖引对、把配置配清楚。
返回列表