在日常開發中,我們總會遇到一些瑣碎但又無處不在的字段處理需求:
??請求處理:?用戶提交的表單,字符串前后帶了多余的空格,需要手動?
trim()
。??響應處理:?返回給前端的?
BigDecimal
?金額,因為精度問題導致JS處理出錯,需要格式化為兩位小數的字符串。??空值處理:?某個VO的?
List
?字段是?null
,序列化成 JSON 后,前端期望得到一個空數組?[]
?而不是?null
,以避免空指針判斷。
如果在每個 DTO 的?setter/getter
?或 Service 邏輯中手動處理這些,代碼會變得非常臃腫和重復。本文將帶你從 0 到 1,構建一個全局的、自動化的字段處理 Starter,通過簡單的配置,一次性解決所有這些惱人的“小問題”。
1. 項目設計與核心思路
我們的?global-field-handler-starter
?目標如下:
1.?請求參數自動 Trim:?自動去除所有傳入 JSON 請求體中 String 類型字段的前后空格。
2.?BigDecimal 響應自動格式化:?自動將所有傳出 JSON 響應體中的?
BigDecimal
?類型格式化為指定小數位數的字符串。3.?Null 集合/數組自動轉換:?自動將?
null
?的集合或數組在 JSON 響應中轉換為空數組?[]
。4.?可配置:?以上所有功能都應提供獨立的開關進行控制。
核心實現機制:自定義 Jackson?ObjectMapper
Spring Boot 使用 Jackson 作為默認的 JSON 處理器。Jackson 提供了極其豐富的定制化能力。我們將通過創建一個?Jackson2ObjectMapperBuilderCustomizer
?Bean,來向 Spring Boot 自動配置的?ObjectMapper
?中“注入”我們自定義的處理邏輯。
? 對于反序列化(請求),我們將注冊一個自定義的?
JsonDeserializer
。? 對于序列化(響應),我們將注冊幾個自定義的?
JsonSerializer
。
2. 創建 Starter 項目與核心組件
我們采用?autoconfigure
?+?starter
?的雙模塊結構。
步驟 2.1: 依賴 (autoconfigure
?模塊)
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies>
步驟 2.2: 實現自定義的序列化/反序列化器
TrimStringDeserializer.java
?(字符串 Trim 反序列化器):
package?com.example.fieldhandler.autoconfigure.handler;import?com.fasterxml.jackson.core.JsonParser;
import?com.fasterxml.jackson.databind.DeserializationContext;
import?com.fasterxml.jackson.databind.JsonDeserializer;
import?java.io.IOException;public?class?TrimStringDeserializer?extends?JsonDeserializer<String> {@Overridepublic?String?deserialize(JsonParser p, DeserializationContext ctxt)?throws?IOException {String?value?=?p.getValueAsString();if?(value ==?null) {return?null;}return?value.trim();}
}
BigDecimalSerializer.java
?(BigDecimal 格式化序列化器):
package?com.example.fieldhandler.autoconfigure.handler;import?com.fasterxml.jackson.core.JsonGenerator;
import?com.fasterxml.jackson.databind.JsonSerializer;
import?com.fasterxml.jackson.databind.SerializerProvider;
import?java.io.IOException;
import?java.math.BigDecimal;
import?java.math.RoundingMode;
import?java.util.Objects;public?class?BigDecimalSerializer?extends?JsonSerializer<BigDecimal> {private?final?int?scale;?// 小數位數public?BigDecimalSerializer(int?scale)?{this.scale = scale;}@Overridepublic?void?serialize(BigDecimal value, JsonGenerator gen, SerializerProvider serializers)?throws?IOException {if?(Objects.nonNull(value)) {// 格式化為指定小數位數的字符串gen.writeString(value.setScale(scale, RoundingMode.HALF_UP).toString());}?else?{gen.writeNull();}}
}
3. 自動裝配的魔法 (GlobalFieldHandlerAutoConfiguration
)
步驟 3.1: 配置屬性類
package?com.example.fieldhandler.autoconfigure;import?org.springframework.boot.context.properties.ConfigurationProperties;@ConfigurationProperties(prefix = "global.field-handler")
public?class?GlobalFieldHandlerProperties?{private?boolean?enabled?=?false;private?boolean?trimStringInput?=?false;private?boolean?serializeNullCollectionsAsEmpty?=?false;private?boolean?serializeNullStringsAsEmpty?=?false;private?int?bigDecimalScale?=?2;?// 默認保留兩位小數// Getters and Setters...
}
步驟 3.2: 自動配置主類
這是整個 Starter 的核心,它負責根據配置,將我們的處理器應用到?ObjectMapper
。
package?com.example.fieldhandler.autoconfigure;import?com.example.fieldhandler.autoconfigure.handler.BigDecimalSerializer;
import?com.example.fieldhandler.autoconfigure.handler.TrimStringDeserializer;
import?com.fasterxml.jackson.core.JsonGenerator;
import?com.fasterxml.jackson.databind.JsonSerializer;
import?com.fasterxml.jackson.databind.SerializerProvider;
import?org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import?org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import?org.springframework.boot.context.properties.EnableConfigurationProperties;
import?org.springframework.context.annotation.Bean;
import?org.springframework.context.annotation.Configuration;
import?java.io.IOException;
import?java.math.BigDecimal;
import?java.util.Collection;@Configuration
@EnableConfigurationProperties(GlobalFieldHandlerProperties.class)
@ConditionalOnProperty(prefix = "global.field-handler", name = "enabled", havingValue = "true")
public?class?GlobalFieldHandlerAutoConfiguration?{@Beanpublic?Jackson2ObjectMapperBuilderCustomizer?jacksonCustomizer(GlobalFieldHandlerProperties properties)?{return?builder -> {// 1. 配置請求參數字符串 Trimif?(properties.isTrimStringInput()) {builder.deserializerByType(String.class,?new?TrimStringDeserializer());}// 2. 配置 BigDecimal 響應格式化builder.serializerByType(BigDecimal.class,?new?BigDecimalSerializer(properties.getBigDecimalScale()));// 3. 配置 Null 值處理builder.postConfigurer(objectMapper -> {if?(properties.isSerializeNullCollectionsAsEmpty()) {objectMapper.getSerializerProvider().setNullValueSerializer(new?JsonSerializer<>() {@Overridepublic?void?serialize(Object value, JsonGenerator gen, SerializerProvider serializers)?throws?IOException {// 對所有類型的 Null 都生效,我們需要篩選// 這里我們簡化為對 Collection 的 Null 進行處理// 注意:更精細的控制可能需要更復雜的 JsonSerializer 或 BeanSerializerModifiergen.writeStartArray();gen.writeEndArray();}});}if?(properties.isSerializeNullStringsAsEmpty()) {objectMapper.getSerializerProvider().setNullValueSerializer(new?JsonSerializer<>() {@Overridepublic?void?serialize(Object value, JsonGenerator gen, SerializerProvider serializers)?throws?IOException {gen.writeString("");}});}});};}
}
步驟 3.3: 注冊自動配置
在?autoconfigure
?模塊的?resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
?文件中添加:
com.example.fieldhandler.autoconfigure.GlobalFieldHandlerAutoConfiguration
4. 如何使用我們的 Starter
步驟 4.1: 引入依賴
<dependency><groupId>com.example</groupId><artifactId>global-field-handler-spring-boot-starter</artifactId><version>1.0.0</version>
</dependency>
步驟 4.2: 在?application.yml
?中配置
global:field-handler:enabled:?truetrim-string-input:?trueserialize-null-collections-as-empty:?truebig-decimal-scale:?2
步驟 4.3: 編寫 DTO 和 Controller 并驗證
DTO:
public?class?ProductVO?{private?String name;private?BigDecimal price;private?List<String> tags;// Getters, Setters...
}
Controller:
@RestController
public?class?ProductController?{// 驗證請求參數 Trim@PostMapping("/products")public?ProductVO?createProduct(@RequestBody?ProductVO product)?{// 傳入的 JSON: {"name": " ?My Product ?", ...}// 在這里,product.getName() 的值已經是 "My Product",空格已被自動去除System.out.println("Product name received: '"?+ product.getName() +?"'");return?product;}// 驗證響應格式化@GetMapping("/products/{id}")public?ProductVO?getProduct(@PathVariable?Long id)?{ProductVO?product?=?new?ProductVO();product.setName("Awesome Gadget");// 價格精度很高product.setPrice(new?BigDecimal("199.998"));// Tags 為 nullproduct.setTags(null);return?product;}
}
驗證:
1.?POST?
/products
?請求,Body 為?{"name": " Spaceship ", "price": 1000}
。??后臺日志輸出:?
Product name received: 'Spaceship'
?(空格已被 trim)。
2.?GET?
/products/1
?請求。- ??前端收到的 JSON 響應:
{"name":?"Awesome Gadget","price":?"200.00",?"tags":?[]? }
price
?被自動格式化為兩位小數的字符串,tags
?從?null
?變成了?[]
。
- ??前端收到的 JSON 響應:
總結
通過自定義一個 Spring Boot Starter 和深入利用 Jackson 的定制化能力,我們成功地將一系列瑣碎、重復但又非常重要的字段處理邏輯,沉淀為了一個可配置、自動化的基礎設施。