Jackson在Spring Boot高级应用技巧【Long精度丢失, @JsonValue, 数据脱敏】

文摘   2024-11-05 08:00   新疆  

最新实战案例锦集:《Spring Boot3实战案例合集》持续更新,每天至少更新一篇文章,订阅后将赠送文章最后展示的所有MD文档(学习笔记)。

环境:SpringBoot3.2.5



1. 简介

在之前的两篇文章中已经对关于jackson在Spring Boot中的应用做过非常多的介绍,有兴趣可查看下面这两篇文章

Jackson才是王!SpringBoot优雅的控制JSON数据

Jackson在Spring Boot中的各种开发技巧,非常实用

本篇文章,我们将介绍下面2个主题:

  1. Long类型在前端缺失精度

  2. @JsonValue序列化单个属性值

  3. 自定义注解的应用

     

2. 实战案例

2.1 Long精度丢失

在Web开发中,使用JavaScript处理大数值(如Long类型)时,可能会遇到精度丢失的问题。这是因为JavaScript中的数字一律采用IEEE-754标准的64位浮点数表示,这种格式可以很好地处理大多数数字运算,但对于非常大的整数(超出2^53 - 1),就可能出现精度损失,这就非常的恶心了,可能会让你感到莫名其妙。当从后端获取到Long类型的值并尝试在前端展示时,如果该值超过了JavaScript安全整数范围,那么显示的结果可能不准确,如下示例:

@RestController@RequestMapping("/longs")public class LongController {
@GetMapping("") public Map<String, Object> getData() { return Map.of("code", 0, "data", 123456789012345678L) ; }}

接口返回了一个17位的Long类型数据,我们先直接通过浏览器访问,结果如下:

我们看到了2个结果,浏览器显示的是正确的,但是通过 Network 查看数据错误,接下来,我们再通过Ajax获取数据。

function getData() {  axios.get('http://localhost:8080/longs')    .then(resp => {      console.log(resp.data) ;    }).catch((error) => {      console.log(error) ;    });}

在这种前后端分离的架构下,千万的小心,如果你返回的Long类型超过了前端number的范围,那么将出现该问题。接下来要解决也是非常的简单,我们只需要进行简单的配置即可。

@Componentpublic class PackMapperCustomizer implements Jackson2ObjectMapperBuilderCustomizer {
@Override public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) { jacksonObjectMapperBuilder.serializerByType(Long.class, ToStringSerializer.instance) ; }}

这里我们将Long类型转换为String类型。再次运行上面的代码

通过自定义 Jackson2ObjectMapperBuilderCustomizer 属于全局的,项目中所有的Long都会被转换为String进行输出。这可能不是我们希望的,我们可能只是希望有针对的处理,这时候我们就可以通过注解的方式了,如下示例:

public class User {
@JsonSerialize(using = ToStringSerializer.class) private Long id ; private String name ; // getters, setters}

通过上面注解的方式就能针对性的控制输出字段类型。

2.2 @JsonValue序列化单个属性值

@JsonValue注解用于指示一个方法,该方法将被用来转换对象为JSON值。通常,这个方法会返回一个基本类型或其包装类的实例,或者是其他可以直接序列化为JSON的对象。使用 @JsonValue 可以简化对象到JSON的转换过程,使得特定属性或计算结果能够直接作为JSON输出,而无需定义复杂的序列化逻辑。

现在有,如下的枚举类

public enum PaymentStatus {
NO_PAY(0, "未支付"), PAID(1, "已支付") ; PaymentStatus(int code, String desc) { this.code = code ; this.desc = desc ; } private Integer code ; private String desc ; // getters, setters}

当接口返回该枚举类时,显示如下:

@GetMapping("")public PaymentStatus status() {  return PaymentStatus.PAID ;}

输出了枚举的字符串形式,如果我们要显示具体的code或者是desc,那么我们就可以使用 @JsonValue 注解。

@JsonValueprivate String desc ;

在需要输出的字段上添加 @JsonValue 注解即可。

如果该注解应用到一个普通的Java Bean对象中的某个属性时,如下示例:

public class User {
@JsonSerialize(using = ToStringSerializer.class) private Long id ; @JsonValue private String name ;}

这时候接口输出的将是name属性对应的值。


2.3 自定义注解

有些时候,我们可能对输出的某些字段要做特殊的处理在输出到前端,比如:身份证号,电话等信息,在前端展示的时候我们需要进行脱敏处理,这时候通过自定义注解就非常的有用了。在Jackson中要自定义注解,我们可以通过@JacksonAnnotationsInside注解来实现,如下示例:

自定义注解

@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@JacksonAnnotationsInside@JsonSerialize(using = SensitiveSerializer.class)public @interface Sensitive {
  int start() default 0 ;   int end() default 0 ;   String mask() default "*" ;}

自定义序列化处理器SensitiveSerializer

public class SensitiveSerializer extends JsonSerializer<String> implements ContextualSerializer {
private Sensitive sensitive;
@Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { String val = value; if (sensitive != null && StringUtils.hasLength(val)) { String m = sensitive.mask(); int start = sensitive.start(); int end = sensitive.end(); int totalLength = value.length();
if (totalLength <= 2) { val = totalLength == 1 ? value + m : value.substring(0, 1) + m; } else if (totalLength <= 6) { val = value.substring(0, 1) + String.join("", Collections.nCopies(totalLength - 2, m)) + value.substring(totalLength - 1); } else { int prefixLength = Math.min(start, totalLength - 1); int suffixLength = Math.min(end, totalLength - 1);
if (prefixLength > totalLength) { prefixLength = totalLength / 2; } if (suffixLength > totalLength) { suffixLength = totalLength / 2; }
int maskLength = Math.max(0, totalLength - (prefixLength + suffixLength)); if (maskLength == 0) { prefixLength -= 2; suffixLength -= 2; maskLength = Math.max(2, totalLength - (prefixLength + suffixLength)); }
prefixLength = Math.min(prefixLength, totalLength - 1); suffixLength = Math.min(suffixLength, totalLength - 1);
maskLength = totalLength - prefixLength - suffixLength;
val = value.substring(0, prefixLength) + String.join("", Collections.nCopies(maskLength, m)) + value.substring(totalLength - suffixLength); } } gen.writeString(val); }
@Override public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { sensitive = property.getAnnotation(Sensitive.class); return this; }}

接下来,在输出的Java Bean中使用上面的注解

public class User {
@JsonSerialize(using = ToStringSerializer.class)  private Long id ; private String name ; @Sensitive(start = 6, end = 4) private String idCard ; @Sensitive(start = 4, end = 3) private String phone ;  // getters, setters}

最后,在前端展示结果如下:

敏感数据得到了脱敏处理。

以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

推荐文章

Tika 与 Spring Boot 的完美结合:支持任意文档解析的神器

Controller 接口支持多达26种参数解析方式

优雅!通过编程方式重启SpringBoot应用的3种方案

强大!SpringBoot通过插件,动态扩展系统功能

几个强大的Spring Boot扩展点

在 Spring Boot 中加载属性文件的7种方法

SpringBoot读取配置文件信息8种方式,你会哪几种?

基于SpringBoot支持任意文件在线预览

当心!SpringBoot在以下几种情况将导致代理失效

总结7种JVM出现OOM时的原因及解决方案

优雅!SpringBoot统一返回结果就该这样处理

强大!SpringBoot通过3种方式实现AOP切面,第三种方式性能极佳

SpringBoot3第三方接口调用10种方式,最后一种你肯定没用过

Spring全家桶实战案例源码
spring, springboot, springcloud 案例开发详解
 最新文章