序列化方式处理枚举字段回显
🗯️ 一般在处理项目中枚举值时,都是把枚举值存入数据库,给前端回显的时候要么前端自己处理好映射,要么后端直接把枚举值代表的中文释义处理好返回去前端直接展示。这样会存在很多让人不舒服的点,比如:
- 项目枚举太多;
- 前端自己处理的话就需要把全部的映射写好,并且如果后端改了前端也得改;
- 后端自己处理的话,就需要每个有枚举值字段的对象中都需要添加相应的回显字段,在针对每个枚举值去处理回显字段。
这对于开发者来说无疑都是一种折磨。
一般为了回显准确都是后端处理枚举值,前端直接回显,但是作为后端的你是不是对这种操作非常痛恨和无奈,痛恨去写这种没营养的代码,又无奈怎么不直接保存中文(谁让编码都是英文的呢🤷,天生对汉字就不够友好)。
为什么数据库中不直接存入中文?
数据库通常不直接保存中文(或其他非ASCII字符)的原因主要有以下几点:
- 数据库编码支持:早期的一些数据库系统,如ASCII字符集的MySQL,对于非ASCII字符的存储和处理能力较弱。即使在后来的版本中引入了更多的字符集支持,但仍然存在兼容性和性能等问题。
- 存储空间和性能考虑:非ASCII字符通常需要使用多字节编码进行存储,相比于ASCII字符,它们占用更多的存储空间。如果存储大量的中文数据,可能会导致数据库表的大小增加,进而影响查询性能和存储需求。
- 处理复杂性:涉及到字符排序、索引、搜索等数据库操作时,对于非ASCII字符的处理往往会更加复杂。一些数据库引擎在设计上可能没有完全优化针对非ASCII字符的操作,导致性能下降或功能受限。
尽管如此,现代的数据库系统已经提供了更好的支持来存储和处理非ASCII字符,例如采用Unicode编码的数据库,常见的关系型数据库如MySQL、PostgreSQL都支持UTF-8编码,可保存包括中文在内的各种字符。
因此,在选择数据库时,应根据具体需求和预期数据量来评估数据库的字符集支持、性能和存储需求,选择适合的字符集和合理的存储方式。对于需要存储大量中文数据或其他非ASCII字符的场景,使用支持Unicode编码的数据库,并进行正确的配置,可以有效地保存和操作这些字符数据。
所以,咱也不是不能存中文🤔
回到主题。最近也碰到了这种问题,需要大量枚举值回显,但是心里十万个不想写,就在想怎么能偷懒,最起码不用写一大堆的匹配逻辑。
基于以下场景去分析:
- 枚举值回显字段只用来给前端回显,后端用不着
- 项目中的枚举值都有对应的枚举类
- 枚举类都包含统一的属性,比如:
//枚举值
private Integer code;
//回显值
private String showName;
这样,后端只需要在返回给前端之前处理一下枚举值就行,以前的做法是添加回显字段,重写getter方法,在方法里匹配枚举。
进一步思考:既然返回给前端之前处理, 那返回给前端之前的最后一步是什么?那肯定是序列化了,我们在枚举字段的序列化时去处理,同时在序列化时写入一个新的字段代表回显字段,这样既不用在对象中编码回显字段,也不用去写回显逻辑,只需要在序列化器中利用反射写一个通用的回显逻辑即可。开整!
1、自定义注解
我们需要去自定义一个枚举,用来标识枚举字段,参数中传递枚举类,这样在序列化时能够去执行匹配逻辑
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fzznkj.boot.common.parser.EnumNameParser;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @desc 枚举值序列化时添加显示名称字段并赋值
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumNameParse {
/**
* 枚举类型
*
* @return
*/
Class byEnum();
/**
* 存放枚举显示值的目标字段, 不填的话默认当前注解修饰字段名+'Name'
*
* @return
*/
String targetFieldName() default "";
}
2、自定义序列化器
在序列化器中我们拿到注解中的参数枚举类和目标字段名,在序列化方法serialize中根据枚举类匹配枚举值,获取回显值,并写入回显字段,最终返回给前端
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fzznkj.boot.common.annotation.EnumNameParse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* @desc 枚举名称转换器
*/
@Slf4j
public class EnumNameParser extends JsonSerializer<Integer> implements ContextualSerializer {
/**
* 枚举类型
*/
private Class type;
/**
* 存放枚举显示值的目标字段
*/
private String targetFieldName;
/**
* 属性赋值
*
* @param type
* @param targetFieldName
* @return
*/
public EnumNameParser(Class type, String targetFieldName) {
this.type = type;
this.targetFieldName = targetFieldName;
log.info("使用枚举名称转换序列化器, 目标字段{}, 枚举{}", targetFieldName, type == null ? null : type.getName());
}
/**
* 必须要有, 执行{@link EnumNameParser#createContextual(SerializerProvider, BeanProperty)}方法之前需要先实例化
*/
public EnumNameParser() {
}
/**
* {@link EnumNameParse}枚举修饰字段的序列化逻辑
*
* @param value Value to serialize; can <b>not</b> be null.
* @param gen Generator used to output resulting Json content
* @param provider Provider that can be used to get serializers for
* serializing Objects value contains, if any.
* @throws IOException
*/
@Override
public void serialize(Integer value, JsonGenerator gen, SerializerProvider provider) throws IOException {
//必须先给自己赋值, 不然序列化的时候当前字段没有值会报错
gen.writeNumber(value);
if (type == null || !type.isEnum()) {
return;
}
String showName = StrUtil.EMPTY;
for (Object constant : type.getEnumConstants()) {
//写死了根据code匹配
int code = (int) ReflectUtil.getFieldValue(constant, "code");
if (code == value) {
showName = (String) ReflectUtil.getFieldValue(constant, "showName");
break;
}
}
try {
//将回显字段和值写入
gen.writeStringField(targetFieldName, showName);
} catch (IOException e) {
log.error("添加字段{}={}失败, 失败原因:{}", targetFieldName, showName, e.getMessage());
}
}
/**
* 获取字段上枚举参数并传递给序列化器,再由序列化器进行字段的序列化
*
* @param prov Serializer provider to use for accessing config, other serializers
* @param beanProperty Method or field that represents the property
* (and is used to access value to serialize).
* Should be available; but there may be cases where caller cannot provide it and
* null is passed instead (in which case impls usually pass 'this' serializer as is)
* @return
* @throws JsonMappingException
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
//为空直接跳过
final EnumNameParse getEnumName = beanProperty.getAnnotation(EnumNameParse.class);
if (getEnumName != null) {
String fieldName = getEnumName.targetFieldName();
if (StrUtil.isBlank(fieldName)) {
final String propertyName = beanProperty.getName();
fieldName = propertyName + "Name";
}
return new EnumNameParser(getEnumName.byEnum(), fieldName);
}
return prov.findValueSerializer(beanProperty.getType(), beanProperty);
}
return prov.findNullValueSerializer(beanProperty);
}
}
编写好序列化器之后,在自定义注解上添加Jackson的序列化注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = EnumNameParser.class)
@JacksonAnnotationsInside
public @interface EnumNameParse {
/**
* 枚举类型
*
* @return
*/
Class byEnum();
/**
* 存放枚举显示值的目标字段, 不填的话默认当前注解修饰字段名+'Name'
*
* @return
*/
String targetFieldName() default "";
}
这样一个自定义注解+序列化器实现枚举值回显的逻辑就完成了,在枚举字段上使用这个注解就可以在返回给前端时加上回显字段,终于偷懒成功!但也存在一些可以优化的点:
- 只能处理这种约定好的枚举(上面分析的),当然你也可以拓展上面的代码,使其支持在EnumNameParse中传参匹配枚举属性(比如code)和获取回显值。
- 后端对象中不在编写回显字段,对于接口文档来说可能不太友好,因为可能看不到这个字段,只能按约定好的字段名来。
- 如果对象中已经编写了回显字段,并且回显字段的顺序在枚举字段之下,那么返回时还是空的,因为枚举字段序列化时给回显字段赋值之后,回显字段本身执行序列化又被字段本身的值给覆盖,所以还是空的。
解决办法:
a. 编写时调整字段顺序,将回显字段写在枚举字段之前,
b. 或者通过@JsonPropertyOrder({“回显字段”, “枚举字段”})来调整序列化顺序。
- 如果枚举值为null,会跳过执行自定义序列化器,那么返回给前端就没有回显字段。
解决办法:
a. @JsonSerialize注解中指定null值序列化器:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @JsonSerialize(using = EnumNameParser.class, nullsUsing = EnumNameParser.class) @JacksonAnnotationsInside public @interface EnumNameParse {}
b. 自定义序列化器中兼容null值
@Override public void serialize(Integer value, JsonGenerator gen, SerializerProvider provider) throws IOException { //必须先给自己赋值, 不然序列化的时候当前字段没有值会报错 gen.writeNumber(value == null ? -100 : value); String showName = StrUtil.EMPTY; if (value != null && type != null && type.isEnum()) { for (Object constant : type.getEnumConstants()) { //写死了根据code匹配 int code = (int) ReflectUtil.getFieldValue(constant, "code"); if (code == value) { showName = (String) ReflectUtil.getFieldValue(constant, "showName"); break; } } } if (targetFieldName == null) { //获取当前上下文中的字段名 targetFieldName = gen.getOutputContext().getCurrentName() + "Name"; } try { //将回显字段和值写入 gen.writeStringField(targetFieldName, showName); } catch (IOException e) { log.error("添加字段{}={}失败, 失败原因:{}", targetFieldName, showName, e.getMessage()); } }
3、优化:支持在原始字段上写入转换后的值
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fzznkj.boot.common.parser.EnumNameParser;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @desc 枚举值序列化时添加显示名称字段并赋值, 使用此注解的枚举必须有code和showName字段,且对象中:
* <ul>
* <li>1.要么没有和回显字段同名的字段</li>
* <li>2.要么手写的回显字段顺序在枚举字段之前</li>
* </ul>
* 不然回显字段会是空
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JsonSerialize(using = EnumNameParser.class, nullsUsing = EnumNameParser.class)
@JacksonAnnotationsInside
public @interface EnumNameParse {
/**
* 枚举类型,必须有code和showName字段
*
* @return
*/
Class byEnum();
/**
* 枚举匹配字段
* @return
*/
String key() default "code";
/**
* 枚举值字段
* @return
*/
String valueKey() default "showName";
/**
* 是否创建新的字段, 否的话直接在原始字段上返回转换后的值
* @return
*/
boolean createNewField() default false;
/**
* 存放枚举显示值的目标字段, 不填的话默认当前注解修饰字段名+'Name'
*
* @return
*/
String targetFieldName() default StrUtil.EMPTY;
}
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fzznkj.boot.common.annotation.EnumNameParse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
/**
* @desc 枚举名称转换器
*/
@Slf4j
public class EnumNameParser extends JsonSerializer<Integer> implements ContextualSerializer {
/**
* 枚举类型
*/
private Class type;
/**
* 枚举匹配字段
*
* @return
*/
private String key;
/**
* 枚举值字段
*
* @return
*/
private String valueKey;
/**
* 是否创建新的字段, 否的话直接在原始字段上返回转换后的值
*/
private boolean createNewField;
/**
* 存放枚举显示值的目标字段
*/
private String targetFieldName;
/**
* 属性赋值
*
* @param enumNameParse
* @return
*/
public EnumNameParser(EnumNameParse enumNameParse) {
this.type = enumNameParse.byEnum();
this.key = enumNameParse.key();
this.valueKey = enumNameParse.valueKey();
this.createNewField = enumNameParse.createNewField();
this.targetFieldName = enumNameParse.targetFieldName();
log.info("使用枚举名称转换序列化器, 目标字段{}, 枚举{}", targetFieldName, type == null ? null : type.getName());
}
/**
* 必须要有, 执行{@link EnumNameParser#createContextual(SerializerProvider, BeanProperty)}方法之前需要先实例化
*/
public EnumNameParser() {
}
/**
* {@link EnumNameParse}枚举修饰字段的序列化逻辑
*
* @param value Value to serialize; can <b>not</b> be null.
* @param gen Generator used to output resulting Json content
* @param provider Provider that can be used to get serializers for
* serializing Objects value contains, if any.
* @throws IOException
*/
@Override
public void serialize(Integer value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (createNewField && StrUtil.isBlank(targetFieldName)) {
//获取当前上下文中的字段名
targetFieldName = gen.getOutputContext().getCurrentName() + "Name";
}
if (value == null) {
gen.writeNull();
if (createNewField) {
gen.writeNullField(targetFieldName);
}
return;
}
String showName = StrUtil.EMPTY;
if (value != null && type != null && type.isEnum()) {
for (Object constant : type.getEnumConstants()) {
//根据key匹配
Object code = ReflectUtil.getFieldValue(constant, key);
if (Objects.equals(code, value)) {
showName = (String) ReflectUtil.getFieldValue(constant, valueKey);
break;
}
}
}
if (!createNewField) {
//不创建新字段的话直接在原始字段上赋值
gen.writeString(showName);
return;
} else {
//必须先给自己赋值, 不然序列化的时候当前字段没有值会报错
gen.writeObject(value);
}
try {
//将回显字段和值写入
gen.writeStringField(targetFieldName, showName);
} catch (IOException e) {
log.error("添加字段{}={}失败, 失败原因:{}", targetFieldName, showName, e.getMessage());
}
}
/**
* 获取字段上枚举参数并传递给序列化器,再由序列化器进行字段的序列化
*
* @param prov Serializer provider to use for accessing config, other serializers
* @param beanProperty Method or field that represents the property
* (and is used to access value to serialize).
* Should be available; but there may be cases where caller cannot provide it and
* null is passed instead (in which case impls usually pass 'this' serializer as is)
* @return
* @throws JsonMappingException
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty beanProperty) throws JsonMappingException {
if (beanProperty != null) {
//为空直接跳过
final EnumNameParse getEnumName = beanProperty.getAnnotation(EnumNameParse.class);
if (getEnumName != null) {
return new EnumNameParser(getEnumName);
}
return prov.findValueSerializer(beanProperty.getType(), beanProperty);
}
return prov.findNullValueSerializer(beanProperty);
}
}