从科学计数法到清晰展示:Spring Boot中BigDecimal的终极处理方案

那天凌晨三点,我被紧急电话叫醒——生产环境的订单系统突然显示所有金额都变成了"1.23E+4"这样的格式。财务部门已经炸开了锅,因为没人能看懂这些科学计数法表示的具体金额。这就是我第一次深刻认识到BigDecimal序列化问题的严重性。作为Java开发者,我们选择BigDecimal就是为了精确表示金融数据,但如果不处理好序列化问题,反而会造成更大的混乱。

1. 问题本质:为什么BigDecimal会变成科学计数法?

BigDecimal被设计为可以精确表示任意精度的十进制数,但当我们把它放入Web应用的JSON响应中时,经常会遇到两个层面的问题:

  1. 后端序列化问题 :默认情况下,Jackson和Fastjson都会将BigDecimal按照数值类型处理,当数值过大或过小时,会自动转换为科学计数法表示
  2. 前端解析问题 :即使后端正确输出了完整数字,JavaScript在解析JSON时也可能自动将长数字转换为科学计数法

关键区别 :后端序列化问题会导致响应体本身就包含科学计数法,而前端问题则是响应体正常但显示异常。要确认问题来源,最简单的方法是用Postman等工具直接查看原始响应:

// 问题出在后端的情况
{
  "amount": 1.23E+4
}

// 问题出在前端的情况
{
  "amount": 12300.00
}

2. Fastjson解决方案:全局与局部的精准控制

2.1 全局配置:一劳永逸的方案

对于新项目或能够接受全面改造的系统,全局配置是最稳妥的选择。Fastjson提供了 SerializeConfig 来实现类型级别的序列化控制:

public class BigDecimalSerializer implements ObjectSerializer {
    @Override
    public void write(JSONSerializer serializer, Object object, 
                     Object fieldName, Type fieldType, int features) throws IOException {
        if (object == null) {
            serializer.out.writeNull();
            return;
        }
        BigDecimal value = (BigDecimal) object;
        // 去除末尾多余的0并转换为普通字符串形式
        serializer.write(value.stripTrailingZeros().toPlainString());
    }
}

@Configuration
public class FastjsonConfig {
    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters() {
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        FastJsonConfig config = new FastJsonConfig();
        
        // 关键配置:注册全局的BigDecimal序列化器
        config.setSerializeConfig(new SerializeConfig()
            .put(BigDecimal.class, new BigDecimalSerializer()));
            
        converter.setFastJsonConfig(config);
        return new HttpMessageConverters(converter);
    }
}

注意事项

  • 此配置会影响所有BigDecimal字段,确保它们都以字符串形式输出
  • stripTrailingZeros() 会去掉不必要的末尾零(如100.00变为100)
  • 对于金融系统,可能需要保留两位小数,可以在序列化器中添加格式化逻辑

2.2 注解驱动:细粒度的字段控制

在遗留系统中,可能只需要对特定字段进行处理。Fastjson提供了 @JSONField 注解来实现字段级别的控制:

public class OrderDTO {
    // 其他字段保持默认序列化方式
    
    @JSONField(serializeUsing = BigDecimalSerializer.class)
    private BigDecimal totalAmount;
    
    // 标准getter/setter
}

适用场景

  • 系统大部分BigDecimal字段不需要特殊处理
  • 只有少数金融相关字段需要确保精确表示
  • 渐进式改造过程中的临时方案

3. Jackson解决方案:Spring Boot默认引擎的调优

3.1 全局配置:定制ObjectMapper

Spring Boot默认使用Jackson,我们可以通过配置 ObjectMapper 来改变其行为:

@Configuration
public class JacksonConfig {
    
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        
        // 添加自定义序列化器
        module.addSerializer(BigDecimal.class, new JsonSerializer<BigDecimal>() {
            @Override
            public void serialize(BigDecimal value, JsonGenerator gen, 
                                SerializerProvider provider) throws IOException {
                // 保留两位小数并转为字符串
                gen.writeString(value.setScale(2, RoundingMode.HALF_UP).toPlainString());
            }
        });
        
        // 添加自定义反序列化器
        module.addDeserializer(BigDecimal.class, new JsonDeserializer<BigDecimal>() {
            @Override
            public BigDecimal deserialize(JsonParser p, 
                                       DeserializationContext ctxt) throws IOException {
                return new BigDecimal(p.getValueAsString());
            }
        });
        
        mapper.registerModule(module);
        return mapper;
    }
}

版本适配建议

  • Spring Boot 1.x: 使用 Jackson2ObjectMapperBuilder
  • Spring Boot 2.x: 直接配置 ObjectMapper 更灵活
  • 对于Spring Boot 2.3+,可以考虑使用 Jackson2ObjectMapperBuilderCustomizer

3.2 注解方案:最小化影响

与Fastjson类似,Jackson也提供了字段级别的控制:

public class ProductDTO {
    @JsonSerialize(using = BigDecimalToStringSerializer.class)
    @JsonDeserialize(using = StringToBigDecimalDeserializer.class)
    private BigDecimal price;
    
    // 其他字段
}

最佳实践

  • 在DTO类上使用注解,而不是直接修改实体类
  • 为序列化和反序列化分别配置,确保双向转换的正确性
  • 考虑创建自定义组合注解简化重复配置

4. 前端协作:确保全链路一致性

即使后端处理得当,前端仍可能遇到数值显示问题。完整的解决方案应该包含前后端约定:

  1. 数据类型约定

    • 方案A:所有金额字段统一为字符串类型
    • 方案B:在JSON Schema中明确字段格式要求
  2. 前端处理示例

// 使用专门的金额显示组件
<MoneyDisplay :value="order.totalAmount" precision="2"/>

// 或者全局过滤器
Vue.filter('money', value => {
  if (typeof value === 'string') return value;
  return new Big(value).toFixed(2);
});
  1. 联调检查清单
    • 确认HTTP响应头 Content-Type: application/json
    • 验证响应体数据格式是否符合约定
    • 检查前端是否有可能覆盖响应数据的拦截器

5. 深入原理:序列化过程中的关键决策点

理解底层机制有助于做出更合理的架构决策:

Jackson处理流程

  1. 检测字段类型(通过反射或显式类型信息)
  2. 查找注册的 JsonSerializer (按照类型→注解的顺序)
  3. 调用 serialize() 方法生成JSON内容

Fastjson处理差异

  1. 使用 SerializeConfig 查找序列化器
  2. 支持更多的内置特性(如 SerializerFeature
  3. 线程安全的全局配置与实例级别配置

性能考量

方案 优点 缺点
全局配置 一劳永逸,一致性高 可能影响不需要处理的字段
注解配置 精准控制,影响面小 需要显式标记每个字段
前端转换 不依赖后端实现 增加前端复杂度

在微服务架构中,建议在API网关层或基础库中统一处理这类跨服务问题,而不是让每个服务自行实现。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐