序列化方式处理枚举字段回显

序列化方式处理枚举字段回显

🗯️ 一般在处理项目中枚举值时,都是把枚举值存入数据库,给前端回显的时候要么前端自己处理好映射,要么后端直接把枚举值代表的中文释义处理好返回去前端直接展示。这样会存在很多让人不舒服的点,比如:

  • 项目枚举太多;
  • 前端自己处理的话就需要把全部的映射写好,并且如果后端改了前端也得改;
  • 后端自己处理的话,就需要每个有枚举值字段的对象中都需要添加相应的回显字段,在针对每个枚举值去处理回显字段。

这对于开发者来说无疑都是一种折磨。
一般为了回显准确都是后端处理枚举值,前端直接回显,但是作为后端的你是不是对这种操作非常痛恨和无奈,痛恨去写这种没营养的代码,又无奈怎么不直接保存中文(谁让编码都是英文的呢🤷,天生对汉字就不够友好)。

为什么数据库中不直接存入中文?
数据库通常不直接保存中文(或其他非ASCII字符)的原因主要有以下几点:

  1. 数据库编码支持:早期的一些数据库系统,如ASCII字符集的MySQL,对于非ASCII字符的存储和处理能力较弱。即使在后来的版本中引入了更多的字符集支持,但仍然存在兼容性和性能等问题。
  2. 存储空间和性能考虑:非ASCII字符通常需要使用多字节编码进行存储,相比于ASCII字符,它们占用更多的存储空间。如果存储大量的中文数据,可能会导致数据库表的大小增加,进而影响查询性能和存储需求。
  3. 处理复杂性:涉及到字符排序、索引、搜索等数据库操作时,对于非ASCII字符的处理往往会更加复杂。一些数据库引擎在设计上可能没有完全优化针对非ASCII字符的操作,导致性能下降或功能受限。

尽管如此,现代的数据库系统已经提供了更好的支持来存储和处理非ASCII字符,例如采用Unicode编码的数据库,常见的关系型数据库如MySQL、PostgreSQL都支持UTF-8编码,可保存包括中文在内的各种字符。

因此,在选择数据库时,应根据具体需求和预期数据量来评估数据库的字符集支持、性能和存储需求,选择适合的字符集和合理的存储方式。对于需要存储大量中文数据或其他非ASCII字符的场景,使用支持Unicode编码的数据库,并进行正确的配置,可以有效地保存和操作这些字符数据。

所以,咱也不是不能存中文🤔
回到主题。最近也碰到了这种问题,需要大量枚举值回显,但是心里十万个不想写,就在想怎么能偷懒,最起码不用写一大堆的匹配逻辑。
基于以下场景去分析:

  1. 枚举值回显字段只用来给前端回显,后端用不着
  2. 项目中的枚举值都有对应的枚举类
  3. 枚举类都包含统一的属性,比如:
//枚举值
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 "";
}

这样一个自定义注解+序列化器实现枚举值回显的逻辑就完成了,在枚举字段上使用这个注解就可以在返回给前端时加上回显字段,终于偷懒成功!但也存在一些可以优化的点:

  1. 只能处理这种约定好的枚举(上面分析的),当然你也可以拓展上面的代码,使其支持在EnumNameParse中传参匹配枚举属性(比如code)和获取回显值。
  2. 后端对象中不在编写回显字段,对于接口文档来说可能不太友好,因为可能看不到这个字段,只能按约定好的字段名来。
  3. 如果对象中已经编写了回显字段,并且回显字段的顺序在枚举字段之下,那么返回时还是空的,因为枚举字段序列化时给回显字段赋值之后,回显字段本身执行序列化又被字段本身的值给覆盖,所以还是空的。

解决办法:

a. 编写时调整字段顺序,将回显字段写在枚举字段之前,
b. 或者通过@JsonPropertyOrder({“回显字段”, “枚举字段”})来调整序列化顺序。

  1. 如果枚举值为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);
    }
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值