1. 程式人生 > >如何優雅的在Java應用中實現全局枚舉處理

如何優雅的在Java應用中實現全局枚舉處理

直接 prop ctype release serializa 一起 cal exceptio 我們

背景描述

為了表達某一個屬性,具備一組可選的範圍,我們一般會采用兩種方式。枚舉類和數據字典,兩者具有各自的優點。枚舉類寫在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 中的 issue 和 release 記錄,發現在 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)

如何優雅的在Java應用中實現全局枚舉處理