Spring Boot 实体类巧用枚举类型字段

前言


定义表结构的时候经常会碰到一类字段:状态 ( 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 本身已经集成了 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
    "待发货"
    

处理 RequestBody 和 ResponseBody


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

    这里有两种方式:

    1. 硬编码给所有 枚举类型 注册 类型转换器
    2. 扫描所有 枚举类型 并批量注册。( 推荐 )

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.ymlMybatis 去扫描注册自定义的 TypeHandler

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

源码


篇幅有限,上面代码并不完整,点击 这里 查看完整代码。

结语


通过这个小小的优化,对于代码的简洁性和健壮性带来的效果还是不错的。


转载请注明出处:https://www.jianshu.com/p/34212407037e

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260