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 来标识自定义的 枚举 类型。

    同时它还负责 序列化反序列化 枚举类,这是本文的 关键

    /**
     * 自定义枚举类型基础接口
     * 

    * 用于扫描、序列化、反序列化实际枚举类 * * @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());
    

解析 RequestParam


这部分比较简单。

  • 实现一个自定义的 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 本身已经集成了 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 
         * @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;
    }
    

    这里有两种方式:

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

DAO 层处理


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

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

源码


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

结语


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

你可能感兴趣的:(JSON)