不是 ObjectMapper 难用,是咱没 get 到它在 SpringBoot 里的 “优雅姿势”。今天就跟大家唠唠,怎么把 ObjectMapper 用得顺风顺水,既不用在业务代码里堆一堆转换逻辑,又能避免那些让人头大的 bug,看完这篇,保准你想把之前的代码重构一遍(狗头)。
兄弟们,咱谁没跟 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;
// 特殊情况: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
);
};
}
// 金额序列化器: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 里,最佳实践是:
- 用@Autowired注入 SpringBoot 自动配置的 ObjectMapper(或者自己用Jackson2ObjectMapperBuilderCustomizer配置的)
- 不要每次用都 new ObjectMapper
- 不要在多线程环境下修改 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 的自动配置逻辑,或者没掌握它的高级用法。
AI大模型学习福利
作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量
859

被折叠的 条评论
为什么被折叠?



