背景描述

为了表达某一个属性,具备一组可选的范围,我们一般会采用两种方式。枚举类和数据字典,两者具有各自的优点。枚举类写在Java代码中,方便编写相应的判断逻辑,代码可读性高,枚举类中的属性是可提前预估和确定的。数据字典,一般保存在数据库,不便于编写判断和分支逻辑,因为数据如果有所变动,那么对应的代码逻辑很有可能失效,强依赖数据库数据的正确性,数据字典中对应的属性对业务影响并不大,日常开发中常用做分类,打标签使用,属性的多少无法估计。

目前基本上没有一个很好的全局处理枚举类的方案,所以我就自己综合各方面资料写了一个。

代码

架构还在不断完善中,代码不一定可以跑起来,不过关于枚举的配置已经完成,大家可以阅读并参考借鉴:pretty-demo

前言

大多数公司处理枚举的时候,会自定义一个枚举转换工具类,或者在枚举类中编写一个静态方法实现Integer转换枚举的方式。比如:

// 静态方法方式
public enum GenderEnum {

     // 代码略

     public static GenderEnum get(int value) {
         for (GenderEnum item : GenderEnum.values()) {
            if (value == item.getValue()) {
                 return item;
             }
         }
         return null;
     }
}
// 工具类方式
public class EnumUtil {

    public static <E extends Enumerable> E of(@Nonnull Class<E> classType, int value) {
        for (E enumConstant : classType.getEnumConstants()) {
            if (value == enumConstant.getValue()) {
                return enumConstant;
            }
        }
        return null;
    }

}

GenderEnum gender = EnumUtil.of(GenderEnum.class,1);

这种方式很麻烦,或者需要手动编写对应的静态方法,或者需要手动调用工具类进行转换。

解决方案

为了方便起见,我做了一个全局枚举值转换的方案,这个方案可以实现前端通过传递int到服务端,服务端自动转换成枚举类,进行相应的业务判断之后,再以数字的形式存到数据库;我们在查数据的时候,又能将数据库的数字转换成java枚举类,在处理完对应的业务逻辑之后,将枚举和枚举类对应的展示信息一起传递到前台,前台不需要维护这个枚举类和展示信息的对应关系,同时展示信息支持国际化处理,具体的方案如下:

1、基于约定大于配置的原则,制定统一的枚举类的编写规则。大概规则如下:

  • 每个枚举类有两个字段: int value(存数据库),String key(通过key找对应的i18n文本信息)。这块需要细细讨论下,枚举值通常存数据库有存int值,也有存String值,各有利弊。存int的好处就是体积小,如果枚举的值是包含规律的,比如-1是删除,0是预处理,1是处理,2是处理完成,那么我们所有非删除数据,我们可以不使用 status in ( 0,1,2)这种方式,而转换为 status >= 0 ; 存String的话,好处就是可读性高,直接能从数据库的值中明白对应的状态,劣势就是占的体积大点。当然这些都是相对的,存int的时候,我们可以完善好注释,也具备好的可读性。如果int换成String,占的体积多的那一点,其实也可以忽略不计的。
  • 枚举枚举类需要继承统一接口,提供相应的方法供通用处理枚举时候使用。

下面是枚举接口和一个枚举示例:

public interface Enumerable<E extends Enumerable> {

    /**
     * 获取在i18n文件中对应的 key
     * @return key
     */
    @Nonnull
    String getKey();

    /**
     * 获取最终保存到数据库的值
     * @return 值
     */
    @Nonnull
    int getValue();

    /**
     * 获取 key 对应的文本信息
     * @return 文本信息
     */
    @Nonnull
    default String getText() {
        return I18nMessageUtil.getMessage(this.getKey(), null);
    }
}
public enum GenderEnum implements Enumerable {

    /** 男 */
    MALE(1, "male"),

    /** 女 */
    FEMALE(2, "female");

    private int value;

    private String key;

    GenderEnum(int value, String key) {
        this.value = value;
        this.key = key;
    }

    @Override
    public String getKey() {
        return this.key;
    }

    @Override
    public int getValue() {
        return this.value;
    }
}

我们要做的就是,每个我们编写的枚举类,都需要按这样的方式进行编写,按照规范定义的枚举类方便下面统一编写。

2、我们分析下controller层面的数据进和出,从而处理好枚举类和int值的转换,在Spring MVC中,框架帮我们做了数据类型的转换,所以我们以 Spring MVC作为切入点。前台发送到服务端的请求,一般有参数在url中和body中两种方式为主,分别以get请求和post请求配合@RequestBody为代表。

【入参】get方法为代表,请求的MediaType为"application/x-www-form-urlencoded",此时将 int 转换成枚举,我们注册一个新的Converter,如果spring MVC判断到一个值要转换成我们定义的枚举类对象时,调用我们设定的这个转换器

@Configuration
public class MvcConfiguration implements WebMvcConfigurer, WebBindingInitializer {

    /**
     * [get]请求中,将int值转换成枚举类
     * @param registry
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(new EnumConverterFactory());
    }
}

public class EnumConverterFactory implements ConverterFactory<String, Enumerable> {

    private final Map<Class, Converter> converterCache = new WeakHashMap<>();

    @Override
    @SuppressWarnings({"rawtypes", "unchecked"})
    public <T extends Enumerable> Converter<String, T> getConverter(@Nonnull Class<T> targetType) {
        return converterCache.computeIfAbsent(targetType,
                k -> converterCache.put(k, new EnumConverter(k))
        );
    }

    protected class EnumConverter<T extends Enumerable> implements Converter<Integer, T> {

        private final Class<T> enumType;

        public EnumConverter(@Nonnull Class<T> enumType) {
            this.enumType = enumType;
        }

        @Override
        public T convert(@Nonnull Integer value) {
            return EnumUtil.of(this.enumType, value);
        }
    }
}

【入参】post为代表,将 int 转换成枚举。这块我们和前台达成一个约定( Ajax中applicationType),所有在body中的数据必须为json格式。同样后台@RequestBody对应的参数的请求的MediaType为"application/json",spring MVC中对于Json格式的数据,默认使用 Jackson2HttpMessageConverter。在Jackson转换成实体时候,有@JsonCreator和@JsonValue两个注解可以用,但是感觉还是有点麻烦。为了统一处理,我们需要修改Jackson对枚举类的序列化和反序列的支持。配置如下:

@Configuration
@Slf4j
public class JacksonConfiguration {

    /**
     * Jackson的转换器
     * @return
     */
    @Bean
    @Primary
    @SuppressWarnings({"rawtypes", "unchecked"})
    public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() {
        final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = converter.getObjectMapper();
        // Include.NON_EMPTY 属性为 空("") 或者为 NULL 都不序列化,则返回的json是没有这个字段的。这样对移动端会更省流量
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        // 反序列化时候,遇到多余的字段不失败,忽略
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 允许出现特殊字符和转义符
        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
        // 允许出现单引号
        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        SimpleModule customerModule = new SimpleModule();
        customerModule.addDeserializer(String.class, new StringTrimDeserializer(String.class));
        customerModule.addDeserializer(Enumerable.class, new EnumDeserializer(Enumerable.class));
        customerModule.addSerializer(Enumerable.class, new EnumSerializer(Enumerable.class));
        objectMapper.registerModule(customerModule);
        converter.setSupportedMediaTypes(ImmutableList.of(MediaType.TEXT_HTML, MediaType.APPLICATION_JSON));
        return converter;
    }

}
public class EnumDeserializer<E extends Enumerable> extends StdDeserializer<E> {

    private Class<E> enumType;

    public EnumDeserializer(@Nonnull Class<E> enumType) {
        super(enumType);
        this.enumType = enumType;
    }

    @Override
    public E deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        return EnumUtil.of(this.enumType, jsonParser.getIntValue());
    }

}

【出参】当我们查询出结果,要展示给前台的时候,我们会对结果集增加@ResponseBody注解,这时候会调用Jackson的序列化方法,所以我们增加了枚举类的序列配置。如果我们只简单的将枚举转换成 int 给前台,那么前台需要维护这个枚举类的 int 和对应展示信息的关系。所以这块我们将值和展示信息一同返给前台,减轻前台的工作压力。

// 注册枚举类序列化处理类
customerModule.addSerializer(Enumerable.class, new EnumSerializer(Enumerable.class));

public class EnumSerializer extends StdSerializer<Enumerable> {

    public EnumSerializer(@Nonnull Class<Enumerable> type) {
        super(type);
    }

    @Override
    public void serialize(Enumerable enumerable, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeNumberField("value", enumerable.getValue());
        jsonGenerator.writeStringField("text", enumerable.getText());
        jsonGenerator.writeEndObject();
    }
}

这样关于入参和出参的配置都完成了,我们可以保证,所有前台传递到后台的 int 都会自动转换成枚举类。如果返回的数据有枚举类,枚举类也会包含值和展示文本,方便简单。

3、存储层关于枚举类的转换。这里选的 ORM 框架为 Mybatis ,但是你如果翻看官网,官网的资料只提供了两个方案,就是通过枚举隐藏字段name和ordinal的转换,没有一个通用枚举的解决方案。但是通过翻看 github 中的 issuerelease 记录,发现在 3.4.5版本中就提供了对应的自定义枚举处理配置,这块不需要我们做过多的配置,我们直接增加 mybatis-spring-boot-starter 的依赖,直接配置对应的Yaml 文件就实现了功能。

application.yml
--
mybatis:
  configuration:
    default-enum-type-handler: github.shiyajian.pretty.config.enums.EnumTypeHandler
public class EnumTypeHandler<E extends Enumerable> extends BaseTypeHandler<E> {

    private Class<E> enumType;

    public EnumTypeHandler() { /* instance */ }


    public EnumTypeHandler(@Nonnull Class<E> enumType) {
        this.enumType = enumType;
    }

    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {
        preparedStatement.setInt(i, e.getValue());
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int value = rs.getInt(columnName);
        return rs.wasNull() ? null : EnumUtil.of(this.enumType, value);
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int value = rs.getInt(columnIndex);
        return rs.wasNull() ? null : EnumUtil.of(this.enumType, value);
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int value = cs.getInt(columnIndex);
        return cs.wasNull() ? null : EnumUtil.of(this.enumType, value);
    }

}

这样我们就完成了从前台页面到业务代码到数据库的存储,从数据库查询到业务代码再到页面的枚举类转换。整个项目中完全不需要再手动去处理枚举类了。我们的开发流程简单了很多。

结语

一个好的方案并不需要多么高大上的技术,比如各种反射,各种设计模式,只要设计合理,就是简单易用,类似中国古代的榫卯。

ps: 打算3月中旬辞职去杭州,3年多点,有需要招人的可以先交流交流:微信(q408859832),个人吹牛扯淡技术探讨qq群(757696438)

02-12 02:11