别再让BigDecimal坑你了!SpringBoot项目里Jackson和Fastjson的两种全局配置方案(附代码)
·
彻底解决SpringBoot中BigDecimal序列化的科学计数法陷阱
财务系统开发中最令人头疼的问题之一,就是金额字段在前端莫名其妙变成了科学计数法显示。上周排查一个线上问题,某笔订单金额"12000"在页面上显示为"1.2E4",客户直接打来了投诉电话。这种看似简单的数据展示问题,背后却涉及序列化框架选择、全局配置策略和前后端协作的深层考量。
1. 科学计数法问题的根源剖析
当我们在SpringBoot项目中用BigDecimal处理金额时,经常会遇到这样的场景:后端明明返回了精确的数值,前端却显示成科学计数法。这背后其实隐藏着三个关键环节的转换问题:
- Java对象到JSON的序列化过程 :默认情况下,Jackson和Fastjson都会保留BigDecimal的原始格式
- JSON数据传输过程 :即使序列化正确,大数字在JSON中仍可能以科学计数法形式存在
- 前端JavaScript的解析渲染 :JS对长数字有自动转换科学计数法的机制
// 典型的BigDecimal使用场景
@RestController
public class PaymentController {
@GetMapping("/balance")
public AccountBalance getBalance() {
return new AccountBalance(new BigDecimal("1200000.00"));
}
}
关键问题定位表 :
| 问题环节 | 典型表现 | 解决方案方向 |
|---|---|---|
| 序列化配置不当 | 后端返回JSON已带E符号 | 修改序列化策略 |
| 前端JS自动转换 | 网络请求数据正常但显示异常 | 前端特殊处理或统一字符串传输 |
| 数据类型混淆 | 接口文档未明确定义number/string | 规范API设计 |
2. Fastjson全局配置方案深度解析
Fastjson作为阿里开源的JSON库,在国内项目中广泛应用。针对BigDecimal问题,我们有两种全局解决方案:
2.1 基础配置:禁用科学计数法
最简单的方案是通过SerializerFeature控制输出格式:
public class FastjsonConfig {
private static final SerializerFeature[] features = {
SerializerFeature.WriteBigDecimalAsPlain
};
public static String toJsonString(Object obj) {
return JSON.toJSONString(obj, features);
}
}
这种方案的局限性 :
- 仅保证数字不以科学计数法输出
- 前端仍可能对长数字进行转换
- 丢失了原始精度信息(如120.00变成120)
2.2 终极方案:统一字符串传输
更彻底的方案是将所有BigDecimal转为字符串传输:
public class BigDecimalSerializer implements ObjectSerializer {
@Override
public void write(JSONSerializer serializer, Object object,
Object fieldName, Type fieldType, int features) {
SerializeWriter out = serializer.out;
if (object == null) {
out.writeNull();
return;
}
BigDecimal value = (BigDecimal) object;
out.writeString(value.stripTrailingZeros().toPlainString());
}
}
// 全局配置
SerializeConfig.getGlobalInstance()
.put(BigDecimal.class, BigDecimalSerializer.instance);
这种方案的优势 :
- 彻底避免任何科学计数法转换
- 保留原始精度和格式
- 前端无需特殊处理
重要提示:在微服务架构中,如果某些服务未采用相同配置,会导致接口数据不一致。建议将配置封装为公共组件。
3. Jackson全局配置的工程化实践
对于使用SpringBoot默认Jackson的项目,推荐以下配置方案:
3.1 基础配置方案
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(BigDecimal.class, new JsonSerializer<>() {
@Override
public void serialize(BigDecimal value, JsonGenerator gen,
SerializerProvider provider) {
gen.writeString(value.stripTrailingZeros().toPlainString());
}
});
mapper.registerModule(module);
return mapper;
}
}
3.2 生产级增强配置
实际项目中还需要考虑反序列化和特殊场景:
module.addDeserializer(BigDecimal.class, new JsonDeserializer<>() {
@Override
public BigDecimal deserialize(JsonParser p,
DeserializationContext ctxt) {
try {
return new BigDecimal(p.getValueAsString());
} catch (NumberFormatException e) {
throw new RuntimeException("金额格式错误", e);
}
}
});
// 处理null值情况
mapper.setSerializationInclusion(Include.NON_NULL);
Jackson与Fastjson的选型对比 :
| 特性 | Fastjson | Jackson |
|---|---|---|
| 性能 | 略高 | 稳定 |
| 社区支持 | 国内活跃 | 国际主流 |
| 配置灵活性 | 较高 | 极高 |
| SpringBoot集成 | 需手动配置 | 默认支持 |
| 安全记录 | 曾有漏洞 | 较稳定 |
4. 全栈解决方案与最佳实践
完整的解决方案需要前后端协同:
4.1 后端工程规范
- 统一配置管理 :将序列化配置封装为starter
@AutoConfiguration
public class BigDecimalAutoConfiguration {
@Bean
@Primary
public ObjectMapper objectMapper() {
// 配置内容同上
}
}
- API文档明确标注 :
## 金额字段规范
- 类型: string
- 示例: "1200.00"
- 格式: 必须包含两位小数
- 单元测试保障 :
@Test
void testBigDecimalSerialization() throws Exception {
MoneyDTO dto = new MoneyDTO(new BigDecimal("123456.78"));
String json = objectMapper.writeValueAsString(dto);
assertThat(json).contains("\"123456.78\"");
}
4.2 前端处理建议
即使后端做了字符串转换,前端仍应做好防御:
// Vue示例:金额显示过滤器
Vue.filter('currency', function(value) {
if (!value) return '0.00';
// 防止科学计数法
if (typeof value === 'number') {
return value.toFixed(2);
}
// 字符串直接显示
return value;
});
性能优化技巧 :
- 对于高频交易系统,可评估使用自定义的轻量级序列化方案
- 缓存频繁使用的BigDecimal的字符串形式
- 在网关层统一处理响应格式
5. 疑难场景与特殊处理
在某些特殊情况下,可能需要更灵活的处理方式:
5.1 混合处理策略
// 注解方式实现字段级控制
public class ProductDTO {
@JsonFormat(shape = JsonFormat.Shape.STRING)
private BigDecimal price;
private BigDecimal cost; // 默认处理
}
5.2 大数字精度控制
public class BigDecimalSerializer extends JsonSerializer<BigDecimal> {
@Override
public void serialize(BigDecimal value, JsonGenerator gen,
SerializerProvider provider) {
// 统一保留6位小数
String str = value.setScale(6, RoundingMode.HALF_UP)
.stripTrailingZeros()
.toPlainString();
gen.writeString(str);
}
}
5.3 微服务上下文传播
在分布式系统中,需要确保所有服务采用一致的序列化策略:
- 将配置封装为公共JAR包
- 在API网关做统一格式转换
- 使用契约测试保障一致性
// 契约测试示例
@SpringBootTest
public class BigDecimalContractTest {
@Test
public void should_serialize_bigdecimal_as_string() {
// 验证所有API的BigDecimal字段都是字符串类型
}
}
在金融级项目中,我们最终采用了Jackson全局字符串序列化方案,配合前端自定义渲染组件,彻底解决了金额显示问题。关键是要在项目初期就确立这些规范,避免后期各模块出现不一致行为。
更多推荐
所有评论(0)