原创

Long和Long类型集合前端精度丢失解决办法锦集以及自定义JSON序列化方法

因为JS解析整型的时候是有最大值的,Number.MAX_SAFE_INTEGER 常量表示在 JavaScript 中最大的安全整数(maxinum safe integer)(253 - 1)

Number.MAX_SAFE_INTEGER // 9007199254740991
Math.pow(2, 53) - 1     // 9007199254740991

最大长度是16位数,超过了就解析不正常了,会丢失精度。

我后端是用的雪花算法生成的20位的唯一ID,我返回给前端的时候,例如:

我返回的是Long类型的,但是前端接收之后精度丢失,导致和我后端给的不一致,解决办法就是使用String类型的。

方法1-后端传输JSON格式化为String类型的

@JsonFormat(shape = JsonFormat.Shape.STRING)
private Long aliyunOssFileId;

@JsonFormat(shape = JsonFormat.Shape.STRING)作用就是将JSON数据的此字段格式化为字符串类型,保证前端超过16位不会出现精度丢失问题!

但是,如果有很多Long类型的话,要一个一个去改,也太累了,Spring MVC中默认是使用了Jackson的,可以通过重写转换器解决。

方法2-重写Jackson转换器(配置全局生效)

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
    /**
     * Long类型转String类型
     *
     * 解决前端Long类型精度丢失问题(js解析只能解析到16位)
     *
     * @param converters
     * @author Zhaopo Liu
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter jackson2HttpMessageConverter =
                new MappingJackson2HttpMessageConverter();

        ObjectMapper objectMapper = new ObjectMapper();
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        jackson2HttpMessageConverter.setObjectMapper(objectMapper);
        converters.add(jackson2HttpMessageConverter);
        converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8));
    }
}

方法3-实现Jackson2ObjectMapperBuilderCustomizer作为Bean(配置全局生效)

和前面说的方法重写Jackson转换器也是一个道理的。

完整的Jackson2ObjectMapperBuilderCustomizer配置(此处我把完整的配置贴出来,可根据需要自行设置):

/** @author lzhpo */
@Configuration
public class JacksonEnhanceConfig {

  /**
   * Callback interface that can be implemented by beans wishing to further customize the {@link
   * ObjectMapper} via {@link Jackson2ObjectMapperBuilder} retaining its default auto-configuration.
   *
   * @return {@link Jackson2ObjectMapperBuilderCustomizer}
   */
  @Bean
  public SunJackson2ObjectMapperBuilderCustomizer sunJackson2ObjectMapperBuilderCustomizer() {
    return new SunJackson2ObjectMapperBuilderCustomizer();
  }

  public static class SunJackson2ObjectMapperBuilderCustomizer
      implements Jackson2ObjectMapperBuilderCustomizer {

    /**
     * Customize the JacksonObjectMapperBuilder.
     *
     * <pre>
     * 在此处{@link Jackson2ObjectMapperBuilderCustomizer}有两种方法,
     * 设置Long以及BigInteger类型序列化为String类型,避免雪花算法Long类型返回前端可能导致精度丢失问题。
     * - 方式1:
     * {@code
     *   SimpleModule simpleModule = new SimpleModule();
     *   simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
     *   simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
     *   simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
     *   builder.modules(simpleModule);
     * }
     * - 方式2:
     * {@code
     *   builder.serializerByType(Long.class, ToStringSerializer.instance);
     *   builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
     *   builder.serializerByType(BigInteger.class, ToStringSerializer.instance)
     * }
     * </pre>
     *
     * @param builder the JacksonObjectMapperBuilder to customize
     */
    @Override
    public void customize(Jackson2ObjectMapperBuilder builder) {
      // 设置Jdk8Module
      builder.modules(new Jdk8Module());
      // 设置JavaTimeModule
      builder.modules(new JavaTimeModule());

      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateConst.DATE_FORMAT_TYPE);
      SimpleModule simpleModule = new SimpleModule();
      // Long以及BigInteger类型序列化为String类型,避免雪花算法Long类型精度丢失问题
      simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
      simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
      simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
      // 指定LocalDateTime序列化和反序列化
      simpleModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
      simpleModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
      // 注册simpleModule
      builder.modules(simpleModule);
      
//      builder.serializerByType(Long.class, ToStringSerializer.instance);
//      builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
//      builder.serializerByType(BigInteger.class, ToStringSerializer.instance);

      // 自定义时间日期格式
      builder.dateFormat(new SimpleDateFormat(DateConst.DATE_FORMAT_TYPE));
      // 不忽略值为 null 的属性
      builder.serializationInclusion(JsonInclude.Include.ALWAYS);
      // 修改jackson默认的字段属性发现规则,设置自动检测哪些方法
      builder.visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
      // 美化输出
      builder.featuresToEnable(SerializationFeature.INDENT_OUTPUT);

      // Define the date/time format with a {@link SimpleDateFormat}.
      builder.simpleDateFormat(DateConst.DATE_FORMAT_TYPE);
      // Configure a custom deserializer for the given type.
      builder.deserializerByType(
          String.class,
          new StdScalarDeserializer<String>(String.class) {
            @Override
            public String deserialize(JsonParser jsonParser, DeserializationContext ctx)
                throws IOException {
              // trim white space
              return StringUtils.trimWhitespace(jsonParser.getValueAsString());
            }
          });
    }
  }
}

方法4-如果是FastJson的话(配置全局生效)

在Spring Boot中将Jackson替换为fastjson一般会有两种方式:
第一种:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
 
    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverter() {
        return new HttpMessageConverters(new FastJsonHttpMessageConverter());
    }
}

第二种:

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
 
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastConverter = 
        new FastJsonHttpMessageConverter();
 
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        converters.add(fastConverter);
    }
}

替换成fastjson之后,对于精度丢失问题,解决方法如下:

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
 
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastConverter = 
        new FastJsonHttpMessageConverter();
 
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        SerializeConfig serializeConfig = SerializeConfig.globalInstance;
        serializeConfig.put(BigInteger.class, ToStringSerializer.instance);
        serializeConfig.put(Long.class, ToStringSerializer.instance);
        serializeConfig.put(Long.TYPE, ToStringSerializer.instance);
        fastJsonConfig.setSerializeConfig(serializeConfig);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        converters.add(fastConverter);
    }
}

方法5-List<Long>类型精度丢失问题

最好的方式就是,将List<Long>改为List<String>方式,这样子啥事没有,性能也高,但是我就是想多折腾:

1.数组转换为String显示(不推荐)

直接使用官方的即可:

@JsonSerialize(using = ToStringSerializer.class)
private List<Long> roleIds;

前端显示的,这明显需要前端特殊处理,个人不太喜欢这样子:

{
  "roleIds": "[1333010224414613506, 1333010224481722369]"
}

2.自定义序列化方式转换为String数组(推荐)

自定义一个JSON序列化方式:

public class ListLongToStringArrayJsonSerializer extends JsonSerializer<List<Long>> {

    @Override
    public void serialize(List<Long> values, JsonGenerator gen, SerializerProvider serializers) throws IOException {
      String[] newValues =
        ObjectUtil.defaultIfNull(values, Collections.emptyList()).stream()
          .map(String::valueOf)
          .toArray(String[]::new);
      gen.writeArray(newValues, 0, newValues.length);
    }
}

特别注意:此处的gen.writeArray(newValues, 0, newValues.length);,类型为String的,Jackson在2.11版本之后才支持此String类型的。

使用方式:

@JsonSerialize(using = LongToJsonSerializer.class)
private List<Long> roleIds;

结果:

{
  "roleIds": [
    "1333010224414613506",
    "1333010224481722369"
  ]
}

方法6-前端使用String类型来接收

e.g.

aliyunOssFileId: ''
正文到此结束
本文目录