偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

SpringBoot中這樣用ObjectMapper,才夠優(yōu)雅!

開發(fā) 前端
不是 ObjectMapper 難用,是咱沒 get 到它在 SpringBoot 里的 “優(yōu)雅姿勢”。今天就跟大家嘮嘮,怎么把 ObjectMapper 用得順風順水,既不用在業(yè)務代碼里堆一堆轉換邏輯,又能避免那些讓人頭大的 bug,看完這篇,保準你想把之前的代碼重構一遍(狗頭)。

兄弟們,咱誰沒跟 ObjectMapper 打過交道啊?每次跟 JSON 打交道,不是日期格式突然冒出個 “T” 讓前端小姐姐追著問,就是 null 值莫名消失被測試懟 “接口漏字段”,更絕的是 —— 明明字段名對著呢,反序列化完字段全是 null,當時真想把鍵盤拍在桌上喊 “這玩意兒咋不按套路出牌!”

其實啊,不是 ObjectMapper 難用,是咱沒 get 到它在 SpringBoot 里的 “優(yōu)雅姿勢”。今天就跟大家嘮嘮,怎么把 ObjectMapper 用得順風順水,既不用在業(yè)務代碼里堆一堆轉換邏輯,又能避免那些讓人頭大的 bug,看完這篇,保準你想把之前的代碼重構一遍(狗頭)。

一、別再瞎 new ObjectMapper 了!SpringBoot 早幫你安排了

先問大家一個問題:你是不是寫過這樣的代碼?

// 是不是你?
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(user);
User user = objectMapper.readValue(json, User.class);

要是你點頭了,那咱得先糾正這個小習慣 ——別再每次用都 new ObjectMapper 了!SpringBoot 早就幫咱們做了自動配置,在JacksonAutoConfiguration里,已經默認創(chuàng)建了一個 ObjectMapper 實例,還幫咱們配了不少基礎參數(shù)。你直接用@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 的那個根本讀不到這個配置,到時候就會出現(xiàn) “我明明配了啊,怎么沒生效” 的迷惑行為。

更坑的是,要是你在 Controller 里返回對象,SpringBoot 會用它自己的 ObjectMapper 來序列化,你自己 new 的那個配置完全沒用,等于白忙活。所以聽我的,先把 “Autowired 注入 ObjectMapper” 這個習慣養(yǎng)成,咱再談后續(xù)優(yōu)化。

二、日期處理:從 “T 亂入” 到 “格式自由”

日期處理絕對是 ObjectMapper 的 “重災區(qū)”,沒有之一。

上次我同事小王寫了個用戶列表接口,返回的日期是2024-05-20T13:14:00.000+08:00,前端小姐姐拿著截圖來找他:“王哥,這日期里的‘T’是啥意思?。渴且覀渥ⅰ裉爝m合表白’嗎?” 小王當場社死,后來查了半小時才知道,這是 ObjectMapper 默認的日期格式 ——ISO 8601 標準,但前端根本不認這個 “T”,還得轉成yyyy-MM-dd HH:mm:ss才行。

2.1 全局配置:一招解決所有日期格式問題

最優(yōu)雅的方式,就是在application.yml(或application.properties)里配全局日期格式,這樣所有用 SpringBoot 默認 ObjectMapper 序列化的日期,都會按這個格式來,不用在每個字段上寫注解。

# application.yml
spring:
  jackson:
    # 日期格式:全局統(tǒng)一成 yyyy-MM-dd HH:mm:ss
    date-format: yyyy-MM-dd HH:mm:ss
    # 時區(qū):必須配!不然會有8小時時差
    time-zone: GMT+8
    # 針對LocalDateTime等JDK8新日期類型的配置
    deserialization:
      adjust-dates-to-context-time-zone: false

這里有個坑必須提醒大家:時區(qū)一定要配! 要是沒配time-zone: GMT+8,ObjectMapper 會默認用 UTC 時區(qū),結果就是返回的日期比實際少 8 小時,比如你本地是 2024-05-20 13:14,序列化后變成 2024-05-20 05:14,到時候前端以為你接口返回的是昨天的數(shù)據(jù),能把你懟到懷疑人生。要是你用的是 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也得寫,不然會繼承全局的時區(qū),但要是你局部格式沒寫時分秒,時區(qū)配置不影響結果,不過寫上更穩(wěn)妥,避免踩坑。

三、null 值處理:留還是刪?別讓前端跟你吵架

另一個高頻痛點:null 值消失。

比如你返回的 User 對象里,nickName是 null,序列化后 JSON 里直接沒這個字段了,前端拿到數(shù)據(jù)一看:“哎?nickName 呢?你接口漏字段了吧!” 你查代碼發(fā)現(xiàn)字段明明在,就是值為 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一樣,駝峰轉下劃線(老版本的寫法,現(xiàn)在推薦用SNAKE_CASE)
  • PASCAL_CASE_TO_CAMEL_CASE:帕斯卡命名(首字母大寫,比如 UserName)轉駝峰(userName)
  • LOWER_CASE:所有字母小寫,比如userName轉username

比如你前端用帕斯卡命名(UserName),就配PASCAL_CASE_TO_CAMEL_CASE,Java 里的userName會自動和前端的UserName映射。

4.2 局部特殊:個別字段不按全局規(guī)則來

要是全局是SNAKE_CASE,但某個字段需要特殊命名,比如userId要映射到user_id,但phoneNumber要映射到mobile(不是phone_number),這時候用@JsonProperty注解覆蓋全局規(guī)則:

public class User {
    // 全局是SNAKE_CASE,這里會自動轉user_id,不用寫@JsonProperty
    private Long userId;

    // 特殊情況:phoneNumber要映射到mobile,用@JsonProperty指定
    @JsonProperty("mobile")
    private String phoneNumber;

    // getter/setter 省略
}

這樣既保留了全局的便捷性,又能處理特殊字段,優(yōu)雅!

五、復雜類型:List變 Map?TypeReference 救場

處理 List、Map 這些復雜類型時,很多人會踩一個坑:反序列化 List 的時候,拿到的不是List<User>,而是List<LinkedHashMap>,遍歷的時候強轉User直接報錯。

比如你寫了這樣的代碼:

// 前端傳的JSON數(shù)組
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

有時候我們需要對字段做特殊轉換,比如數(shù)據(jù)庫里存的性別是 1(男)、2(女),但接口要返回 “男”、“女”;或者金額存的是分(比如 1000 分 = 10 元),接口要返回元(10.00 元)。

要是在業(yè)務代碼里寫 if-else 轉換,比如:

// 不優(yōu)雅的寫法:業(yè)務代碼里混著格式轉換
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;
}

這樣寫能實現(xiàn)功能,但業(yè)務代碼和格式轉換混在一起,要是有多個地方需要轉換,就會寫一堆重復代碼,維護起來麻煩。這時候用 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": "男",不用在業(yè)務代碼里寫 if-else 了,清爽!

6.2 自定義反序列化器:前端傳 “男”→后端存 1

要是前端傳的是 “男”、“女”,后端需要轉成 1、2 存數(shù)據(jù)庫,就需要自定義反序列化器,繼承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": "男",完美閉環(huán)。

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);
        };
    }
}

這里有兩種方式:

  1. serializerByType:按類型注冊,比如所有Integer類型的字段都用這個序列化器(適合所有同類型字段都需要轉換的場景)
  2. addSerializer(字段名, 序列化器):按字段名注冊,只有指定字段名的字段才用這個序列化器(適合特定字段的場景)

根據(jù)自己的需求選就行,全局注冊后,就不用在每個字段上寫注解了,更高效。

七、SpringBoot 高級配置:Jackson2ObjectMapperBuilderCustomizer 才是王道

前面我們講了很多配置,比如全局日期格式、字段命名策略、自定義序列化器,有些是在application.yml里配的,有些是用 Bean 配置的。其實 SpringBoot 推薦用Jackson2ObjectMapperBuilderCustomizer來統(tǒng)一管理所有 ObjectMapper 的配置,這樣所有配置都在一個地方,方便維護。

Jackson2ObjectMapperBuilderCustomizer是一個函數(shù)式接口,通過它可以自定義Jackson2ObjectMapperBuilder,而Jackson2ObjectMapperBuilder又會用來創(chuàng)建 ObjectMapper 實例,所以用它配置,能覆蓋所有 ObjectMapper 的參數(shù)。

咱寫一個完整的配置類,把前面講的痛點解決方案都整合進去:

@Configuration
publicclass JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
        // 函數(shù)式接口,用lambda表達式實現(xiàn)
        return builder -> {
            // 1. 日期配置
            builder.dateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")) // Date類型格式
                  .timeZone(TimeZone.getTimeZone("GMT+8")) // 時區(qū)
                  .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(開發(fā)環(huán)境用,生產環(huán)境關閉)
                  .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 {
            // 分轉元,保留兩位小數(shù)
            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 的配置都在一個地方,后續(xù)要修改某個配置,直接改這里就行,不用到處找,非常優(yōu)雅。

這里提個小建議:開發(fā)環(huán)境可以開啟SerializationFeature.INDENT_OUTPUT(格式化 JSON),方便調試;生產環(huán)境要關閉,因為格式化會增加 JSON 的體積,影響接口性能。

八、性能優(yōu)化:ObjectMapper 線程安全,別再 “買杯子” 了

之前我們說過 “別瞎 new ObjectMapper”,除了配置不生效的問題,還有性能問題。

ObjectMapper 的創(chuàng)建成本很高,它需要加載很多模塊、初始化序列化器 / 反序列化器,要是每次用都 new 一個,就像每次喝水都新買個杯子,喝完就扔,太浪費資源了。

而且 ObjectMapper 是線程安全的!只要初始化后不修改它的配置(比如不調用setDateFormat、registerModule這些方法),多個線程同時用它序列化 / 反序列化,完全沒問題。

所以在 SpringBoot 里,最佳實踐是:

  1. 用@Autowired注入 SpringBoot 自動配置的 ObjectMapper(或者自己用Jackson2ObjectMapperBuilderCustomizer配置的)
  2. 不要每次用都 new ObjectMapper
  3. 不要在多線程環(huán)境下修改 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 全局一致,不會出現(xiàn)配置不生效的問題。

九、異常處理:JSON 錯了別返回 500,友好點

最后再聊聊異常處理。當 ObjectMapper 序列化 / 反序列化出錯時(比如 JSON 格式錯誤、字段類型不匹配),會拋出JsonProcessingException(序列化)或JsonMappingException(反序列化)。

要是不處理這些異常,SpringBoot 會默認返回 500 錯誤,前端看到 “服務器內部錯誤”,根本不知道哪里錯了。咱得捕獲這些異常,返回友好的提示,比如 “JSON 格式錯誤,請檢查參數(shù)”。

9.1 全局異常處理:用 @RestControllerAdvice

寫一個全局異常處理器,捕獲 Jackson 相關的異常:

@RestControllerAdvice
publicclass GlobalExceptionHandler {

    // 捕獲序列化異常(比如對象里有循環(huán)引用)
    @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; // 錯誤信息
    }
}

這樣一來,當出現(xiàn) JSON 錯誤時,會返回:

{
  "code": 400,
  "message": "JSON反序列化失敗:字段[age]Cannot deserialize value of type `java.lang.Integer` from String \"二十\": not a valid Integer value"
}

前端能清楚知道是哪個字段錯了,錯在哪里,不用再跟后端反復溝通 “我傳的參數(shù)沒問題啊”,效率高多了。

十、總結:優(yōu)雅使用 ObjectMapper 的 9 個要點

嘮了這么多,最后總結一下,SpringBoot 里優(yōu)雅用 ObjectMapper 的核心就是這 9 點:

  1. 別瞎 new:用@Autowired注入 SpringBoot 自動配置的 ObjectMapper,別自己 new;
  2. 全局配置優(yōu)先:日期格式、null 值處理、字段命名策略,先在application.yml或Jackson2ObjectMapperBuilderCustomizer里配全局的,減少重復代碼;
  3. 局部配置補充:個別字段特殊需求,用@JsonFormat、@JsonInclude、@JsonProperty等注解覆蓋全局;
  4. 復雜類型用 TypeReference:反序列化 List、Map 等泛型類型,一定要用new TypeReference<>() {};
  5. 自定義序列化抽離邏輯:特殊轉換(比如性別、金額)用自定義序列化器,別在業(yè)務代碼里堆 if-else;
  6. 全局注冊序列化器:多個字段用同一個序列化器,全局注冊比每個字段加注解更高效;
  7. 線程安全要記?。篛bjectMapper 是線程安全的,初始化一次全局用,別頻繁 new;
  8. 開發(fā)生產環(huán)境區(qū)分:開發(fā)環(huán)境開啟 JSON 格式化,生產環(huán)境關閉,兼顧調試和性能;
  9. 異常處理要友好:捕獲 Jackson 異常,返回明確的錯誤信息,別讓前端猜。

其實 ObjectMapper 這玩意兒,你用順了之后會發(fā)現(xiàn),它比你想象中靈活多了。以前踩的那些坑,大多是因為沒搞懂 SpringBoot 的自動配置邏輯,或者沒掌握它的高級用法。

責任編輯:武曉燕 來源: 石杉的架構筆記
相關推薦

2023-11-23 08:25:31

String性能

2021-04-20 10:50:38

Spring Boot代碼Java

2023-03-23 22:46:38

Spring限流機制

2023-08-01 08:54:02

接口冪等網絡

2024-10-11 11:46:40

2021-10-15 06:58:57

零信任高危漏洞

2022-02-15 17:56:19

SpringBoot日志

2024-10-16 12:23:55

技巧Spring驗證

2022-08-21 14:00:11

消息中間件MQ

2021-11-10 10:03:18

SpringBootJava代碼

2017-07-27 16:18:18

開源項目使用

2020-10-25 19:58:04

Pythonic代碼語言

2025-07-29 02:15:00

2014-06-09 10:51:59

2024-12-17 08:20:50

2023-03-06 11:36:13

SpingBoot注解

2021-02-05 11:36:42

數(shù)據(jù)業(yè)務指標

2025-02-26 08:46:31

2017-06-26 09:40:50

Python代碼寫法

2017-07-07 16:57:35

代碼Python
點贊
收藏

51CTO技術棧公眾號