前言
定义表结构的时候经常会碰到一类字段:状态 ( status 或者 state ) 、类型 ( type ) ,而通常的做法一般是:
-
数据库 中定义 tinyint 类型。
比如:status tinyint(1) NOT NULL COMMENT '订单状态 1-待支付;2-待发货;3-待收货;4-已收货;5-已完结;'
-
Java 实体类 中定义 Short 类型。( 也见识过用 Byte 类型的,看着怪怪的 )
比如:private Short status
然后项目中可能会充斥着下面这样的代码:
order.setStatus((short) 1);
if (order.getStatus() == 1) {
order.setStatus((short) 2);
}
if (order.getStatus() == 4) {
order.setStatusName("已收货");
}
这都是些什么魔鬼数字啊,没有注释根本没法看,如果手滑可能状态就设错了,而且不好排查是在哪处赋值的。
改进方案是用 常量 ,但是又会产生另一种效果:
public static final Short WAIT_PAY = 1;
if (WAIT_PAY.equals(order.getStatus())) {
// 混用了解下
order.setStatus((short) 2);
}
这时候就该 枚举 出场了,枚举 的本质就是 类 + 常量 ,可以使用 枚举 来定义 一组 相关的元数据 ( 值、描述及其他必要信息 ) ,使用 枚举 类型不仅减小了数据维护 ( 比如调整了值的定义 ) 的成本,还加强了代码的 约束力 。
下文就来介绍如何在项目中 "完美" 使用 枚举 类型。
需要修改的地方
解析 RequestParam 将值转为 枚举 类型。( 只做反序列化 )
解析 RequestBody 将相应字段值转为 枚举 类型,ResponseBody 将 枚举 字段转为 实际的值 。
保存到数据库的时候将 枚举 值转换为 实际的值 ,从数据库读取数据的时候将 实际的值 转为 枚举 值。
主要是这三处地方的改动,其他地方按需调整。
准备工作
-
表结构:
DROP TABLE IF EXISTS `order`; CREATE TABLE `order` ( id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', orderNo varchar(40) NOT NULL COMMENT '订单号', status tinyint(1) NOT NULL COMMENT '订单状态', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
实体类:
@Data public class Order implements Serializable { /** * 主键 */ private Integer id; /** * 订单号 */ private String orderNo; /** * 订单状态 */ private Status status; }
-
枚举类:
@AllArgsConstructor public enum Status implements EnumValue { /** * 已取消 */ CANCEL((short) 0, "已取消"), /** * 待支付 */ WAIT_PAY((short) 1, "待支付"), /** * 待发货 */ WAIT_TRANSFER((short) 2, "待发货"), /** * 待收货 */ WAIT_RECEIPT((short) 3, "待收货"), /** * 已收货 */ RECEIVE((short) 4, "已收货"), /** * 已完结 */ COMPLETE((short) 5, "已完结"); private final Short value; private final String desc; public Short value() { return value; } public String desc() { return desc; } @Override public Object toValue() { return value; } }
-
定义接口 EnumValue 来标识自定义的 枚举 类型。
同时它还负责 序列化 和 反序列化 枚举类,这是本文的 关键 。
/** * 自定义枚举类型基础接口 * <p> * 用于扫描、序列化、反序列化实际枚举类 * * @author anyesu */ public interface EnumValue { /** * 序列化 * * @return 不允许返回 null */ Object toValue(); /** * 反序列化 * * @param enumType 实际枚举类型 * @param value 当前值 * @param <T> 枚举类型并且实现 {@link EnumValue} 接口 * @return 枚举常量 */ static <T extends Enum<T> & EnumValue> T valueOf(Class<T> enumType, Object value) { if (enumType == null || value == null) { return null; } T[] enumConstants = enumType.getEnumConstants(); for (T enumConstant : enumConstants) { Object enumValue = enumConstant.toValue(); if (Objects.equals(enumValue, value) || Objects.equals(enumValue.toString(), value.toString())) { return enumConstant; } } return null; } }
-
用法:
Order order = new Order(); // 设置订单状态 order.setStatus(Status.COMPLETE); // 打印订单状态描述 System.out.println(order.getStatus().desc());
解析 RequestParam
这部分比较简单。
-
实现一个自定义的 Spring Converter 就可以实现 数字或者字符串类型 到 枚举类型 的转换。
public final class StringToEnumConverterFactory implements ConverterFactory<String, EnumValue> { @Override @SuppressWarnings("unchecked") public <T extends EnumValue> Converter<String, T> getConverter(Class<T> targetType) { return new StringToEnum(targetType); } private class StringToEnum<T extends Enum<T> & EnumValue> implements Converter<String, T> { private final Class<T> enumType; StringToEnum(Class<T> enumType) { this.enumType = enumType; } @Override public T convert(String source) { source = source.trim();// 去除首尾空白字符 return source.isEmpty() ? null : EnumValue.valueOf(this.enumType, source); } } }
-
然后在 WebMvcConfigurer 中注册它
@Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(new StringToEnumConverterFactory()); }
Spring 本身已经集成了 StringToEnumConverterFactory 对 Enum 类型进行解析,不要和自己定义的 Converter 搞混了。
-
定义一个 RequestMapping
@RestController public class TestController { @RequestMapping("test") public String test(@RequestParam(required = false) Status status) { return status == null ? "无值" : status.desc(); } }
-
访问看下效果:
# curl http://127.0.0.1:8080/test?status=2 "待发货"
处理 RequestBody 和 ResponseBody
RequestBody 和 ResponseBody 的解析依赖于 HttpMessageConverter。因为我使用 FastJson 作为 序列化框架,所以只需要针对 FastJsonHttpMessageConverter 做配置。
-
实现一个自定义的 序列化/反序列化器 ( 参考 ) :
public class EnumConverter implements ObjectSerializer, ObjectDeserializer { /** * fastjson 序列化 * * @param serializer * @param object * @param fieldName * @param fieldType * @param features */ @Override public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) { serializer.write(((EnumValue) object).toValue()); } @Override public int getFastMatchToken() { return JSONToken.LITERAL_STRING; } /** * fastjson 反序列化 * * @param parser * @param type * @param fieldName * @param <T> * @return */ @Override @SuppressWarnings("unchecked") public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { Class enumType = (Class) type; // 类型校验:枚举类型并且实现 EnumValue 接口 if (!enumType.isEnum() || !EnumValue.class.isAssignableFrom(enumType)) { return null; } final JSONLexer lexer = parser.lexer; final int token = lexer.token(); Object value = null; if (token == JSONToken.LITERAL_INT) { value = lexer.integerValue(); } else if (token == JSONToken.LITERAL_STRING) { value = lexer.stringVal(); } else if (token != JSONToken.NULL) { value = parser.parse(); } return (T) EnumValue.valueOf(enumType, value); } }
-
在 WebMvcConfigurer 中注册 类型转换器 。
@Bean FastJsonHttpMessageConverter fastJsonHttpMessageConverter(FastJsonConfig fastJsonConfig) { FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); converter.setFastJsonConfig(fastJsonConfig); converter.setDefaultCharset(StandardCharsets.UTF_8); converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8)); return converter; } /** * fastjson 配置 * * @param enumValues 自定义枚举类型 {@link MybatisTypeHandlerConfiguration#enumValues()} * @return */ @Bean public FastJsonConfig fastjsonConfig(@Qualifier("enumValues") List<Class<?>> enumValues) { FastJsonConfig config = new FastJsonConfig(); config.setSerializerFeatures(SerializerFeature.WriteDateUseDateFormat); // TODO 这里只是为了测试, 最好都通过扫描来查找而不是硬编码 // enumValues.add(Sex.class); if (enumValues != null && enumValues.size() > 0) { // 枚举类型字段:序列化反序列化配置 EnumConverter enumConverter = new EnumConverter(); ParserConfig parserConfig = config.getParserConfig(); SerializeConfig serializeConfig = config.getSerializeConfig(); for (Class<?> clazz : enumValues) { parserConfig.putDeserializer(clazz, enumConverter); serializeConfig.put(clazz, enumConverter); } } return config; }
这里有两种方式:
- 硬编码给所有 枚举类型 注册 类型转换器 。
- 扫描所有 枚举类型 并批量注册。( 推荐 )
DAO 层处理
由于使用 Mybatis 作为 ORM 框架,这里使用 Mybatis 提供的 TypeHandler 实现 枚举类型 的 序列化 和 反序列化 。
-
实现一个自定义的通用的 TypeHandler
public class EnumTypeHandler<T extends Enum<T> & EnumValue> extends BaseTypeHandler<T> { private final Class<T> type; /** * 只能由子类调用 */ @SuppressWarnings("unchecked") protected EnumTypeHandler() { type = GenericsUtils.getSuperClassGenericClass(getClass()); } /** * 由 Mybatis 根据类型动态生成实例 * * @param type * @see org.apache.ibatis.type.TypeHandlerRegistry#getInstance(Class, Class) */ public EnumTypeHandler(Class<T> rawClass) { this.type = rawClass; } @Override public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { Object value = parameter.toValue(); if (jdbcType == null) { ps.setObject(i, value); } else { ps.setObject(i, value, jdbcType.TYPE_CODE); } } @Override public T getNullableResult(ResultSet rs, String columnName) throws SQLException { return valueOf(rs.getString(columnName)); } @Override public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return valueOf(rs.getString(columnIndex)); } @Override public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return valueOf(cs.getString(columnIndex)); } private T valueOf(String s) { return s == null ? null : EnumValue.valueOf(type, s); } }
-
注册 EnumTypeHandler
@Configuration @ConditionalOnClass({SqlSessionFactory.class}) public class MybatisTypeHandlerConfiguration { private TypeHandlerRegistry typeHandlerRegistry; private final SpringClassScanner springClassScanner; public MybatisTypeHandlerConfiguration(SqlSessionFactory sqlSessionFactory, SpringClassScanner springClassScanner) { this.typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry(); this.springClassScanner = springClassScanner; } /** * 注册 Mybatis 类型转换器 */ @Autowired public void registerTypeHandlers() { enumValues().forEach(this::registerEnumTypeHandler); } /** * 注册 枚举 类型的类型转换器 * * @param javaTypeClass Java 类型 */ private void registerEnumTypeHandler(Class<?> javaTypeClass) { register(javaTypeClass, EnumTypeHandler.class); } /** * 注册类型转换器 * * @param javaTypeClass Java 类型 * @param typeHandlerClass 类型转换器类型 */ private void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) { this.typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } /** * 扫描所有的 {@link EnumValue} 实现类 * 注册到 Spring 中 * * @return 类集合 */ @Bean public List<Class<?>> enumValues() { // 过滤自定义枚举类 Predicate<Class<?>> filter = clazz -> clazz.isEnum() && EnumValue.class.isAssignableFrom(clazz); return springClassScanner.scanClass(ENTITY_PACKAGE, filter); } }
上面是全自动的方式,也可以定义一个具体类型的 EnumTypeHandler :
public class StatusTypeHandler extends EnumTypeHandler<Status> { }
-
然后修改 application.yml 让 Mybatis 去扫描注册自定义的 TypeHandler
mybatis: type-handlers-package: com.github.anyesu.common.typehandler
源码
篇幅有限,上面代码并不完整,点击 这里 查看完整代码。
结语
通过这个小小的优化,对于代码的简洁性和健壮性带来的效果还是不错的。