彻底解决SpringBoot中BigDecimal序列化的科学计数法陷阱

财务系统开发中最令人头疼的问题之一,就是金额字段在前端莫名其妙变成了科学计数法显示。上周排查一个线上问题,某笔订单金额"12000"在页面上显示为"1.2E4",客户直接打来了投诉电话。这种看似简单的数据展示问题,背后却涉及序列化框架选择、全局配置策略和前后端协作的深层考量。

1. 科学计数法问题的根源剖析

当我们在SpringBoot项目中用BigDecimal处理金额时,经常会遇到这样的场景:后端明明返回了精确的数值,前端却显示成科学计数法。这背后其实隐藏着三个关键环节的转换问题:

  1. Java对象到JSON的序列化过程 :默认情况下,Jackson和Fastjson都会保留BigDecimal的原始格式
  2. JSON数据传输过程 :即使序列化正确,大数字在JSON中仍可能以科学计数法形式存在
  3. 前端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 后端工程规范

  1. 统一配置管理 :将序列化配置封装为starter
@AutoConfiguration
public class BigDecimalAutoConfiguration {
    @Bean
    @Primary
    public ObjectMapper objectMapper() {
        // 配置内容同上
    }
}
  1. API文档明确标注
## 金额字段规范
- 类型: string
- 示例: "1200.00"
- 格式: 必须包含两位小数
  1. 单元测试保障
@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 微服务上下文传播

在分布式系统中,需要确保所有服务采用一致的序列化策略:

  1. 将配置封装为公共JAR包
  2. 在API网关做统一格式转换
  3. 使用契约测试保障一致性
// 契约测试示例
@SpringBootTest
public class BigDecimalContractTest {
    @Test
    public void should_serialize_bigdecimal_as_string() {
        // 验证所有API的BigDecimal字段都是字符串类型
    }
}

在金融级项目中,我们最终采用了Jackson全局字符串序列化方案,配合前端自定义渲染组件,彻底解决了金额显示问题。关键是要在项目初期就确立这些规范,避免后期各模块出现不一致行为。

更多推荐