Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
anyesu opened this issue Sep 29, 2019 · 0 comments
Open

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

anyesu opened this issue Sep 29, 2019 · 0 comments

Comments

@anyesu
Copy link
Owner

anyesu commented Sep 29, 2019

前言


定义表结构的时候经常会碰到一类字段:状态 ( 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant