1. 为什么BigDecimal序列化会出问题第一次在项目里用BigDecimal处理金额时我踩了个大坑。后端明明返回的是12.00前端却显示成12。更诡异的是0.00000000变成了0E-8。这种问题在金融、电商等对数字精度要求高的场景简直是灾难。根本原因在于BigDecimal的三重门数据库映射门Mybatis将DECIMAL类型转为BigDecimal时会保留原始精度序列化门Jackson默认把BigDecimal当作普通数字处理前端解析门JavaScript的Number类型无法区分12.0和12实测发现当BigDecimal值为0.00000000时直接toString() → 0.00000000未经处理的Jackson序列化 → 0E-8前端JSON.parse() → 02. Mybatis层的关键配置2.1 数据库类型映射在Mybatis的XML映射文件中要显式指定jdbcTyperesult columnamount propertyamount jdbcTypeDECIMAL/建议在全局配置中添加类型处理器typeHandlers typeHandler handlerorg.apache.ibatis.type.BigDecimalTypeHandler jdbcTypeDECIMAL javaTypejava.math.BigDecimal/ /typeHandlers2.2 精度丢失防护遇到过数据库存的是19.99查出来变成19.989999999的情况吗这是因为MySQL的DECIMAL默认精度问题。解决方案-- 建表时明确指定精度 CREATE TABLE orders ( amount DECIMAL(20, 6) NOT NULL COMMENT 金额字段 );在MyBatis的SQL中也要注意!-- 错误示范 -- select idgetAmount resultTypeBigDecimal SELECT amount FROM orders !-- 可能丢失精度 -- /select !-- 正确做法 -- select idgetAmount resultTypeBigDecimal SELECT CAST(amount AS DECIMAL(20,6)) FROM orders /select3. Jackson序列化终极方案3.1 自定义序列化器原始文章提供的方案可以优化升级public class SafeBigDecimalSerializer extends JsonSerializerBigDecimal { Override public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) throws IOException { if (value null) { gen.writeNull(); return; } // 处理科学计数法问题 String plain value.stripTrailingZeros().toPlainString(); // 处理.00情况 if (plain.endsWith(.0) || plain.endsWith(.00)) { plain plain.substring(0, plain.indexOf(.)); } gen.writeString(plain); } }使用方式public class OrderVO { JsonSerialize(using SafeBigDecimalSerializer.class) private BigDecimal amount; }3.2 全局配置方案不想每个字段都加注解可以这样全局配置Configuration public class JacksonConfig { Bean public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); SimpleModule module new SimpleModule(); module.addSerializer(BigDecimal.class, new SafeBigDecimalSerializer()); mapper.registerModule(module); return mapper; } }4. 前端处理最佳实践4.1 接收方案前端axios接收时建议axios.interceptors.response.use(response { if (response.data?.amount) { // 防止科学计数法 response.data.amount String(response.data.amount) } return response })4.2 显示格式化推荐使用decimal.js处理高精度数字import Decimal from decimal.js // 金额显示格式化 function formatMoney(value) { return new Decimal(value).toFixed(2) }对于Vue项目可以创建过滤器Vue.filter(money, value { return new Decimal(value || 0).toFixed(2) })5. 全链路测试方案5.1 边界值测试用例建议覆盖这些测试场景零值0.00000000 → 应显示0整数12.00 → 应显示12小数12.30 → 应显示12.3科学计数法0.00000012 → 应显示0.00000012超大数123456789.123456789 → 保持原样5.2 集成测试技巧在SpringBoot测试中可这样验证Test public void testSerialization() throws Exception { OrderVO order new OrderVO(); order.setAmount(new BigDecimal(12.00)); String json objectMapper.writeValueAsString(order); assertThat(json).contains(\amount\:\12\); order.setAmount(new BigDecimal(0.00000000)); json objectMapper.writeValueAsString(order); assertThat(json).contains(\amount\:\0\); }6. 性能优化建议6.1 对象池技术频繁创建BigDecimal影响性能可以考虑private static final BigDecimal[] CACHE new BigDecimal[100]; static { for (int i 0; i 100; i) { CACHE[i] new BigDecimal(i); } } public static BigDecimal valueOf(long val) { if (val 0 val 100) { return CACHE[(int)val]; } return new BigDecimal(val); }6.2 线程安全方案在多线程环境下建议使用不可变对象public final class Money { private final BigDecimal value; public Money(String value) { this.value new BigDecimal(value).setScale(4, RoundingMode.HALF_UP); } // 提供运算方法... }7. 常见问题排查7.1 科学计数法再现如果仍然出现科学计数法检查是否有多处Jackson配置冲突前端是否有二次JSON.parse操作数据库字段类型是否为DECIMAL而非FLOAT7.2 精度异常处理发现精度丢失时使用ArithmeticException捕获运算异常重要计算使用MathContext指定精度try { BigDecimal result a.divide(b); } catch (ArithmeticException e) { BigDecimal result a.divide(b, new MathContext(8)); }8. 扩展应用场景8.1 多币种处理对于跨境支付场景public class Money { private BigDecimal amount; private Currency currency; public String format() { NumberFormat fmt NumberFormat.getCurrencyInstance(); fmt.setCurrency(currency); fmt.setMinimumFractionDigits(currency.getDefaultFractionDigits()); return fmt.format(amount); } }8.2 分布式计算方案在微服务环境下建议定义通用的Decimal.proto文件使用gRPC进行传输message Decimal { string value 1; // 始终用字符串传输 int32 scale 2; // 可选保留小数位 }