原文:赵侠客
前言
字节(Byte
)是计算机信息技术用于计量存储容量的一种基本单位,通常简写为B
,1Byte=8bit
,在ASCII
编码中1Byte
可以表示一个标准的英文字符,包括大写字母、小写字母、数字、标点符号和控制字符等,共128个不同的字符,如1、2、3、a、b、c
都占用一个Byte
,所以1Byte
其实是非常小的单位,比Byte
大的单位就是KB
,一般一篇博客文字的大小应该在几十KB
,比KB
大的单位是MB
,目前手机拍摄一张照片的大小大概是几MB
,比MB
大的还有GB、TB、PB、EB、ZB、YB
,以下是各单位间的转换。
名称 | 简写 | 换算 |
---|---|---|
比特(Byte) | B | 1B=8bit |
千字节(KiloByte) | KB | 1KB=2^10 B =1024B |
兆字节(Mega Byte) | MB | 1MB=2^10 KB =2^20 B |
吉字节(GigaByte) | GB | 1GB=2^10 MB =2^30 B |
太字节(TeraByte) | TB | 1TB=2^10 GB =2^40 B |
拍字节(PetaByte) | PB | 1PB=2^10 TB =2^50 B |
艾字节(EXAByte) | EB | 1EB=2^10 PB =2^60 B |
泽字节(Zetta Byte) | ZB | 1ZB=2^10 EB =2^70 B |
尧字节(Yotta Byte) | YB | 1YB=2^10 ZB =2^80 B |
从字节有这么多单位可以看出选择合适的单位可以让人很直观有个大小概念,比如你可以说我买了最新款的IPhone15 128GB
版本,别人一看就知道是最低配版本了,可能觉得你是买了丐版的来装逼一下,但是你说我买最新款的IPhone15Pro 134217728KB
版本,别人第一感肯定不知道你买的是一个丐版,但是会觉得你是个SB。为了精度我一般在数据库中会存储Byte
类型,另外也方便我们在代码中作计算和比较,返回给用户时则会转成对用户友好的单位,例如我们记录用户空间使用量在数据库中会存储最小单位Byte
:
用户ID | 空间用量 |
---|---|
18314 | 2212058073480 |
16765 | 2085350264853 |
15138 | 1439009188728 |
9152 | 1319605924042 |
24080 | 1259223266116 |
3325 | 1139222905087 |
9401 | 1128752330535 |
3838 | 1125023100502 |
返回给用户显示时会转成对用户友好的单位
由于字节的单位比较多,所以代码中会经常出现手动单位转换,这样代码就不太优雅,本文介绍一种优雅处理这些字节转换的方法,接下来我们以用户空间使用量为为例,说明如何优雅的处理这种数据格式转换。
应用场景
比如我们现在有一个类似百度云盘的系统,需要记录用户云盘空间使用量,并且后台可以设置用户云盘的最大容量。那么我们至少有两个接口,一个是返回用户当前云盘空间使用量,另一个是设置云盘最大容量
- 获取用户当前空间使用量接口
GET http://localhost:80/userSize/1返回结果
{"id": 1,"size": "1.5M"
}
需要解决的问题:将数据库存的1572864
格式化成1.5M
- 设置用户最大容量接口
POST http://localhost:80/userSize
Content-Type: application/json { "id":1, "maxSize":"10.5G"
}
需要解决的问题:将前端传的10.5G
转成11274289152
存入数据库
解决思路
目前大部分开发框架都使用SpringBoot
,SpringBoot
将JAVA
对象序列化成JSON
和将JSON
反序列化成JAVA
对象默认使用Jackson
,那么我们可以自定义Jackson
序列化器和反序列器来达到此效果。最终效果是:我们在想要格式化的字段中增加 @ByteFormat(scale = 1)
返回时自动将1572864
格式化成1.5M
,接收时自动将10.5G
转成11274289152
,这样是不是很优雅?而且项目中所有地方只要增加这个注解,就自动处理这个格式转换,下次再遇到字节类型再也不需要去做一大堆的格式转换了。
@Data
public class UserDTO {private Long id;@ByteFormat(scale = 3)private Long size;@ByteFormat(scale = 1)private Long maxSize;
}
实现步骤
定义注解ByteFormat
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = ByteFormatSerializer.class)
@JsonDeserialize(using = ByteFormatDeserializer.class)
@JacksonAnnotationsInside
public @interface ByteFormat {// 保留精度int scale() default 2;
}
Jackson
是可以支持自定义序列化器和反序列化器的, 所以基于此我们可以扩展实现一些自定义序列化注解, 就像 @JsonFormat
注解对时间格式处理一样。 那我们扩展自定义注解原理也很简单,主要是利用 @JsonSerialize
、@JsonDeserialize
、@JacksonAnnotationsInside
注解去实现, @JacksonAnnotationsInside
是一个组合注解,主要标记在用户的自定义注解上,那么这个用户自定义注解上标记的所有其他注解也会生效。
定义序列化器ByteFormatSerializer
ByteFormatSerializer
类的作用是当Jackson
序列化遇到Number
类型时会调用createContextual()
方法,在该方法中判断字段上是否有ByteFormat
注解,如果有则告诉Jackson
来调用ByteFormatSerializer
的serialize
来序列化,在serialize()
方法中完成了数据格式的转换。
public class ByteFormatSerializer extends JsonSerializer<Number> implements ContextualSerializer {protected ByteFormat byteFormat;public ByteFormatSerializer(){}public ByteFormatSerializer(ByteFormat byteFormat){this.byteFormat=byteFormat;}@Overridepublic void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException {if (value == null){return;}int scale = byteFormat.scale();BigDecimal bigValue = new BigDecimal(value.toString());String result = ByteConvert.convertValue(bigValue, scale);gen.writeString(result );}@Overridepublic JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {if (beanProperty != null) {if (Objects.equals(beanProperty.getType().getRawClass().getGenericSuperclass(), Number.class) ) {ByteFormat t = beanProperty.getAnnotation(ByteFormat.class);if (t != null) {return new ByteFormatSerializer(t);}}return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);}return serializerProvider.findNullValueSerializer(beanProperty);}}
定义反序列化器ByteFormatDeserializer
ByteFormatDeserializer
类的作用是Jackson
反序列化遇到Number
类型时会调用createContextual()
方法,在该方法中判断如果字段上有ByteFormat
注解则告诉Jackson
来调用ByteFormatDeserializer
的deserialize
方法,在deserialize()
方法中完成了数据的转换。
public class ByteFormatDeserializer extends JsonDeserializer<Number> implements ContextualDeserializer {protected ByteFormat byteFormat;public ByteFormatDeserializer(){}public ByteFormatDeserializer(ByteFormat byteFormat){this.byteFormat=byteFormat;}@Overridepublic Number deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {if (!StringUtils.hasText(p.getText())) {return null;}if(byteFormat!=null){String value = p.getText();return ByteConvert.convertNumber(value);}return null;}@Overridepublic JsonDeserializer<?> createContextual(DeserializationContext serializerProvider, BeanProperty beanProperty) throws JsonMappingException {if (beanProperty != null) {if (Objects.equals(beanProperty.getType().getRawClass().getGenericSuperclass(), Number.class) ) {ByteFormat t = beanProperty.getAnnotation(ByteFormat.class);if (t != null) {return new ByteFormatDeserializer(t);}}return serializerProvider.findContextualValueDeserializer(beanProperty.getType(), beanProperty);}return this;}
}
格式转换工具类ByteConvert
public class ByteConvert {public static final Long KB=1L<<10;public static final Long MB=KB<<10;public static final Long GB=MB<<10;public static final Long TB=GB<<10;public static String convertValue(BigDecimal bigValue, int scale) {if(bigValue.compareTo(BigDecimal.valueOf(TB))>=0){return String.format("%sT",bigValue.divide(BigDecimal.valueOf(TB), scale, RoundingMode.HALF_UP));}if(bigValue.compareTo(BigDecimal.valueOf(GB))>=0){return String.format("%sG",bigValue.divide(BigDecimal.valueOf(GB), scale, RoundingMode.HALF_UP));}if(bigValue.compareTo(BigDecimal.valueOf(MB))>=0){return String.format("%sM",bigValue.divide(BigDecimal.valueOf(MB), scale, RoundingMode.HALF_UP));}if(bigValue.compareTo(BigDecimal.valueOf(KB))>=0){return String.format("%sK",bigValue.divide(BigDecimal.valueOf(KB), scale, RoundingMode.HALF_UP));}return String.format("%sB",bigValue);}public static Number convertNumber(String stringValue) {if (stringValue.endsWith("T")) {Double value = Double.parseDouble(stringValue.replaceAll("T", "")) * TB;return value.longValue();}if (stringValue.endsWith("G")) {Double value = Double.parseDouble(stringValue.replaceAll("G", "")) * GB;return value.longValue();}if (stringValue.endsWith("M")) {Double value = Double.parseDouble(stringValue.replaceAll("M", "")) * MB;return value.longValue();}if (stringValue.endsWith("K")) {Double value = Double.parseDouble(stringValue.replaceAll("K", "")) * KB;return value.longValue();}return Double.valueOf(stringValue).longValue();}
}
测试
编写两个测试接口,一个接口返回用户当前使用容器量,然后把size
大小设置成1572864
,另一个是设置用户最大使用容量,使用UserDTO
直接接收。
@GetMapping("/userSize/{id}")public ResponseEntity<UserDTO> userSize(@PathVariable Long id) {UserDTO userDTO = new UserDTO();userDTO.setId(id);userDTO.setSize(1572864L);return ResponseEntity.ok(userDTO);}@PostMapping("/userSize")public ResponseEntity<UserDTO> setUserSize(@RequestBody UserDTO userDTO) {log.info("user {} maxSize {}", userDTO.getId(), userDTO.getMaxSize());return ResponseEntity.ok(userDTO);}
可以接口返回用户使用容量字段size
成功格式化成1.5M
,当然如里返回List<UserDTO>
或Map
中也是能正常格式化的,完全符合预期
可以看出用户传maxSize:10.5G
,后端成功使用Long maxSize
类型接收到了String
类型数据,并且将String
数值转成了11274289152
,完全符合预期。
总结
本文使用Jackson
自定义了ByteFormat
注解,解决了字节类型数据在前端与后端之间的优雅转换。当然本方法不仅可以解决字节类型的数据格式转换,还可以用于如时间格式、枚举格式、金钱格式的转换,再扩展一下也可以用于数据脱敏等场景。本解决方法主要有以下优点:
- 使用优雅:使用者只需要在字段上增加
@ByteFormat(scale = 3)
即可,代码很优雅 - 方法通用:该方法不仅可用于
http
接口参数的转换,还可用于Jackson
数据的转换的所有场景 - 降本增效:该方法完全可以在团队中推广,大家都可以使用,不用每个人写一堆转换
- 前端友好:前端拿到这样的接口使用很方便,返回数据直接显示就好,用户输入数据直接传后端
当然本方法也是有缺点的:
- 只能用于
Jackson
:其它JSON
序列化工具不支持如使用FastJson
、Gson
等 - 使用域实体对象:实体对象一旦添加了
ByteFormat
都会作格式转换,如果有特殊场景不想做转换则需要使用新实体对象