最近在做一个 SpringBoot + Vue 的项目,持久层框架用的是 MyBatis-Plus,然后遇到了一个问题,一起来看下怎么回事。

这个项目就是一个文章收藏器,可以收藏一些技术文章,然后可以选择星标,以便查找这篇文章。
在这里插入图片描述
那么点击星标的按钮,实际上就是调了后端一个接口,更新了数据库中相应字段。每一个列表的字段如下:
在这里插入图片描述
可以看到,标记星标的字段就是 isFavorite ,没有星标的时候是 0 ,星标之后变成 1 。同时这边还有个 id 字段,这个字段是由 MyBatis-Plus 插入数据库时自动填充的,是一个 Long 类型的 uuid 。在调接口的时候,后台根据传过来的 id 进行条件更新。

但是在测试的时候,调接口发现 isFavorite 一直更新失败,但是SQL 语句正常执行,没有报错,看一下打印的 SQL 语句:
在这里插入图片描述
然后 Controller 层的逻辑是这样写的:

@PutMapping
public ResponseEntity<ServerResponse> changeFavoriteStatus(
        @NotBlank(message = "文章 ID 不能为空") @RequestParam("articleId") String articleId,
        @NotBlank(message = "星标状态不能为空") @RequestParam("isFavorite") String isFavorite
) {
	LambdaUpdateWrapper<ArticleModel> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper
            .eq(ArticleModel::getId, Long.valueOf(articleId))
            .set(ArticleModel::getIsFavorite, Integer.valueOf(isFavorite));
	articleMapper.update(null, updateWrapper);
    return ServerResponse.ok(null);
}

这就有点头疼了,关键没有报错,无从下手啊。假如是 MyBatis-Plus 提供的 API 有问题,应该会有报错信息。然后网上查了很多资料,看别人的代码都是跟我一样写的,但是也没看他们遇到这样的问题。

后来尝试硬编码,直接在代码里面填写参数,发现可以更新成功:

@PutMapping
public ResponseEntity<ServerResponse> changeFavoriteStatus(
        @NotBlank(message = "文章 ID 不能为空") @RequestParam("articleId") String articleId,
        @NotBlank(message = "星标状态不能为空") @RequestParam("isFavorite") String isFavorite
) {
	LambdaUpdateWrapper<ArticleModel> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper
            .eq(ArticleModel::getId, 1404788380714967042L)
            .set(ArticleModel::getIsFavorite, 1);
	articleMapper.update(null, updateWrapper);
    return ServerResponse.ok(null);
}

这样一下就有了思路,因为我这边的 id 是直接数据库复制过来的,那么基本可以确定是前端传过来的 id 有问题。看一下查询接口打印的 SQL 语句:
在这里插入图片描述
看到第一条记录的 id 字段是 1404788380714967042 。然后再来看一下传给前端的字段,发现确实不一样:
在这里插入图片描述
综上,本人再查询了相关资料,Java 中 long 数据类型是 64 位,最大值是 9,223,372,036,854,775,807 ,也就是 2^63 - 1 ,然后 JavaScript 中的 number 类型,最大的安全整数 2^53 - 1

也就是说,Java 中的 Long 类型能表示的范围比 JS 的 number 类型大,如果后端在序列化的时候直接用 Long 类型,前端在反序列化的时候,number 会损失精度,导致 id 字段不是实际的 id ,进而查询不到记录,无法更新。因此对于 Long 类型,后端需要转为字符串才能传给前端。

转为字符串的话,常规的做法是定义一个 DTO 类,将 Long 类型的字段都定义成 String ,那么这样的做法比较麻烦,而且如果接口有很多,需要大量的 DTO 。另一种做法是使用注解,在序列化的时候保留精度。

SpringBoot 默认使用的 JSON 解析框架是 jackson,可以直接使用。如果要用第三方框架,例如 fastjson 则需要进行配置。以 jackson 为例,对于在序列化时需要保留精度的字段,添加 @JsonSerialize 注解即可:

@Data
@TableName("tb_article")
public class ArticleModel {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    private Integer isFavorite;
}

在添加注解之后,前端拿到的参数如下,可以看到 Long 类型 id 变成字符串了:
在这里插入图片描述
另外这边还有一个时间戳,没有转为字符串,本人认为没有必要,因为 JS 里面本身也有时间戳哈,而且这个范围没有超出 number 的最大安全整数,可以直接用。另外 jackson 还提供了注解可以在序列化时自定义格式化:

@Data
//序列化、反序列化忽略的属性,多个时用“,”隔开
@JsonIgnoreProperties({"captcha"})
//当属性的值为空(null或者"")时,不进行序列化,可以减少数据传输
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class UserVoByJson {

    // 序列化、反序列化时,属性的名称
    @JsonProperty("userName")
    private String username;

    // 为反序列化期间要接受的属性定义一个或多个替代名称,可以与@JsonProperty一起使用
    @JsonAlias({"pass_word", "passWord"})
    @JsonProperty("pwd")
    private String password;

    //序列化、反序列化时,格式化时间
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createDate;

    //序列化、反序列化忽略属性
    @JsonIgnore
    private String captcha;

}

参考:
java long类型报错:error: integer number too large
java的long类型传输到前端损失精度
Springboot引入FastJson
修复Long类型太长,而Java序列化JSON丢失精度问题的方法
Number.MAX_SAFE_INTEGER - MDN
SpringBoot系列——Jackson序列化

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐