枚举转换器使用
枚举的使用场景:
- 有限且固定的取值范围: 当字段的取值范围是有限且稳定的情况下,使用枚举能够清晰地表达这个范围。
- 业务含义明确: 当字段具有明确的业务含义,并且这个含义在系统中是一致的,使用枚举有助于保持一致性。
*枚举优缺点分析*
优点:
- 类型安全: 使用枚举类型可以在编译时保证类型的安全性,减少运行时的类型错误。
- 代码可读性: 将数据库中的数据映射到具体的枚举值,提高代码的可读性和维护性。
- 适用场景清晰: 明确了字段代表的含义,使得在数据库和代码中的字段含义更加一致。缺点:
- 可扩展性局限: 如果数据库中的字段需要新增枚举值,需要修改代码并重新编译,相对不够灵活。
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进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




