定义表结构的时候经常会碰到一类字段:状态 ( 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 来标识自定义的 枚举 类型。
同时它还负责 序列化 和 反序列化 枚举类,这是本文的 关键 。
/**
* 自定义枚举类型基础接口
*
* 用于扫描、序列化、反序列化实际枚举类
*
* @author anyesu
*/
public interface EnumValue {
/**
* 序列化
*
* @return 不允许返回 null
*/
Object toValue();
/**
* 反序列化
*
* @param enumType 实际枚举类型
* @param value 当前值
* @param 枚举类型并且实现 {@link EnumValue} 接口
* @return 枚举常量
*/
static & EnumValue> T valueOf(Class 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());
这部分比较简单。
实现一个自定义的 Spring Converter 就可以实现 数字或者字符串类型 到 枚举类型 的转换。
public final class StringToEnumConverterFactory implements ConverterFactory {
@Override
@SuppressWarnings("unchecked")
public Converter getConverter(Class targetType) {
return new StringToEnum(targetType);
}
private class StringToEnum & EnumValue> implements Converter {
private final Class enumType;
StringToEnum(Class 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 的解析依赖于 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
* @return
*/
@Override
@SuppressWarnings("unchecked")
public 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> 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;
}
这里有两种方式:
由于使用 Mybatis 作为 ORM 框架,这里使用 Mybatis 提供的 TypeHandler 实现 枚举类型 的 序列化 和 反序列化 。
实现一个自定义的通用的 TypeHandler
public class EnumTypeHandler & EnumValue> extends BaseTypeHandler {
private final Class type;
/**
* 只能由子类调用
*/
@SuppressWarnings("unchecked")
protected EnumTypeHandler() {
type = GenericsUtils.getSuperClassGenericClass(getClass());
}
/**
* 由 Mybatis 根据类型动态生成实例
*
* @param type
* @see org.apache.ibatis.type.TypeHandlerRegistry#getInstance(Class, Class)
*/
public EnumTypeHandler(Class 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> enumValues() {
// 过滤自定义枚举类
Predicate> filter = clazz -> clazz.isEnum() && EnumValue.class.isAssignableFrom(clazz);
return springClassScanner.scanClass(ENTITY_PACKAGE, filter);
}
}
上面是全自动的方式,也可以定义一个具体类型的 EnumTypeHandler :
public class StatusTypeHandler extends EnumTypeHandler {
}
然后修改 application.yml 让 Mybatis 去扫描注册自定义的 TypeHandler
mybatis:
type-handlers-package: com.github.anyesu.common.typehandler
篇幅有限,上面代码并不完整,点击 这里 查看完整代码。
通过这个小小的优化,对于代码的简洁性和健壮性带来的效果还是不错的。