兄弟们,咱谁没跟 ObjectMapper 打过交道啊?每次跟 JSON 打交道,不是日期格式突然冒出个 “T” 让前端小姐姐追着问,就是 null 值莫名消失被测试怼 “接口漏字段”,更绝的是 —— 明明字段名对着呢,反序列化完字段全是 null,当时真想把键盘拍在桌上喊 “这玩意儿咋不按套路出牌”!其实啊,不是 ObjectMapper 难用,是咱没 get 到它在 SpringBoot 里的 “优雅姿势”。今天就跟大家唠唠,怎么把 ObjectMapper 用得顺风顺水,既不用在业务代码里堆一堆转换逻辑,又能避免那些让人头大的 bug,看完这篇,保准你想把之前的代码重构一遍(狗头)。
一、别再瞎 new ObjectMapper 了!SpringBoot 早帮你安排了
先问大家一个问题:你是不是写过这样的代码?
// 是不是你?
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(user);
User user = objectMapper.readValue(json, User.class);
要是你点头了,那咱得先纠正这个小习惯 ——别再每次用都 new ObjectMapper 了!SpringBoot 早就帮咱们做了自动配置,在JacksonAutoConfiguration里,已经默认创建了一个 ObjectMapper 实例,还帮咱们配了不少基础参数。你直接用@Autowired注入就行,既不用自己管理生命周期,还能跟 SpringBoot 的其他组件(比如消息转换器、接口返回值处理)无缝衔接。
不信你看,咱随便写个 Service:
@Service
public class UserService {
// 直接注入,不用自己new!
private final ObjectMapper objectMapper;
// 构造器注入,Spring推荐姿势
public UserService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public String getUserJson(User user) throws JsonProcessingException {
// 直接用,省心!
return objectMapper.writeValueAsString(user);
}
}
有人可能会问:“我自己 new,想配啥配啥,不行吗?”还真不行!你自己 new 的 ObjectMapper,跟 SpringBoot 默认的不是一个实例。比如你在application.yml里配了全局日期格式,自己 new 的那个根本读不到这个配置,到时候就会出现 “我明明配了啊,怎么没生效” 的迷惑行为。更坑的是,要是你在 Controller 里返回对象,SpringBoot 会用它自己的 ObjectMapper 来序列化,你自己 new 的那个配置完全没用,等于白忙活。所以听我的,先把 “Autowired 注入 ObjectMapper” 这个习惯养成,咱再谈后续优化。
二、日期处理:从 “T 乱入” 到 “格式自由”
日期处理绝对是 ObjectMapper 的 “重灾区”,没有之一。上次我同事小王写了个用户列表接口,返回的日期是2024-05-20T13:14:00.000+08:00,前端小姐姐拿着截图来找他:“王哥,这日期里的‘T’是啥意思啊?是要我备注‘今天适合表白’吗?” 小王当场社死,后来查了半小时才知道,这是 ObjectMapper 默认的日期格式 ——ISO 8601 标准,但前端根本不认这个 “T”,还得转成yyyy-MM-dd HH:mm:ss才行。
2.1 全局配置:一招解决所有日期格式问题
最优雅的方式,就是在application.yml(或application.properties)里配全局日期格式,这样所有用 SpringBoot 默认 ObjectMapper 序列化的日期,都会按这个格式来,不用在每个字段上写注解。
# application.yml
spring:
jackson:
# 日期格式:全局统一成 yyyy-MM-dd HH:mm:ss
date-format: yyyy-MM-dd HH:mm:ss
# 时区:必须配!不然会有8小时时差
time-zone: GMT+8
# 针对LocalDateTime等JDK8新日期类型的配置
deserialization:
adjust-dates-to-context-time-zone: false
这里有个坑必须提醒大家:时区一定要配! 要是没配time-zone: GMT+8,ObjectMapper 会默认用 UTC 时区,结果就是返回的日期比实际少 8 小时,比如你本地是 2024-05-20 13:14,序列化后变成 2024-05-20 05:14,到时候前端以为你接口返回的是昨天的数据,能把你怼到怀疑人生。要是你用的是 JDK8 的新日期类型(LocalDateTime、LocalDate),光配date-format还不够,因为date-format是针对java.util.Date的。这时候得加个依赖,让 Jackson 支持 JDK8 日期类型:
<!-- pom.xml 加这个依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<!-- SpringBoot父工程已经管理了版本,不用自己写version -->
</dependency>
加了依赖后,再在application.yml里配 JDK8 日期的格式:
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 配置LocalDateTime的格式
Java-time-module:
date-time-formatter: yyyy-MM-dd HH:mm:ss
这样不管是Date还是LocalDateTime,序列化后都是yyyy-MM-dd HH:mm:ss,前端再也不用问你 “T 是啥” 了。
2.2 局部调整:个别字段要特殊格式怎么办?
有时候全局格式是yyyy-MM-dd HH:mm:ss,但某个字段需要yyyy-MM-dd(比如用户的生日,不用时分秒),这时候用@JsonFormat注解就能搞定,局部配置会覆盖全局配置,非常灵活。
public class User {
private Long id;
private String userName;
// 生日只需要日期,局部配置格式
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private LocalDate birthday;
// 注册时间用全局格式,但这里可以显式指定(可选)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime registerTime;
// getter/setter 省略
}
这里要注意:@JsonFormat里的timezone也得写,不然会继承全局的时区,但要是你局部格式没写时分秒,时区配置不影响结果,不过写上更稳妥,避免踩坑。
三、null 值处理:留还是删?别让前端跟你吵架
另一个高频痛点:null 值消失。比如你返回的 User 对象里,nickName是 null,序列化后 JSON 里直接没这个字段了,前端拿到数据一看:“哎?nickName 呢?你接口漏字段了吧!” 你查代码发现字段明明在,就是值为 null,这时候才反应过来 ——ObjectMapper 默认会忽略 null 值。
3.1 全局配置:决定 null 值要不要显示
想让所有 null 值都显示在 JSON 里,直接在application.yml里配:
spring:
jackson:
# 序列化时包含所有字段,包括null值
serialization:
include-null-map-values: true
# 更直接的配置:所有null值都包含
default-property-inclusion: ALWAYS
default-property-inclusion有四个可选值,咱解释一下:
ALWAYS:不管是不是 null,都包含字段(最常用)
NON_NULL:忽略 null 值(默认)
NON_EMPTY:忽略 null、空字符串("")、空集合(比如 [])
NON_DEFAULT:忽略字段值等于默认值的(比如 int 字段 0,boolean 字段 false)
比如你想忽略空字符串和空集合,但保留 null 值,就配NON_EMPTY:
spring:
jackson:
default-property-inclusion: NON_EMPTY
这样一来,nickName: null会显示,address: ""(空字符串)和hobbies: [](空集合)会被忽略,很灵活。
3.2 局部控制:个别字段特殊处理
要是全局配置是ALWAYS(显示所有 null),但某个字段是 null 时不想显示,比如password字段(用户没传的话,null 值没必要返回),用@JsonInclude注解就行:
public class User {
private Long id;
privateString userName;
// 要是password是null,序列化时忽略这个字段
@JsonInclude(JsonInclude.Include.NON_NULL)
privateString password;
// 要是nickName是空字符串或null,都忽略
@JsonInclude(JsonInclude.Include.NON_EMPTY)
privateString nickName;
// getter/setter 省略
}
@JsonInclude的取值和全局配置的default-property-inclusion对应,局部配置会覆盖全局,比如全局是ALWAYS,但password用了NON_NULL,那password为 null 时就会被忽略,其他字段还是显示 null。这里插个小技巧:要是你想让某个字段 “永远显示”,哪怕是 null,就用@JsonInclude(JsonInclude.Include.ALWAYS),不管全局怎么配,这个字段都会显示,适合那些前端必须拿到的字段(比如userId,哪怕是 null,前端也要知道这个字段存在)。
四、字段名映射:camelCase 和 snake_case 的 “和解方案”
Java 里我们习惯用驼峰命名(camelCase),比如userName、registerTime,但前端有时候用下划线命名(snake_case),比如user_name、register_time,这时候反序列化就会出问题 —— 前端传user_name,你用userName接收,结果userName是 null,因为字段名对不上。以前我见过有人这么解决:在每个字段上写@JsonProperty注解,指定下划线的字段名:
public class User {
@JsonProperty("user_id")
private Long userId;
@JsonProperty("user_name")
private String userName;
@JsonProperty("register_time")
private LocalDateTime registerTime;
// getter/setter 省略
}
这么写能解决问题,但字段多了的话,每个都要加注解,手都酸了,而且容易漏写。其实 SpringBoot 里配个全局字段命名策略,就能让 camelCase 自动转 snake_case,不用写一个注解。
4.1 全局配置:驼峰自动转下划线
在application.yml里加一行配置:
spring:
jackson:
# 字段命名策略:驼峰转下划线(snake_case)
property-naming-strategy: SNAKE_CASE
这样一来,你 Java 类里的userName,序列化后会变成user_name;前端传user_name,反序列化时也会自动映射到userName,完美!除了SNAKE_CASE,还有其他命名策略,比如:
CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES:和SNAKE_CASE一样,驼峰转下划线(老版本的写法,现在推荐用SNAKE_CASE)
PASCAL_CASE_TO_CAMEL_CASE:帕斯卡命名(首字母大写,比如 UserName)转驼峰(userName)
LOWER_CASE:所有字母小写,比如userName转username
比如你前端用帕斯卡命名(UserName),就配PASCAL_CASE_TO_CAMEL_CASE,Java 里的userName会自动和前端的UserName映射。
4.2 局部特殊:个别字段不按全局规则来
要是全局是SNAKE_CASE,但某个字段需要特殊命名,比如userId要映射到user_id,但phoneNumber要映射到mobile(不是phone_number),这时候用@JsonProperty注解覆盖全局规则:
public class User {
// 全局是SNAKE_CASE,这里会自动转user_id,不用写@JsonProperty
private Long userId;
// 堆代码 duidaima.com
// 特殊情况:phoneNumber要映射到mobile,用@JsonProperty指定
@JsonProperty("mobile")
private String phoneNumber;
// getter/setter 省略
}
这样既保留了全局的便捷性,又能处理特殊字段,优雅!
五、复杂类型:List变 Map?TypeReference 救场
处理 List、Map 这些复杂类型时,很多人会踩一个坑:反序列化 List 的时候,拿到的不是List<User>,而是List<LinkedHashMap>,遍历的时候强转User直接报错。比如你写了这样的代码:
// 前端传的JSON数组
String userJson = "[{\"user_name\":\"张三\",\"age\":20},{\"user_name\":\"李四\",\"age\":22}]";
// 想反序列化成List<User>
List<User> userList = objectMapper.readValue(userJson, List.class);
// 遍历的时候强转,报错!
for (User user : userList) { // ClassCastException: LinkedHashMap cannot be cast to User
System.out.println(user.getUserName());
}
为啥会这样?因为 Java 的 “泛型擦除”—— 编译的时候List<User>会变成List,ObjectMapper 不知道你要反序列化成User对象,就默认转成LinkedHashMap(JSON 对象转 Map)。这时候就得用TypeReference来告诉 ObjectMapper:“我要的是List<User>,不是普通的 List!”
5.1 用 TypeReference 处理 List
正确的写法是这样的:
String userJson = "[{\"user_name\":\"张三\",\"age\":20},{\"user_name\":\"李四\",\"age\":22}]";
// 用TypeReference指定泛型类型
List<User> userList = objectMapper.readValue(userJson, new TypeReference<List<User>>() {});
// 遍历,没问题!
for (User user : userList) {
System.out.println(user.getUserName()); // 正常输出:张三、李四
}
TypeReference是 Jackson 提供的一个抽象类,通过匿名内部类的方式,保留了泛型的具体类型(因为匿名内部类会在编译时生成 class 文件,泛型信息不会被擦除),ObjectMapper 就能知道要转成List<User>了。
5.2 处理嵌套泛型:比如 Map<String, List>
要是更复杂一点,比如 JSON 是{"male":[{"user_name":"张三"}], "female":[{"user_name":"李四"}]},想转成Map<String, List<User>>,同样用TypeReference:
String json = "{\"male\":[{\"user_name\":\"张三\",\"age\":20}], \"female\":[{\"user_name\":\"李四\",\"age\":22}]}";
// 嵌套泛型也能搞定
Map<String, List<User>> genderMap = objectMapper.readValue(json, new TypeReference<Map<String, List<User>>>() {});
// 取值
List<User> maleUsers = genderMap.get("male");
System.out.println(maleUsers.get(0).getUserName()); // 张三
这里要注意:TypeReference的匿名内部类不能复用,比如你不能写个public class UserListTypeReference extends TypeReference<List<User>> {}然后反复用,虽然能跑,但可能会有线程安全问题(Jackson 官方不推荐),最好每次用的时候都 new 一个匿名内部类,虽然代码看起来重复,但安全第一。
六、自定义序列化:让性别 1→“男”,不用再写 if-else
有时候我们需要对字段做特殊转换,比如数据库里存的性别是 1(男)、2(女),但接口要返回 “男”、“女”;或者金额存的是分(比如 1000 分 = 10 元),接口要返回元(10.00 元)。要是在业务代码里写 if-else 转换,比如:
// 不优雅的写法:业务代码里混着格式转换
public UserVO convert(User user) {
UserVO vo = new UserVO();
vo.setUserName(user.getUserName());
// 性别转换:1→男,2→女
if (user.getGender() == 1) {
vo.setGender("男");
} elseif (user.getGender() == 2) {
vo.setGender("女");
} else {
vo.setGender("未知");
}
// 金额转换:分→元
vo.setBalance(user.getBalance() / 100.00);
return vo;
}
这样写能实现功能,但业务代码和格式转换混在一起,要是有多个地方需要转换,就会写一堆重复代码,维护起来麻烦。这时候用 ObjectMapper 的自定义序列化器,就能把转换逻辑抽离出来,一劳永逸。
6.1 写个自定义序列化器:处理性别转换
首先,写一个序列化器,继承StdSerializer,重写serialize方法:
// 性别序列化器:Integer(1/2)→ String(男/女)
publicclass GenderSerializer extends StdSerializer<Integer> {
// 必须写无参构造器,不然Jackson会报错
public GenderSerializer() {
this(null);
}
protected GenderSerializer(Class<Integer> t) {
super(t);
}
// 核心方法:转换逻辑
@Override
public void serialize(Integer gender, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 转换逻辑:1→男,2→女,其他→未知
String genderStr = switch (gender) {
case1 -> "男";
case2 -> "女";
default -> "未知";
};
// 把转换后的值写入JSON
gen.writeString(genderStr);
}
}
然后,在需要转换的字段上用@JsonSerialize注解指定这个序列化器:
public class User {
private Long id;
private String userName;
// 用自定义序列化器处理gender字段
@JsonSerialize(using = GenderSerializer.class)
private Integer gender; // 1→男,2→女
// getter/setter 省略
}
这样一来,序列化 User 对象时,gender: 1会自动变成"gender": "男",不用在业务代码里写 if-else 了,清爽!
6.2 自定义反序列化器:前端传 “男”→后端存 1
要是前端传的是 “男”、“女”,后端需要转成 1、2 存数据库,就需要自定义反序列化器,继承StdDeserializer:
// 性别反序列化器:String(男/女)→ Integer(1/2)
publicclass GenderDeserializer extends StdDeserializer<Integer> {
public GenderDeserializer() {
this(null);
}
protected GenderDeserializer(Class<?> vc) {
super(vc);
}
// 核心方法:反序列化逻辑
@Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 拿到前端传的字符串(比如“男”)
String genderStr = p.getText();
// 转换逻辑:男→1,女→2,其他→0(未知)
returnswitch (genderStr) {
case"男" -> 1;
case"女" -> 2;
default -> 0;
};
}
}
然后在字段上用@JsonDeserialize注解指定:
public class User {
private Long id;
private String userName;
// 序列化用GenderSerializer,反序列化用GenderDeserializer
@JsonSerialize(using = GenderSerializer.class)
@JsonDeserialize(using = GenderDeserializer.class)
private Integer gender;
// getter/setter 省略
}
这样前端传"gender": "男",反序列化后gender就是 1;后端存 1,序列化后返回"gender": "男",完美闭环。
6.3 全局注册自定义序列化器
要是很多字段都需要用同一个序列化器(比如所有性别字段),每个字段都加@JsonSerialize太麻烦,这时候可以全局注册序列化器。在 SpringBoot 里,写一个Jackson2ObjectMapperBuilderCustomizer的 Bean,把自定义序列化器注册进去:
@Configuration
publicclass JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
// 全局注册性别序列化器:所有Integer类型的gender字段都用这个序列化器
builder.serializerByType(Integer.class, new GenderSerializer());
// 全局注册性别反序列化器
builder.deserializerByType(Integer.class, new GenderDeserializer());
// 要是只想针对特定字段(比如字段名叫gender),可以用Module
SimpleModule module = new SimpleModule();
// 这里的“gender”是字段名,指定这个字段用GenderSerializer
module.addSerializer("gender", new GenderSerializer());
module.addDeserializer("gender", new GenderDeserializer());
builder.modules(module);
};
}
}
这里有两种方式:
serializerByType:按类型注册,比如所有Integer类型的字段都用这个序列化器(适合所有同类型字段都需要转换的场景)
addSerializer(字段名, 序列化器):按字段名注册,只有指定字段名的字段才用这个序列化器(适合特定字段的场景)
根据自己的需求选就行,全局注册后,就不用在每个字段上写注解了,更高效。
七、SpringBoot 高级配置:Jackson2ObjectMapperBuilderCustomizer 才是王道
前面我们讲了很多配置,比如全局日期格式、字段命名策略、自定义序列化器,有些是在application.yml里配的,有些是用 Bean 配置的。其实 SpringBoot 推荐用Jackson2ObjectMapperBuilderCustomizer来统一管理所有 ObjectMapper 的配置,这样所有配置都在一个地方,方便维护。Jackson2ObjectMapperBuilderCustomizer是一个函数式接口,通过它可以自定义Jackson2ObjectMapperBuilder,而Jackson2ObjectMapperBuilder又会用来创建 ObjectMapper 实例,所以用它配置,能覆盖所有 ObjectMapper 的参数。
咱写一个完整的配置类,把前面讲的痛点解决方案都整合进去:
@Configuration
publicclass JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
// 函数式接口,用lambda表达式实现
return builder -> {
// 1. 日期配置
builder.dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")) // Date类型格式
.timeZone(TimeZone.getTimeZone("GMT+8")) // 时区
.modules(new JavaTimeModule() // JDK8日期类型支持
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))));
// 2. null值处理
builder.serializationInclusion(JsonInclude.Include.ALWAYS) // 显示所有null值
.featuresToEnable(SerializationFeature.INDENT_OUTPUT) // 格式化JSON(开发环境用,生产环境关闭)
.featuresToDisable(SerializationFeature.WRITE_NULL_MAP_VALUES); // 忽略Map中的null值(可选)
// 3. 字段命名策略
builder.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); // 驼峰转下划线
// 4. 自定义序列化器/反序列化器
SimpleModule module = new SimpleModule();
module.addSerializer(Integer.class, new GenderSerializer()) // 性别序列化
.addDeserializer(Integer.class, new GenderDeserializer()) // 性别反序列化
.addSerializer(Long.class, new MoneySerializer()) // 金额序列化(分→元)
.addDeserializer(Long.class, new MoneyDeserializer()); // 金额反序列化(元→分)
builder.modules(module);
// 5. 其他配置:比如允许单引号、允许非标准JSON格式
builder.featuresToEnable(
JsonParser.Feature.ALLOW_SINGLE_QUOTES, // 允许JSON里用单引号(比如'user_name':'张三')
JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, // 允许字段名不加引号(比如user_name:'张三')
DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT // 空字符串转null
);
};
}
// 堆代码 duidaima.com
// 金额序列化器:Long(分)→ Double(元)
staticclass MoneySerializer extends StdSerializer<Long> {
public MoneySerializer() {
this(null);
}
protected MoneySerializer(Class<Long> t) {
super(t);
}
@Override
public void serialize(Long money, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 分转元,保留两位小数
gen.writeNumber(BigDecimal.valueOf(money).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).doubleValue());
}
}
// 金额反序列化器:Double(元)→ Long(分)
staticclass MoneyDeserializer extends StdDeserializer<Long> {
public MoneyDeserializer() {
this(null);
}
protected MoneyDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// 元转分,四舍五入
Double moneyDouble = p.getDoubleValue();
return BigDecimal.valueOf(moneyDouble).multiply(new BigDecimal(100)).setScale(0, RoundingMode.HALF_UP).longValue();
}
}
}
这个配置类整合了:
.日期处理(Date 和 LocalDateTime 都搞定)
.null 值显示
.字段名驼峰转下划线
.性别和金额的自定义序列化 / 反序列化
.允许单引号、空字符串转 null 等友好配置
这样一来,所有 ObjectMapper 的配置都在一个地方,后续要修改某个配置,直接改这里就行,不用到处找,非常优雅。
这里提个小建议:开发环境可以开启SerializationFeature.INDENT_OUTPUT(格式化 JSON),方便调试;生产环境要关闭,因为格式化会增加 JSON 的体积,影响接口性能。
八、性能优化:ObjectMapper 线程安全,别再 “买杯子” 了
之前我们说过 “别瞎 new ObjectMapper”,除了配置不生效的问题,还有性能问题。ObjectMapper 的创建成本很高,它需要加载很多模块、初始化序列化器 / 反序列化器,要是每次用都 new 一个,就像每次喝水都新买个杯子,喝完就扔,太浪费资源了。而且 ObjectMapper 是线程安全的!只要初始化后不修改它的配置(比如不调用setDateFormat、registerModule这些方法),多个线程同时用它序列化 / 反序列化,完全没问题。
所以在 SpringBoot 里,最佳实践是:
1.用@Autowired注入 SpringBoot 自动配置的 ObjectMapper(或者自己用Jackson2ObjectMapperBuilderCustomizer配置的)
2.不要每次用都 new ObjectMapper
3.不要在多线程环境下修改 ObjectMapper 的配置
比如你写个工具类,也应该注入 ObjectMapper,而不是自己 new:
@Component
publicclass JsonUtils {
privatefinal ObjectMapper objectMapper;
// 注入,不是new!
public JsonUtils(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
// 序列化
public <T> String toJson(T obj) throws JsonProcessingException {
return objectMapper.writeValueAsString(obj);
}
// 反序列化
public <T> T fromJson(String json, Class<T> clazz) throws JsonProcessingException {
return objectMapper.readValue(json, clazz);
}
// 反序列化复杂类型(List、Map)
public <T> T fromJson(String json, TypeReference<T> typeReference) throws JsonProcessingException {
return objectMapper.readValue(json, typeReference);
}
}
这样工具类里的 ObjectMapper 是单例的,性能好,而且配置和 SpringBoot 全局一致,不会出现配置不生效的问题。
九、异常处理:JSON 错了别返回 500,友好点
最后再聊聊异常处理。当 ObjectMapper 序列化 / 反序列化出错时(比如 JSON 格式错误、字段类型不匹配),会抛出JsonProcessingException(序列化)或JsonMappingException(反序列化)。要是不处理这些异常,SpringBoot 会默认返回 500 错误,前端看到 “服务器内部错误”,根本不知道哪里错了。咱得捕获这些异常,返回友好的提示,比如 “JSON 格式错误,请检查参数”。
9.1 全局异常处理:用 @RestControllerAdvice
写一个全局异常处理器,捕获 Jackson 相关的异常:
@RestControllerAdvice
publicclass GlobalExceptionHandler {
// 捕获序列化异常(比如对象里有循环引用)
@ExceptionHandler(JsonProcessingException.class)
public ResponseEntity<ErrorResult> handleJsonProcessingException(JsonProcessingException e) {
ErrorResult result = new ErrorResult(
HttpStatus.BAD_REQUEST.value(),
"JSON序列化失败:" + e.getMessage()
);
returnnew ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
}
// 捕获反序列化异常(比如JSON格式错、字段类型不匹配)
@ExceptionHandler(JsonMappingException.class)
public ResponseEntity<ErrorResult> handleJsonMappingException(JsonMappingException e) {
// 提取错误字段(比如哪个字段类型不匹配)
String field = e.getPath().stream()
.map(JsonMappingException.Reference::getFieldName)
.findFirst()
.orElse("未知字段");
String message = "JSON反序列化失败:字段[" + field + "]" + e.getOriginalMessage();
ErrorResult result = new ErrorResult(
HttpStatus.BAD_REQUEST.value(),
message
);
returnnew ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
}
// 错误响应体
@Data
@AllArgsConstructor
@NoArgsConstructor
publicstaticclass ErrorResult {
private Integer code; // 错误码
private String message; // 错误信息
}
}
这样一来,当出现 JSON 错误时,会返回:
{
"code": 400,
"message": "JSON反序列化失败:字段[age]Cannot deserialize value of type `java.lang.Integer` from String \"二十\": not a valid Integer value"
}
前端能清楚知道是哪个字段错了,错在哪里,不用再跟后端反复沟通 “我传的参数没问题啊”,效率高多了。
十、总结:优雅使用 ObjectMapper 的 9 个要点
唠了这么多,最后总结一下,SpringBoot 里优雅用 ObjectMapper 的核心就是这 9 点:
.别瞎 new:用@Autowired注入 SpringBoot 自动配置的 ObjectMapper,别自己 new;
.全局配置优先:日期格式、null 值处理、字段命名策略,先在application.yml或Jackson2ObjectMapperBuilderCustomizer里配全局的,减少重复代码;
.局部配置补充:个别字段特殊需求,用@JsonFormat、@JsonInclude、@JsonProperty等注解覆盖全局;
.复杂类型用 TypeReference:反序列化 List、Map 等泛型类型,一定要用new TypeReference<>() {};
.自定义序列化抽离逻辑:特殊转换(比如性别、金额)用自定义序列化器,别在业务代码里堆 if-else;
.全局注册序列化器:多个字段用同一个序列化器,全局注册比每个字段加注解更高效;
.线程安全要记住:ObjectMapper 是线程安全的,初始化一次全局用,别频繁 new;
.开发生产环境区分:开发环境开启 JSON 格式化,生产环境关闭,兼顾调试和性能;
.异常处理要友好:捕获 Jackson 异常,返回明确的错误信息,别让前端猜。
其实 ObjectMapper 这玩意儿,你用顺了之后会发现,它比你想象中灵活多了。以前踩的那些坑,大多是因为没搞懂 SpringBoot 的自动配置逻辑,或者没掌握它的高级用法。