暂无图片
暂无图片
3
暂无图片
暂无图片
暂无图片

枚举转换器的使用

原创 soul0202 2025-01-25
305

枚举转换器使用

枚举的使用场景

  1. 有限且固定的取值范围: 当字段的取值范围是有限且稳定的情况下,使用枚举能够清晰地表达这个范围。
  2. 业务含义明确: 当字段具有明确的业务含义,并且这个含义在系统中是一致的,使用枚举有助于保持一致性。


*枚举优缺点分析*

优点:

  1. 类型安全: 使用枚举类型可以在编译时保证类型的安全性,减少运行时的类型错误。
  2. 代码可读性: 将数据库中的数据映射到具体的枚举值,提高代码的可读性和维护性。
  3. 适用场景清晰: 明确了字段代表的含义,使得在数据库和代码中的字段含义更加一致。缺点:
  4. 可扩展性局限: 如果数据库中的字段需要新增枚举值,需要修改代码并重新编译,相对不够灵活。


1.数据库与 Java 枚举的映射

在数据库设计中,有时我们会将一些字段定义为枚举类型,以便更清晰地表示其取值范围。而在 Java 中,使用枚举类型能够更加规范和方便地处理这些有限的取值。在 MyBatis Plus 中,为了能够更好地将数据库中的枚举类型映射到 Java 中,我们需要实现一种方式来处理这种转换关系来实现数据库与 Java 枚举的映射。

1.1定义一个可序列化为json的枚举接口

首先,我们定义一个可序列化为json的枚举接口,其它枚举类只需实现该接口:

@JsonSerialize(using = EntityEnumSerializer.class)
@JsonDeserialize(using = EntityEnumDeserializer.class)
public interface SerializeEnum<T extends Serializable> {
    /** log */
    Logger LOG = LoggerFactory.getLogger(EntityEnumDeserializer.class);
    /** 子类枚举的 code 字段的字段名 */
    String VALUE_FILED_NAME = "code";

    /**
     * 存入数据库的值
     *
     * @return the values
     * @since 0.0.1
     */
    T getCode();

    /**
     * Type
     *
     * @return the class
     * @since 0.0.1
     */
    default Class<?> codeClass() {
        return this.getCode().getClass();
    }

    /**
     * 获取枚举value值
     *
     * @return the desc
     * @since 0.0.1
     */
    String getValue();

    /**
     * 返回枚举名
     *
     * @return the string
     * @since 0.0.1
     */
    String name();

    /**
     * 返回枚举下标
     *
     * @return the int
     * @since 0.0.1
     */
    int ordinal();

    /**
     * Value of e.
     *
     * @param <E>   the type parameter
     * @param clazz the clazz
     * @param value the value
     * @return the e
     * @since 0.0.1
     */
    static <E extends Enum<E> & SerializeEnum<?>> E valueOf(Class<E> clazz, Serializable value) {
        String errorMessage = StrFormatter.format("枚举类型转换错误: 没有找到对应枚举. code = {}, SerializeEnum = {}",
                                                  value, clazz);
        return EnumUtils.of(clazz, e -> e.getCode().equals(value)).orElseThrow(() -> new BusinessException(errorMessage));
    }

    /**
     * 如果无法使用 {@link SerializeEnum#getCode()} 解析枚举, 将再次使用 name() 获取枚举, 最后是 ordinal().
     *
     * @param <T>        parameter
     * @param clz        clz
     * @param finalValue final value
     * @return the enum by order
     * @since 0.0.1
     */
    @SuppressWarnings("unchecked")
    static <T> @NotNull T getEnumByNameOrOrder(Class<? extends Enum<?>> clz, Serializable finalValue) {
        T result;
        String value = String.valueOf(finalValue);
        // 使用 name() 匹配
        Optional<? extends Enum<?>> getForName = EnumUtils.of(clz, e -> e.name().equals(value));
        if (getForName.isPresent()) {
            result = (T) getForName.get();
        } else {
            LOG.debug("无法通过 name 找到枚举, 尝试使用枚举下标查找, name: {}", value);
            throw new BusinessException(StrFormatter.format("未找到匹配的枚举: [{}]", value));
        }
        return result;
    }
}
1.2实现枚举类型处理器

MyBatis Plus 提供了 BaseTypeHandler类,通过继承这个类来实现自定义的类型处理器,实现写DB 时枚举转换把code存入数据库,读取 DB 时通过code转换成枚举。

@SuppressWarnings("all")
public class GeneralEnumTypeHandler<E extends Enum<?>> extends BaseTypeHandler<Enum<?>> {

    /** REFLECTOR_FACTORY */
    private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
    /** TABLE_METHOD_OF_ENUM_TYPES */
    private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
    /** Type */
    private final Class<E> type;
    /** Invoker */
    private final Invoker invoker;

    /**
     * General enum type handler
     *
     * @param type type
     * @since 0.0.1
     */
    @Contract("null -> fail")
    public GeneralEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }

        this.type = type;
        MetaClass metaClass = MetaClass.forClass(type, REFLECTOR_FACTORY);
        String name = SerializeEnum.VALUE_FILED_NAME;

        // 不是 SerializeEnum 枚举时
        if (!SerializeEnum.class.isAssignableFrom(type)) {
            new IllegalArgumentException(String.format("Could not find @SerializeValue in Class: %s.", this.type.getName()));
        }
        // 是 SerializeEnum 子类, 则使用 com.xtm.common.enums.SerializeEnum.getCode
        this.invoker = metaClass.getGetInvoker(name);
    }

    /**
     * 写入 DB 转换
     *
     * @param ps        ps
     * @param i         the first parameter is 1, the second is 2, ...
     * @param parameter parameter
     * @param jdbcType  jdbc type
     * @throws SQLException sql exception
     * @since 0.0.1
     */
    @SuppressWarnings("Duplicates")
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Enum<?> parameter, JdbcType jdbcType)
        throws SQLException {
        if (jdbcType == null) {
            ps.setObject(i, this.getCode(parameter));
        } else {
            ps.setObject(i, this.getCode(parameter), jdbcType.TYPE_CODE);
        }
    }

    /**
     * 读取 DB 时转换
     *
     * @param rs         rs
     * @param columnName column name
     * @return the nullable result
     * @throws SQLException sql exception
     * @since 0.0.1
     */
    @Override
    public E getNullableResult(@NotNull ResultSet rs, String columnName) throws SQLException {
        if (null == rs.getObject(columnName) && rs.wasNull()) {
            return null;
        }
        return this.valueOf(this.type, rs.getObject(columnName));
    }

    /**
     * 读取 DB 时转换
     *
     * @param rs          rs
     * @param columnIndex column index
     * @return the nullable result
     * @throws SQLException sql exception
     * @since 0.0.1
     */
    @Override
    public E getNullableResult(@NotNull ResultSet rs, int columnIndex) throws SQLException {
        if (null == rs.getObject(columnIndex) && rs.wasNull()) {
            return null;
        }
        return this.valueOf(this.type, rs.getObject(columnIndex));
    }

    /**
     * 读取 DB 时转换
     *
     * @param cs          cs
     * @param columnIndex column index
     * @return the nullable result
     * @throws SQLException sql exception
     * @since 0.0.1
     */
    @Override
    public E getNullableResult(@NotNull CallableStatement cs, int columnIndex) throws SQLException {
        if (null == cs.getObject(columnIndex) && cs.wasNull()) {
            return null;
        }
        return this.valueOf(this.type, cs.getObject(columnIndex));
    }

    /**
     * Value of e
     *
     * @param enumClass enum class
     * @param value     value
     * @return the e
     * @since 0.0.1
     */
    private E valueOf(@NotNull Class<E> enumClass, Object value) {
        E[] es = enumClass.getEnumConstants();
        return Arrays.stream(es).filter((e) -> this.equalsValue(value, this.getCode(e))).findAny().orElse(null);
    }

    /**
     * 值比较
     *
     * @param sourceValue 数据库字段值
     * @param targetValue 当前枚举属性值
     * @return 是否匹配 boolean
     * @since 0.0.1
     */
    private boolean equalsValue(Object sourceValue, Object targetValue) {
        if (sourceValue instanceof Number && targetValue instanceof Number
            && new BigDecimal(String.valueOf(sourceValue)).compareTo(new BigDecimal(String.valueOf(targetValue))) == 0) {
            return true;
        }
        return Objects.equals(sourceValue, targetValue);
    }

    /**
     * Gets code *
     *
     * @param object object
     * @return the code
     * @since 0.0.1
     */
    private Object getCode(Object object) {
        try {
            return this.invoker.invoke(object, new Object[0]);
        } catch (ReflectiveOperationException e) {
            throw ExceptionUtils.mpe(e);
        }
    }
}
1.3 注册枚举类型处理器
@Configuration
public class MybatisConfig {

    /**
     * 使用 MybatisEnumTypeHandler 代替默认的 EnumTypeHandler, 实现 EntityEnum 子类的类型转换(数据库存 value, 返回 Entity)
     *
     * @return the configuration customizer
     * @since 1.0.0
     */
    @Bean
    @ConditionalOnMissingBean(MybatisEnumTypeHandler.class)
    public ConfigurationCustomizer configurationCustomizer() {
        // 通用枚举转换器
        return configuration -> configuration.setDefaultEnumTypeHandler(GeneralEnumTypeHandler.class);
    }
}
1.4 实体类中使用枚举类型

定义性别枚举

@Getter
@RequiredArgsConstructor
public enum GenderEnum implements SerializeEnum<Integer> {

    /** 男 */
    male(0, "男"),
    /** 女 */
    female(1, "女"),
    /** 未知 */
    unknown(2, "未知");

    /** value */
    private final Integer code;
    /** desc */
    private final String value;
}

User类中使用枚举

@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("test_user")
public class User extends BasePO<Long> {
    /** serialVersionUID */
    private static final long serialVersionUID = 1L;

    /** 用户名称 */
    private String userName;
    /** 用户昵称 */
    private String nickName;
    /** 性别 */
    private GenderEnum sex;
    /** 状态 */
    private UserStatusEnum status;
}

2.枚举反序列化和序列化

前端可以传入枚举code值,后端可以直接进行反序列化为枚举对象。返回时枚举对象可以进行序列化为json格式如:{"code": 0,"value": "男"}前端能很好的进行展示而不需要前端再去进行转换,更不需要后端在代码中进行code转换,或者sql中进行转换。

2.1 请求参数枚举值和枚举的映射

前端只需要传实体枚举对应的 code 值, 后端使用实体枚举接收参数即可, 自动通过 code 转换为枚举

@Slf4j
@Data
@SuppressWarnings("unchecked")
@EqualsAndHashCode(callSuper = true)
public class EntityEnumDeserializer<T extends SerializeEnum<?>> extends JsonDeserializer<T> implements ContextualDeserializer {
    /** 当前被处理的枚举类 */
    private Class<T> clz;
    /** 缓存枚举反序列化器 */
    private static final Map<String, EntityEnumDeserializer<? extends SerializeEnum<?>>> DESERIALIZER_MAP = Maps.newConcurrentMap();
    /** 缓存枚举值 */
    private static final Map<String, Map<Serializable, SerializeEnum<?>>> ALL_ENUM_MAP = Maps.newHashMap();

    /**
     * 反序列化优先级: code > 枚举名 > 枚举索引
     *
     * @param jsonParser json parser
     * @param ctx        ctx
     * @return the entity enum
     * @throws IOException io exception
     * @since 0.0.1
     */
    @Override
    public T deserialize(@NotNull JsonParser jsonParser, DeserializationContext ctx) throws IOException {
        if (StringUtils.isBlank(jsonParser.getText())) {
            return null;
        }
        JsonNode node = jsonParser.getCodec().readTree(jsonParser);
        log.trace("执行自定义枚举反序列化: Enum = {}, valueType = {}, code = {}", this.clz, node.getNodeType(), node);

        Serializable finalValue = getValue(node);

        if (finalValue == null) {
            return null;
        }

        SerializeEnum<?> result = ALL_ENUM_MAP.get(this.clz.getName()).get(finalValue);

        if (result == null) {
            log.debug("无法通过 code 找到枚举, 尝试使用枚举名查找: code: {}", finalValue);
            result = SerializeEnum.getEnumByNameOrOrder((Class<? extends Enum<?>>) this.clz, finalValue);
            ALL_ENUM_MAP.get(this.clz.getName()).put(finalValue, result);
        }

        return (T) result;
    }

    /**
     * 通过 {@link SerializeEnum#getCode()} 反序列化枚举.
     *
     * @param node node
     * @return the value
     * @since 0.0.1
     */
    @Contract("null -> fail")
    private static @Nullable Serializable getValue(JsonNode node) {
        // 请求传入的 json 字符串中没有 code 节点, 则直接抛出异常
        if (node == null) {
            log.debug("未找到枚举类型的 code 节点");
            return null;
        }
        JsonNodeType jsonNodeType = node.getNodeType();
        Serializable value;
        switch (jsonNodeType) {
            case OBJECT:
                // 如果是 "{}" 类型, 则直接返回 null
                if (CommonConstant.EMPTY_JSON.equals(node.asText())) {
                    value = null;
                    break;
                }
                // 枚举的 json 类型, 需要递归解析找出 value
                value = getValue(node.get(SerializeEnum.VALUE_FILED_NAME));
                break;
            case NUMBER:
                if (node.isDouble()) {
                    value = node.doubleValue();
                } else if (node.isInt()) {
                    value = node.intValue();
                } else if (node.isFloat()) {
                    value = node.floatValue();
                } else if (node.isLong()) {
                    value = node.longValue();
                } else if (node.isShort()) {
                    value = node.shortValue();
                } else if (node.isBigInteger()) {
                    value = node.bigIntegerValue();
                } else {
                    value = node.decimalValue();
                }
                break;
            case STRING:
                // 枚举名或者 code 为 string 类型
                value = node.asText();
                break;
            case BOOLEAN:
                // code 为 boolean 类型
                value = node.asBoolean();
                break;
            case NULL:
            case ARRAY:
                // 不可能是 pojo, 因为只能是可序列化的值
            case POJO:
                // 不可能是 pojo, 因为只能是可序列化的值
            case BINARY:
                // 不可能是 binary, 因为只能是可序列化的值
            case MISSING:
            default:
                throw new BusinessException(StrFormatter.format("不支持的枚举转换: {}", node.toString()));
        }
        return value;
    }

    /**
     * 获取合适的解析器, 把当前解析的属性 Class 对象存起来, 以便反序列化的转换类型.
     *
     * @param ctx      ctx
     * @param property property
     * @return json deserializer
     * @since 0.0.1
     */
    @Override
    public JsonDeserializer<?> createContextual(@NotNull DeserializationContext ctx, BeanProperty property) {
        Class<T> rawCls = (Class<T>) ctx.getContextualType().getRawClass();
        // 当 key 存在返回当前 value 值, 不存在执行函数并保存到 map 中
        EntityEnumDeserializer<?> entityEnumJsonDeserializer = DESERIALIZER_MAP.get(rawCls.getName());

        if (entityEnumJsonDeserializer == null) {
            EntityEnumDeserializer<T> deserializer = new EntityEnumDeserializer<>();
            DESERIALIZER_MAP.put(rawCls.getName(), deserializer);

            T[] enums = rawCls.getEnumConstants();
            // 使用 getCode 作为 key 缓存当前枚举类的所有枚举
            Map<Serializable, SerializeEnum<?>> currentEnumMap = Maps.newHashMap();
            for (T e : enums) {
                currentEnumMap.put(e.getCode(), e);
            }

            ALL_ENUM_MAP.put(rawCls.getName(), currentEnumMap);

            deserializer.setClz(rawCls);
            DESERIALIZER_MAP.put(rawCls.getName(), deserializer);

            entityEnumJsonDeserializer = deserializer;
        }

        return entityEnumJsonDeserializer;
    }
}
2.2 枚举子类序列化

如实体枚举子类GenderEnum会被序列化为 {"code": 0,"value": "男"} 的形式

@Slf4j
public class EntityEnumSerializer<T extends SerializeEnum<?>> extends JsonSerializer<T> {
    /** 缓存枚举类的 field */
    private static final Map<String, List<Field>> FIELD_MAP = Maps.newConcurrentMap();

    /**
     * 将 {@link SerializeEnum} 实现枚举类型序列化为 json
     *
     * @param entityEnum         entity enum
     * @param jsonGenerator      json generator
     * @param serializerProvider serializer provider
     * @throws IOException io exception
     * @since 0.0.1
     */
    @Override
    @SuppressWarnings("all")
    public void serialize(@NotNull T entityEnum,
                          @NotNull JsonGenerator jsonGenerator,
                          SerializerProvider serializerProvider) throws IOException {
        log.trace("执行自定义枚举序列化: Enum = [{}], code = [{}], value = [{}]",
                  entityEnum.getClass(), entityEnum.getCode(), entityEnum.getValue());

        Class<? extends SerializeEnum<?>> enumClass = (Class<? extends SerializeEnum<?>>) entityEnum.getClass();

        List<Field> fieldList = FIELD_MAP.get(enumClass.getName());
        if (CollectionUtils.isEmpty(fieldList)) {
            Field[] fields = enumClass.getDeclaredFields();
            fieldList = Arrays.stream(fields).filter(f -> !Modifier.isStatic(f.getModifiers())).collect(Collectors.toList());
            FIELD_MAP.put(enumClass.getName(), fieldList);
        }

        if (CollectionUtils.isEmpty(fieldList)) {
            jsonGenerator.writeObject(entityEnum.getValue());
        } else {
            jsonGenerator.writeStartObject();
            for (Field field : fieldList) {
                field.setAccessible(true);
                jsonGenerator.writeFieldName(field.getName());
                try {
                    jsonGenerator.writeObject(field.get(entityEnum));
                } catch (IllegalAccessException e) {
                    throw new IOException(e);
                }
            }
            jsonGenerator.writeEndObject();
        }
    }
}

2.3 注册通用枚举转换器

@Configuration
@Slf4j
public class WebMvcConfig implements WebMvcConfigurer {
    /** GLOBAL_ENUM_CONVERTER_FACTORY */
    private static final ConverterFactory<String, SerializeEnum<?>> GLOBAL_ENUM_CONVERTER_FACTORY = new GlobalEnumConverterFactory();

    /**
     * 注册通用枚举转换器
     *
     * @param registry registry
     * @since 0.0.1
     */
    @Override
    public void addFormatters(@NotNull FormatterRegistry registry) {
        log.debug("注册通用枚举转换器: [{}]", GlobalEnumConverterFactory.class);
        registry.addConverterFactory(GLOBAL_ENUM_CONVERTER_FACTORY);
    }
}
2.3 示例

后端接口:

 @RequiredArgsConstructor
@RequestMapping(value = "/user")
@RestController
public class UserController {

    /** User service */
    private final UserService userService;

    /**
     * 新增用户
     *
     * @param dto dto
     * @since 0.0.1
     */
    @PostMapping
    public Result<?> save(@RequestBody @Validated UserDTO dto) {
        this.userService.save(dto);
        return Result.ok();
    }

    /**
     * 查询用户集合
     *
     * @return the string
     * @since 0.0.1
     */
    @GetMapping
    public Result<?> list() {
        return Result.ok(UserConverter.INSTANCE.vo(this.userService.getList()));
    }

    /**
     * 获取性别枚举列表 *
     *
     * @return the sex list
     * @since 0.0.1
     */
    @GetMapping(value = "/getSexList")
    public Result<?> getSexList() {
        return Result.ok(Arrays.asList(GenderEnum.values()));
    }

    /**
     * 获取用户状态列表 *
     *
     * @return the sex list
     * @since 0.0.1
     */
    @GetMapping(value = "/getUserStatusList")
    public Result<?> getUserStatusList() {
        return Result.ok(Arrays.asList(UserStatusEnum.values()));
    }
}

UserDTO

@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
public class UserDTO extends BaseDTO<Long> {
    /** serialVersionUID */
    private static final long serialVersionUID = 1L;

    /** 用户名称 */
    @NotBlank(message = "用户名称不能为空!")
    private String userName;
    /** 用户昵称 */
    @NotBlank(message = "用户昵称不能为空!")
    private String nickName;
    /** 性别 */
    @NotNull(message = "性别不能为空!")
    private GenderEnum sex;
    /** 状态 */
    @NotNull(message = "状态不能为空!")
    private UserStatusEnum status;
}

1.新增用户请求:

{
    "userName": "管理员",
    "nickName": "admin",
    "sex": 2,
    "status": 0
}

数据库表记录

2.查询用户列表:

{
    "data": [
        {
            "id": 102,
            "createTime": "2024-08-18T09:45:19.000+00:00",
            "userName": "李四",
            "nickName": "lisi",
            "sex": {
                "code": 0,
                "value": "男"
            },
            "status": {
                "code": 1,
                "value": "停用"
            }
        },
        {
            "id": 103,
            "createTime": "2024-08-18T09:46:25.000+00:00",
            "userName": "管理员",
            "nickName": "admin",
            "sex": {
                "code": 2,
                "value": "未知"
            },
            "status": {
                "code": 0,
                "value": "正常"
            }
        }
    ],
    "code": 0,
    "msg": "成功",
    "message": "成功"
}

3.对于页面下拉框,可以查询性别枚举列表:

{
    "data": [
        {
            "code": 0,
            "value": "男"
        },
        {
            "code": 1,
            "value": "女"
        },
        {
            "code": 2,
            "value": "未知"
        }
    ],
    "code": 0,
    "msg": "成功",
    "message": "成功"
}
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论