前言
定義表結(jié)構(gòu)的時候經(jīng)常會碰到一類字段:狀態(tài) ( status 或者 state ) 俯萎、類型 ( type ) 凉袱,而通常的做法一般是:
-
數(shù)據(jù)庫 中定義 tinyint 類型芥吟。
比如:status tinyint(1) NOT NULL COMMENT '訂單狀態(tài) 1-待支付;2-待發(fā)貨;3-待收貨;4-已收貨;5-已完結(jié);'
-
Java 實體類 中定義 Short 類型。( 也見識過用 Byte 類型的专甩,看著怪怪的 )
比如:private Short status
然后項目中可能會充斥著下面這樣的代碼:
order.setStatus((short) 1);
if (order.getStatus() == 1) {
order.setStatus((short) 2);
}
if (order.getStatus() == 4) {
order.setStatusName("已收貨");
}
這都是些什么魔鬼數(shù)字啊钟鸵,沒有注釋根本沒法看,如果手滑可能狀態(tài)就設錯了涤躲,而且不好排查是在哪處賦值的棺耍。
改進方案是用 常量 ,但是又會產(chǎn)生另一種效果:
public static final Short WAIT_PAY = 1;
if (WAIT_PAY.equals(order.getStatus())) {
// 混用了解下
order.setStatus((short) 2);
}
這時候就該 枚舉 出場了种樱,枚舉 的本質(zhì)就是 類 + 常量 蒙袍,可以使用 枚舉 來定義 一組 相關(guān)的元數(shù)據(jù) ( 值俊卤、描述及其他必要信息 ) ,使用 枚舉 類型不僅減小了數(shù)據(jù)維護 ( 比如調(diào)整了值的定義 ) 的成本害幅,還加強了代碼的 約束力 消恍。
下文就來介紹如何在項目中 "完美" 使用 枚舉 類型。
需要修改的地方
解析 RequestParam 將值轉(zhuǎn)為 枚舉 類型以现。( 只做反序列化 )
解析 RequestBody 將相應字段值轉(zhuǎn)為 枚舉 類型狠怨,ResponseBody 將 枚舉 字段轉(zhuǎn)為 實際的值 。
保存到數(shù)據(jù)庫的時候?qū)?枚舉 值轉(zhuǎn)換為 實際的值 邑遏,從數(shù)據(jù)庫讀取數(shù)據(jù)的時候?qū)?實際的值 轉(zhuǎn)為 枚舉 值佣赖。
主要是這三處地方的改動,其他地方按需調(diào)整记盒。
準備工作
-
表結(jié)構(gòu):
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 '訂單狀態(tài)', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-
實體類:
@Data public class Order implements Serializable { /** * 主鍵 */ private Integer id; /** * 訂單號 */ private String orderNo; /** * 訂單狀態(tài) */ private Status status; }
-
枚舉類:
@AllArgsConstructor public enum Status implements EnumValue { /** * 已取消 */ CANCEL((short) 0, "已取消"), /** * 待支付 */ WAIT_PAY((short) 1, "待支付"), /** * 待發(fā)貨 */ WAIT_TRANSFER((short) 2, "待發(fā)貨"), /** * 待收貨 */ WAIT_RECEIPT((short) 3, "待收貨"), /** * 已收貨 */ RECEIVE((short) 4, "已收貨"), /** * 已完結(jié) */ COMPLETE((short) 5, "已完結(jié)"); 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 來標識自定義的 枚舉 類型茵汰。
同時它還負責 序列化 和 反序列化 枚舉類,這是本文的 關(guān)鍵 孽鸡。
/** * 自定義枚舉類型基礎(chǔ)接口 * <p> * 用于掃描蹂午、序列化、反序列化實際枚舉類 * * @author anyesu */ public interface EnumValue { /** * 序列化 * * @return 不允許返回 null */ Object toValue(); /** * 反序列化 * * @param enumType 實際枚舉類型 * @param value 當前值 * @param <T> 枚舉類型并且實現(xiàn) {@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(); // 設置訂單狀態(tài) order.setStatus(Status.COMPLETE); // 打印訂單狀態(tài)描述 System.out.println(order.getStatus().desc());
解析 RequestParam
這部分比較簡單彬碱。
-
實現(xiàn)一個自定義的 Spring Converter 就可以實現(xiàn) 數(shù)字或者字符串類型 到 枚舉類型 的轉(zhuǎn)換豆胸。
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 本身已經(jīng)集成了 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 "待發(fā)貨"
處理 RequestBody 和 ResponseBody
RequestBody 和 ResponseBody 的解析依賴于 HttpMessageConverter晚胡。因為我使用 FastJson 作為 序列化框架,所以只需要針對 FastJsonHttpMessageConverter 做配置嚼沿。
-
實現(xiàn)一個自定義的 序列化/反序列化器 ( 參考 ) :
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; // 類型校驗:枚舉類型并且實現(xiàn) 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 中注冊 類型轉(zhuǎn)換器 估盘。
@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; }
這里有兩種方式:
- 硬編碼給所有 枚舉類型 注冊 類型轉(zhuǎn)換器 。
- 掃描所有 枚舉類型 并批量注冊骡尽。( 推薦 )
DAO 層處理
由于使用 Mybatis 作為 ORM 框架遣妥,這里使用 Mybatis 提供的 TypeHandler 實現(xiàn) 枚舉類型 的 序列化 和 反序列化 。
-
實現(xiàn)一個自定義的通用的 TypeHandler
public class EnumTypeHandler<T extends Enum<T> & EnumValue> extends BaseTypeHandler<T> { private final Class<T> type; /** * 只能由子類調(diào)用 */ @SuppressWarnings("unchecked") protected EnumTypeHandler() { type = GenericsUtils.getSuperClassGenericClass(getClass()); } /** * 由 Mybatis 根據(jù)類型動態(tài)生成實例 * * @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 類型轉(zhuǎn)換器 */ @Autowired public void registerTypeHandlers() { enumValues().forEach(this::registerEnumTypeHandler); } /** * 注冊 枚舉 類型的類型轉(zhuǎn)換器 * * @param javaTypeClass Java 類型 */ private void registerEnumTypeHandler(Class<?> javaTypeClass) { register(javaTypeClass, EnumTypeHandler.class); } /** * 注冊類型轉(zhuǎn)換器 * * @param javaTypeClass Java 類型 * @param typeHandlerClass 類型轉(zhuǎn)換器類型 */ private void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) { this.typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); } /** * 掃描所有的 {@link EnumValue} 實現(xiàn)類 * 注冊到 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
源碼
篇幅有限箫踩,上面代碼并不完整,點擊 這里 查看完整代碼谭贪。
結(jié)語
通過這個小小的優(yōu)化境钟,對于代碼的簡潔性和健壯性帶來的效果還是不錯的。