分布式项目中广泛使用雪花算法生成ID作为数据库表的主键,Long类型的雪花ID有19位,而前端接收Long类型用的是number类型,但是number类型的精度只有16位。这就导致雪花ID传到前端会出现精度丢失的问题。如下:
数据库的值:1461642036950462475
前端接收的值:1461642036950462500
(拓展)
使用雪花算法ID的优点:
缺点:
1.依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。
UUID方式明显的缺点是因为其无序的规则,导致数据库插入效率低,自增方式明显的缺点是不适合分表分库场景,分布式存储的场景会出现主键冲突。而且安全性低,因为ID增长有规律的,容易被非法获取数据。所以综合考虑下,使用雪花算法生成ID的方式更为推荐。
如果要将id字段从Long类型改成String类型,涉及到表,实体类等等代码的改动,影响面比较大所以这个做法是不提倡的。
Jackson是Spring框架默认的序列化框架,使用Jackson的@JsonSerialize注解可以完美解决这个问题。具体做法:后端的ID(Long) ==> Jackson(Long转String) ==> 前端使用String类型接收,并且Jackson反序列化默认支持的前端穿回来的String类型的19位数字字段用Long类型接收。
使用方面,将@JsonSerialize注解定义在字段上即可。Jackson提供了许多Json序列化器。另外我们也可以自定义序列化器。
需要注意不要再引入Gson,在我使用过程中发现同时引入Gson包的话,会导致@JsonSerialize注解失效。
在项目中都是将注解标注在对应字段上,在Json序列化的时候把Long自动转为String。
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
需要注意的是被转换的字段必须是包装类类型,否则会转换失败。
@JsonSerialize(using = ToStringSerializer.class)
private Long parentId; //转化成功
@JsonSerialize(using = ToStringSerializer.class)
private long parentId; //转化失败
对于雪花ID集合,我们继续使用 ToStringSerializer,前端解析接收到到之后,还需要特殊处理。因此需要针对List类型的雪花ID集合自定义序列化器,实现将Long转换成String类型传递给前端。
自定义序列化器ListLongToStringArrayJsonSerializer,继承JsonSerializer
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);
}
}
具体使用:
@JsonSerialize(using = ListLongToStringArrayJsonSerializer.class)
private List<Long> idList;
每个实体类的id字段都需要加@JsonSerialize注解有些繁琐,我们可以通过先修改Jackson转换器,实现全局统一处理Long类型字段。如下所示:
@EnableWebMvc
@Configuration
public class MvcConfig implements WebMvcConfigurer {
/**
* 重写Jackson转换器
* Long类型转String类型
*
* 解决前端Long类型精度丢失问题(js解析只能解析到16位)
*
* @param converters
*/
@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));
}
}
但是因为这里的全局配置是将所有Long类型的字段都转换成String传给前端,可能导致前端界面存在有类型不兼容的情况,所以这里的全局配置需要谨慎使用。
更多: http://www.lzhpo.com/article/106