
在日常开发中,我们总会遇到一些琐碎但又无处不在的字段处理需求:
-
• 请求处理: 用户提交的表单,字符串前后带了多余的空格,需要手动
trim()
。 -
• 响应处理: 返回给前端的
BigDecimal
金额,因为精度问题导致JS处理出错,需要格式化为两位小数的字符串。 -
• 空值处理: 某个VO的
List
字段是null
,序列化成 JSON 后,前端期望得到一个空数组[]
而不是null
,以避免空指针判断。
如果在每个 DTO 的 setter/getter
或 Service 逻辑中手动处理这些,代码会变得非常臃肿和重复。本文将带你从 0 到 1,构建一个全局的、自动化的字段处理 Starter,通过简单的配置,一次性解决所有这些恼人的"小问题"。
1. 项目设计与核心思路
我们的 global-field-handler-starter
目标如下:
-
- 请求参数自动 Trim: 自动去除所有传入 JSON 请求体中 String 类型字段的前后空格。
-
- BigDecimal 响应自动格式化: 自动将所有传出 JSON 响应体中的
BigDecimal
类型格式化为指定小数位数的字符串。
- BigDecimal 响应自动格式化: 自动将所有传出 JSON 响应体中的
-
- Null 集合/数组自动转换: 自动将
null
的集合或数组在 JSON 响应中转换为空数组[]
。
- Null 集合/数组自动转换: 自动将
-
- 可配置: 以上所有功能都应提供独立的开关进行控制。
核心实现机制:自定义 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> {
@Override
public 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;
}
@Override
public 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 {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer(GlobalFieldHandlerProperties properties) {
return builder -> {
// 1. 配置请求参数字符串 Trim
if (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<>() {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 对所有类型的 Null 都生效,我们需要筛选
// 这里我们简化为对 Collection 的 Null 进行处理
// 注意:更精细的控制可能需要更复杂的 JsonSerializer 或 BeanSerializerModifier
gen.writeStartArray();
gen.writeEndArray();
}
});
}
if (properties.isSerializeNullStringsAsEmpty()) {
objectMapper.getSerializerProvider().setNullValueSerializer(new JsonSerializer<>() {
@Override
public 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: true
trim-string-input: true
serialize-null-collections-as-empty: true
big-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 为 null
product.setTags(null);
return product;
}
}
验证:
-
- POST
/products
请求,Body 为{"name": " Spaceship ", "price": 1000}
。
- • 后台日志输出:
Product name received: 'Spaceship'
(空格已被 trim)。
- POST
-
- GET
/products/1
请求。
-
• 前端收到的 JSON 响应:
{ "name": "Awesome Gadget", "price": "200.00", "tags": [] }
可以看到,
price
被自动格式化为两位小数的字符串,tags
从null
变成了[]
。
- GET
总结
通过自定义一个 Spring Boot Starter 和深入利用 Jackson 的定制化能力,我们成功地将一系列琐碎、重复但又非常重要的字段处理逻辑,沉淀为了一个可配置、自动化的基础设施。