Spring Boot 實體類巧用枚舉類型字段

前言


定義表結(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)集成了 StringToEnumConverterFactoryEnum 類型進行解析,不要和自己定義的 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


RequestBodyResponseBody 的解析依賴于 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;
    }
    

    這里有兩種方式:

    1. 硬編碼給所有 枚舉類型 注冊 類型轉(zhuǎn)換器
    2. 掃描所有 枚舉類型 并批量注冊骡尽。( 推薦 )

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.ymlMybatis 去掃描注冊自定義的 TypeHandler

    mybatis:
      type-handlers-package: com.github.anyesu.common.typehandler
    

源碼


篇幅有限箫踩,上面代碼并不完整,點擊 這里 查看完整代碼谭贪。

結(jié)語


通過這個小小的優(yōu)化境钟,對于代碼的簡潔性和健壯性帶來的效果還是不錯的。


轉(zhuǎn)載請注明出處:http://www.reibang.com/p/34212407037e

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末俭识,一起剝皮案震驚了整個濱河市慨削,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖缚态,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件磁椒,死亡現(xiàn)場離奇詭異,居然都是意外死亡猿规,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門宙橱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姨俩,“玉大人,你說我怎么就攤上這事师郑』房” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵宝冕,是天一觀的道長张遭。 經(jīng)常有香客問我,道長地梨,這世上最難降的妖魔是什么菊卷? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮宝剖,結(jié)果婚禮上洁闰,老公的妹妹穿的比我還像新娘。我一直安慰自己万细,他們只是感情好扑眉,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赖钞,像睡著了一般腰素。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上雪营,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天弓千,我揣著相機與錄音,去河邊找鬼献起。 笑死计呈,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的征唬。 我是一名探鬼主播捌显,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼总寒!你這毒婦竟也來了扶歪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎善镰,沒想到半個月后妹萨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡炫欺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年乎完,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片品洛。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡树姨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出桥状,到底是詐尸還是另有隱情帽揪,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布辅斟,位于F島的核電站转晰,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏士飒。R本人自食惡果不足惜查邢,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酵幕。 院中可真熱鬧侠坎,春花似錦、人聲如沸裙盾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽番官。三九已至庐完,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間徘熔,已是汗流浹背门躯。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留酷师,地道東北人讶凉。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像山孔,于是被迫代替她去往敵國和親懂讯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348