枚举处理规范
– 写在前面的话
约定配置确不假, 怎能封堵个性化?
三天两夜增白发, 欲破困局撸源码.
东海飞鱼穿浪花, 西湖静水邻古刹.
春去秋来接冬夏, 梦里还是那个她.
背景
枚举在项目使用中, 高频使用的是其值(value)和业务名称(name), 但在使用过程中命名的字段参差不齐, 易造成混乱.
问题1: 对于枚举的属性字段, 有的用code表示值, 有的用code表示名称…, 能否对枚举进行统一约束?
目前框架处理的约定如下
- 枚举做请求参数
SpringBoot 2.x
默认使用StringToEnumIgnoringCaseConverterFactory处理
- 枚举做响应
- 使用枚举的默认序列化
toString
, 即Enum.name()
- 使用枚举的默认序列化
- 枚举与数据库的映射
mybatis-spring-boot-start 2.x
提供了Enum与数据库值映射的默认处理,
默认是EnumTypeHandler
Enum.name(), 存储枚举类的名称,
可配置为EnumOrdinalTypeHandler
Enum.original(), 存储枚举类的索引
但是, 实际应用中, 枚举带有的业务含义, 往往通过其属性字段反映
- 枚举类的名称
name
用于代码(区别于枚举Enum.name()) - 枚举类的业务中文名称
viewName
往往用于展示 - 枚举类的值
value
存于数据库(区别于Enum.original()),
若使用Enum.original()存于数据库, 那么枚举类定义顺序则一定不能变更, 降低开发友好性
问题2: 如何实现以下规则?
1.数字value
存于数据库
2.字符串name
用于指示业务含义, 可用于前端交互
3.(可选)字符串viewName
则是业务含义的中文表达, 可用于前端交互
如果一并解决问题1,2, 如何实现?
方案
- 定义一个接口IEnum
- 包含getValue()待实现方法, 约束枚举类值的字段为value
- 包含getName()待实现方法, 约束枚举类名称的字段为name
- viewName作为可选项, 增加getViewName()默认方法, 默认返回空串, 子类可复写
- 业务枚举类实现IEnum, 约束业务枚举类获取值和名称的方法(字段)
- 对于请求参数对象中含有枚举字段, 以及响应对象中含有枚举字段, 自定义实现
HttpMessageConverter
消息转换器- 请求枚举转换规则:
value
→name
→viewName
- 响应枚举转换规则:
viewName
→name
→Enum.name()
- 请求枚举转换规则:
- 对于数据库存储枚举值
value
, 自定义实现TypeHandler
转换器,
由于框架本身提供了默认枚举转换器, 自定义实现中对于那些未实现BaseXXX接口的处理做默认处理, 保留一定的兼容性
实现
- 1.枚举约束, 基类接口
- 2.请求响应枚举序列化转换器
- 3.数据库枚举值映射处理器
1.1 枚举基类
/**
* 枚举接口
*/
public interface IEnum {
/**
* 要存入数据库的类型值
*
* @return
*/
Integer getValue();
/**
* 业务或代码上的类型名称(中文|英文)
*
* @return
*/
String getName();
/**
* 类型别名, 可用于前端展示
* 默认为空串
*
* @return
*/
default String getViewName() {
return StringUtils.EMPTY;
}
}
1.2 枚举子类(示例)
/**
* 成功与否状态枚举
*/
public enum SuccessStatusEnum implements IEnum {
SUCCESSFUL(1, "Success"),
FAILED(0, "Failed"),
;
@Getter
private Integer value;
@Getter
private String name;
SuccessStatusEnum(Integer value, String name) {
this.value = value;
this.name = name;
}
}
2. 请求与响应枚举转换处理器
- 2.1 请求消息转换器
- 2.1.1 枚举请求转换器工具类
EnumRequestJSONConverterUtil
/**
* 枚举请求转换器工具类
*
* @author caofanCPU
*/
public class EnumRequestJSONConverterUtil {
@SuppressWarnings({"unchecked"})
public static EnumRequestJSONConverter build() {
return new EnumRequestJSONConverter();
}
}
- 2.1.2 枚举请求反序列化处理器
EnumRequestJSONConverter
/**
* 请求字段对应枚举类型序列化处理器
* 如果发现是自定义IEnum的子类, 走自定义枚举转换: value -> name -> viewName
* 否则, 走默认枚举转换: Enum.original() -> Enum.name(), 参见: StringToEnumIgnoringCaseConverterFactory
*
* @author caofanCPU
*/
@Slf4j
public class EnumRequestJSONConverter<E extends Enum<E>> extends JsonDeserializer<E> implements
ContextualDeserializer {
private Class<E> enumType;
public EnumRequestJSONConverter() {
}
private EnumRequestJSONConverter(Class<E> enumType) {
this.enumType = enumType;
}
@Override
public E deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException {
String source = jsonParser.getText();
if (StringUtils.isBlank(source)) {
log.warn("请求传参枚举字段为空, 则目标枚举类[{}]转换为null", this.enumType.getSimpleName());
return null;
}
return IEnum.class.isAssignableFrom(enumType) ? this.customEnumParse(enumType, source) : this.originEnumParse(enumType, source);
}
@Override
@SuppressWarnings("unchecked")
public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty)
throws JsonMappingException {
Class<?> rawClass = deserializationContext.getContextualType().getRawClass();
return new EnumRequestJSONConverter(rawClass);
}
/**
* 转换顺序: value -> name -> viewName
*
* @param
* @param source
* @return
*/
private E customEnumParse(Class<E> enumType, String source) {
Integer value = null;
E resultEnum = null;
try {
value = Integer.parseInt(source);
} catch (Exception e) {
// do nothing
}
E[] enumConstants = enumType.getEnumConstants();
for (E enumConstant : enumConstants) {
if (!(enumConstant instanceof IEnum)) {
continue;
}
IEnum temp = (IEnum) enumConstant;
if ((Objects.nonNull(value) && value.equals(temp.getValue())
|| source.equals(temp.getName())
|| source.equals(temp.getViewName()))) {
resultEnum = enumConstant;
}
}
if (Objects.isNull(resultEnum)) {
log.error("接口传参枚举转换错误, 原因: 传值[{}], 目标枚举类[{}]", source, this.enumType.getSimpleName());
throw new RuntimeException("参数非法, [" + this.enumType.getSimpleName() + "]不存在枚举值[" + source + "]");
}
return resultEnum;
}
private E originEnumParse(Class<E> enumType, String source) {
if (source.isEmpty()) {
return null;
}
source = source.trim();
try {
return Enum.valueOf(enumType, source);
} catch (Exception ex) {
return findEnum(enumType, source);
}
}
private E findEnum(Class<E> enumType, String source) {
String name = getLettersAndDigits(source);
for (E candidate : EnumSet.allOf(enumType)) {
if (getLettersAndDigits(candidate.name()).equals(name)) {
return candidate;
}
}
throw new IllegalArgumentException("No enum constant "
+ this.enumType.getCanonicalName() + "." + source);
}
private String getLettersAndDigits(String name) {
StringBuilder canonicalName = new StringBuilder(name.length());
name.chars().map((c) -> (char) c).filter(Character::isLetterOrDigit)
.map(Character::toLowerCase).forEach(canonicalName::append);
return canonicalName.toString();
}
}
- 2.2 响应消息转换器
- 2.2.1 枚举响应转换器工具类
EnumResponseJSONConverterUtil
/**
* 枚举响应转换器
*/
public class EnumResponseJSONConverterUtil {
public static JsonSerializer build() {
return new EnumResponseJSONConverter();
}
}
- 2.2.2 枚举响应序列化处理器
EnumResponseJSONConverter
/**
* 响应字段枚举类型序列化处理器
* 如果发现是自定义IEnum的子类, 走自定义枚举转换: viewName -> name -> Enum.name()
* 否则, 取Enum.name()
*
* @author caofanCPU
*/
@Slf4j
public class EnumResponseJSONConverter<E extends Enum<E>> extends JsonSerializer<E> {
@Override
public void serialize(E enumInstance, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) {
if (Objects.isNull(enumInstance)) {
return;
}
String name;
if (enumInstance instanceof IEnum) {
// 自定义
IEnum temp = (IEnum) enumInstance;
name = StringUtils.isNotBlank(temp.getViewName()) ? temp.getViewName() : temp.getName();
if (StringUtils.isBlank(name)) {
name = enumInstance.name();
}
} else {
// 默认
name = enumInstance.name();
}
try {
jsonGenerator.writeString(name);
} catch (IOException ex) {
log.error("枚举响应转换异常, 枚举[{}]原因: {}", enumInstance.name(), ex);
}
}
}
- 2.3 个性化Http请求响应消息转换处理器工具类
MappingJackson2HttpMessageConverterUtil
借助Jackson2ObjectMapperBuilder + JDK8新特性 + 时间处理
/**
* 自定义消息转换器工具类
* 1.过滤null字段
* 2.自定义响应枚举转换 + 请求枚举转换
* 3.时间处理(包含时区): "2019-12-31 13:14:15",
* 项目当前限定只支持LocalXXX, 首推LocalDateTime
* 也可考虑使用Instant
* 4.jackson底层默认UTF8编码
*
* @author caofanCPU
*/
public class MappingJackson2HttpMessageConverterUtil {
@SuppressWarnings("unchecked")
public static MappingJackson2HttpMessageConverter build() {
return new MappingJackson2HttpMessageConverter(
Jackson2ObjectMapperBuilder
.json()
.serializationInclusion(JsonInclude.Include.NON_NULL)
.modules(Lists.newArrayList(
new Jdk8Module(),
new ParameterNamesModule(),
new JavaTimeModule()
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DateUtil.DATETIME_FORMAT_SIMPLE)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DateUtil.DATE_FORMAT_SIMPLE)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DateUtil.TIME_FORMAT_SIMPLE)))
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DateUtil.DATETIME_FORMAT_SIMPLE)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DateUtil.DATE_FORMAT_SIMPLE)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DateUtil.TIME_FORMAT_SIMPLE))),
// 请求枚举类型的反序列化转换器 + 响应枚举类型的序列化转换器
new SimpleModule().addDeserializer(Enum.class, EnumRequestJSONConverterUtil.build())
.addSerializer(Enum.class, EnumResponseJSONConverterUtil.build()),
new JsonComponentModule()
)
)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.featuresToEnable(MapperFeature.PROPAGATE_TRANSIENT_MARKER)
.build()
.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()))
);
}
}
- 2.4 注册Sring的WebApplicationContext
使用@Configuration + 实现WebMvcConfigurer完成, 请留意doc给出的采坑心得
/**
* WEB配置
* 可增加拦截器、异步/跨域支持
* 注意:
* 1.@EnableWebMvc + implements WebMvcConfigurer
* 2.extends WebMvcConfigurationSupport
* 都会覆盖@EnableAutoConfiguration关于WebMvcAutoConfiguration的配置
* 例如: 请求参数时间格式/响应字段为NULL剔除
* 因此, 推荐使用 implements WebMvcConfigurer方式, 保留原有配置
* 3.自定义消息转换器, 推荐直接使用注册Bean
* 也可使用复写extendMessageConverters()方法,
* 但是注意: 不要使用configureMessageConverters, 该方法要么不起作用, 要么关闭了默认配置
*
* @author caofan
*/
@Configuration
@Slf4j
public class WebConfig implements WebMvcConfigurer {
/**
* 剔除响应对象中为NULL的字段
* 请求枚举类转换: value -> name -> viewName
* 响应枚举类转换: viewName -> name
*
*/
@Bean(name = "customerMappingJackson2HttpMessageConverter")
public HttpMessageConverter customerMappingJackson2HttpMessageConverter() {
log.info("枚举请求&&响应转换器初始化完成!");
return MappingJackson2HttpMessageConverterUtil.build();
}
}
3.数据库枚举映射处理器
- 3.1 自定义MyBatis枚举类型转换处理器
/**
* MyBatis枚举类型转换处理器
*
* @author caofanCPU
*/
@Slf4j
public class BaseMybatisEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private final Class<E> type;
private final E[] enums;
public BaseMybatisEnumTypeHandler(Class<E> type) {
if (Objects.isNull(type)) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
this.enums = type.getEnumConstants();
if (Objects.isNull(this.enums)) {
throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, ((IEnum) parameter).getValue());
}
@Override
public E getNullableResult(ResultSet rs, String columnName)
throws SQLException {
int value = rs.getInt(columnName);
return rs.wasNull() ? null : valueOf(value);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
int value = rs.getInt(columnIndex);
return rs.wasNull() ? null : valueOf(value);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
int value = cs.getInt(columnIndex);
return cs.wasNull() ? null : valueOf(value);
}
private E valueOf(int value) {
E resultEnum = null;
for (E enumConstant : enums) {
if (!(enumConstant instanceof IEnum)) {
continue;
}
IEnum temp = (IEnum) enumConstant;
if (value == temp.getValue()) {
resultEnum = enumConstant;
}
}
if (Objects.isNull(resultEnum)) {
log.error("枚举转换异常: 值[{}]无法转换为枚举类[{}]", value, type.getSimpleName());
throw new RuntimeException("Cannot convert " + value + " to " + type.getSimpleName() + " by enum value.");
}
return resultEnum;
}
}
- 3.2 枚举类型自动转换器
AutoDispatchMyBatisEnumTypeHandler
/**
* 枚举类型自动转换器
*/
@SuppressWarnings("unchecked")
@Slf4j
public class AutoDispatchMyBatisEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private BaseTypeHandler typeHandler;
public AutoDispatchMyBatisEnumTypeHandler(Class<E> enumType) {
if (Objects.isNull(enumType)) {
throw new RuntimeException("参数非法, 类型不能为空");
}
if (IEnum.class.isAssignableFrom(enumType)) {
typeHandler = new BaseMybatisEnumTypeHandler(enumType);
log.info("创建枚举类型: [{}]的自定义DB转换器: [{}]", enumType.getSimpleName(), typeHandler.getClass().getSimpleName());
} else {
typeHandler = new EnumOrdinalTypeHandler(enumType);
log.info("创建枚举类型: [{}]的默认DB转换器: [{}]", enumType.getSimpleName(), typeHandler.getClass().getSimpleName());
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
throws SQLException {
typeHandler.setNonNullParameter(ps, i, parameter, jdbcType);
}
@Override
public E getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return (E) typeHandler.getNullableResult(rs, columnName);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
return (E) typeHandler.getNullableResult(rs, columnIndex);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
return (E) typeHandler.getNullableResult(cs, columnIndex);
}
}
- 3.3 注册数据库枚举转换器
- (推荐)启动配置文件
application.yaml
或application.properties
中增加配置
#Mybatis配置(yaml风格)
mybatis:
configuration:
default-enum-type-handler: 文件父目录.AutoDispatchMyBatisEnumTypeHandler
# Mybatis配置(properties风格)
mybatis.configuration.default-enum-type-handler: 文件父目录.AutoDispatchMyBatisEnumTypeHandler
- Bean配置方式
/**
* MyBatis配置
* 推荐使用mybatis.configuration.default-enum-type-handler进行配置
*
* @see <a href=http://www.mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/>
* @author caofanCPU
*/
@Configuration
@Slf4j
@Deprecated
public class MyBatisConfig {
/**
* 方式二: 个性化设置默认枚举转换类
* <p>
* mybatis-spring-boot-start支持配置自定义枚举转换器, 不推荐用此方式
*/
@Bean
@Deprecated
public ConfigurationCustomizer configurationCustomizer() {
log.info("自定义枚举转换器注册成功!");
return configuration -> configuration.setDefaultEnumTypeHandler(AutoDispatchMyBatisEnumTypeHandler.class);
}
/**
* 方式三: 设置sqlSessionFactory属性
* 依赖: @AutoConfigureAfter(MybatisAutoConfiguration.class)
*
* @deprecated 不推荐
*/
// @Resource
private SqlSessionFactory sqlSessionFactory;
/**
* 注册自定义枚举转换器
*/
// @PostConstruct
@Deprecated
public void customMybatisEnumTypeHandler() {
TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
typeHandlerRegistry.setDefaultEnumTypeHandler(AutoDispatchMyBatisEnumTypeHandler.class);
log.info("自定义枚举转换器注册成功!");
}
}
验证
-
Model实体含枚举字段
SuccessEnum status
-
数据库枚举转换器, 注册成功, 以及将枚举
SuccessEnum..SUCCESSFUL
转为数字1(Integer)
-
POSTMan测试结果, 请求字段
status
使用枚举SuccessEnum.SUCCESSFUL#getName()
, 响应字段status
使用枚举SuccessEnum..SUCCESSFUL#getName()
参考信息
总结
微信 |
支付宝 |
MiXin |