一站式解決使用枚舉的各種痛點(diǎn)

如果變量值僅有有限的可選值,那么用枚舉類來定義常量是一個(gè)很常規(guī)的操作。

但是在業(yè)務(wù)代碼中,我們不希望依賴 ordinary() 進(jìn)行業(yè)務(wù)運(yùn)算,而是自定義數(shù)字屬性,避免枚舉值的增減調(diào)序造成影響。

@Getter
@AllArgsConstructor
public enum CourseType {

    PICTURE(102, "圖文"),
    AUDIO(103, "音頻"),
    VIDEO(104, "視頻"),
    ;

    private final int index;
    private final String name;
}

但也正是因?yàn)槭褂昧俗远x的數(shù)字屬性,很多框架自帶的枚舉轉(zhuǎn)化功能也就不再適用了。因此,我們需要自己來擴(kuò)展相應(yīng)的轉(zhuǎn)化機(jī)制,這其中包括:

  1. SpringMVC 枚舉轉(zhuǎn)換器
  2. ORM 枚舉映射
  3. JSON 序列化和反序列化

自定義 SpringMVC 枚舉轉(zhuǎn)換器

明確需求

以上文的 CourseType 為例,我們希望達(dá)到的效果是:

前端傳參時(shí)給我們枚舉的 index 值,在 controller 中,我們可以直接使用 CourseType 來接收,由框架負(fù)責(zé)完成 indexCourseType 的轉(zhuǎn)換。

@GetMapping("/list")
public void list(@RequestParam CourseType courseType) {
    // do something
}

SpringMVC 自帶枚舉轉(zhuǎn)換器

SpringMVC 自帶了兩個(gè)和枚舉相關(guān)的轉(zhuǎn)換器:

  • org.springframework.core.convert.support.StringToEnumConverterFactory
  • org.springframework.boot.convert.StringToEnumIgnoringCaseConverterFactory

這兩個(gè)轉(zhuǎn)換器是通過調(diào)用枚舉的 valueOf 方法來進(jìn)行轉(zhuǎn)換的,感興趣的同學(xué)可以自行查閱源碼。

實(shí)現(xiàn)自定義枚舉轉(zhuǎn)換器

雖然這兩個(gè)轉(zhuǎn)換器不能滿足我們的需求,但它也給我們帶來了思路,我們可以通過模仿這兩個(gè)轉(zhuǎn)換器來實(shí)現(xiàn)我們的需求:

  1. 實(shí)現(xiàn) ConverterFactory 接口,該接口要求我們返回 Converter,這是一個(gè)典型的工廠設(shè)計(jì)模式
  2. 實(shí)現(xiàn) Converter 接口,完成自定義數(shù)字屬性到枚舉類的轉(zhuǎn)化

廢話不多說,上源碼:

/**
 * springMVC 枚舉類的轉(zhuǎn)換器
 * 如果枚舉類中有工廠方法(靜態(tài)方法)被標(biāo)記為{@link EnumConvertMethod },則調(diào)用該方法轉(zhuǎn)為枚舉對(duì)象
 */
@SuppressWarnings("all")
public class EnumMvcConverterFactory implements ConverterFactory<String, Enum<?>> {

    private final ConcurrentMap<Class<? extends Enum<?>>, EnumMvcConverterHolder> holderMapper = new ConcurrentHashMap<>();


    @Override
    public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
        EnumMvcConverterHolder holder = holderMapper.computeIfAbsent(targetType, EnumMvcConverterHolder::createHolder);
        return (Converter<String, T>) holder.converter;
    }


    @AllArgsConstructor
    static class EnumMvcConverterHolder {
        @Nullable
        final EnumMvcConverter<?> converter;

        static EnumMvcConverterHolder createHolder(Class<?> targetType) {
            List<Method> methodList = MethodUtils.getMethodsListWithAnnotation(targetType, EnumConvertMethod.class, false, true);
            if (CollectionUtils.isEmpty(methodList)) {
                return new EnumMvcConverterHolder(null);
            }
            Assert.isTrue(methodList.size() == 1, "@EnumConvertMethod 只能標(biāo)記在一個(gè)工廠方法(靜態(tài)方法)上");
            Method method = methodList.get(0);
            Assert.isTrue(Modifier.isStatic(method.getModifiers()), "@EnumConvertMethod 只能標(biāo)記在工廠方法(靜態(tài)方法)上");
            return new EnumMvcConverterHolder(new EnumMvcConverter<>(method));
        }

    }

    static class EnumMvcConverter<T extends Enum<T>> implements Converter<String, T> {

        private final Method method;

        public EnumMvcConverter(Method method) {
            this.method = method;
            this.method.setAccessible(true);
        }

        @Override
        public T convert(String source) {
            if (source.isEmpty()) {
                // reset the enum value to null.
                return null;
            }
            try {
                return (T) method.invoke(null, Integer.valueOf(source));
            } catch (Exception e) {
                throw new IllegalArgumentException(e);
            }
        }

    }


}

  • EnumMvcConverterFactory :工廠類,用于創(chuàng)建 EnumMvcConverter

  • EnumMvcConverter:自定義枚舉轉(zhuǎn)換器,完成自定義數(shù)字屬性到枚舉類的轉(zhuǎn)化

  • EnumConvertMethod:自定義注解,在自定義枚舉類的工廠方法上標(biāo)記該注解,用于 EnumMvcConverter 來進(jìn)行枚舉轉(zhuǎn)換

EnumConvertMethod 的具體源碼如下:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnumConvertMethod {
}

怎么使用

1、注冊(cè) EnumMvcConverterFactory

@Configuration
public class MvcConfiguration implements WebMvcConfigurer {

    @Bean
    public EnumMvcConverterFactory enumMvcConverterFactory() {
        return new EnumMvcConverterFactory();
    }

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // org.springframework.core.convert.support.GenericConversionService.ConvertersForPair.add
        // this.converters.addFirst(converter);
        // 所以我們自定義的會(huì)放在前面
        registry.addConverterFactory(enumMvcConverterFactory());
    }
}

2、在自定義枚舉中提供一個(gè)工廠方法,完成自定義數(shù)字屬性到枚舉類的轉(zhuǎn)化,同時(shí)在該工廠方法上添加 @EnumConvertMethod 注解

@Getter
@AllArgsConstructor
public enum CourseType {

    PICTURE(102, "圖文"),
    AUDIO(103, "音頻"),
    VIDEO(104, "視頻"),
    ;

    private final int index;
    private final String name;

    private static final Map<Integer, CourseType> mappings;

    static {
        Map<Integer, CourseType> temp = new HashMap<>();
        for (CourseType courseType : values()) {
            temp.put(courseType.index, courseType);
        }
        mappings = Collections.unmodifiableMap(temp);
    }

    @EnumConvertMethod
    @Nullable
    public static CourseType resolve(int index) {
        return mappings.get(index);
    }
}

自定義 ORM 枚舉映射

遇到什么問題

還是以上述的 CourseType 枚舉為例,一般業(yè)務(wù)代碼的數(shù)據(jù)都要持久化到 DB 中的。假設(shè),現(xiàn)在有一張課程元數(shù)據(jù)表,用于記錄當(dāng)前課程所屬的類型,我們的 entity 對(duì)象可能是這樣的:

@Getter
@Setter
@Entity
@Table(name = "course_meta")
public class CourseMeta {
    private Integer id;

    /**
     * 課程類型,{@link CourseType}
     */
    private Integer type;
}

上述做法是通過 javadoc 注釋的方式來告訴使用方 type 的取值類型是被關(guān)聯(lián)到了 CourseType。

但是,我們希望通過更清晰的代碼來避免注釋,讓代碼不言自明。

因此,能不能讓 ORM 在映射的時(shí)候,直接把 Integer 類型的 type 映射成 CourseType 枚舉呢?答案是可行的。

AttributeConverter

我們當(dāng)前系統(tǒng)使用的是 Spring Data JPA 框架,是對(duì) JPA 的進(jìn)一步封裝。因此,本文只提供在 JPA 環(huán)境下的解決方案。

在 JPA 規(guī)范中,提供了 javax.persistence.AttributeConverter 接口,用于擴(kuò)展對(duì)象屬性和數(shù)據(jù)庫(kù)字段類型的映射。

public class CourseTypeEnumConverter implements AttributeConverter<CourseType, Integer> {

    @Override
    public Integer convertToDatabaseColumn(CourseType attribute) {
        return attribute.getIndex();
    }

    @Override
    public CourseType convertToEntityAttribute(Integer dbData) {
        return CourseType.resolve(dbData);
    }
}

怎么生效呢?有兩種方式

  1. 將 AttributeConverter 注冊(cè)到全局 JPA 容器中,此時(shí)需要與 javax.persistence.Converter 配合使用
  2. 第二種方式是配合 javax.persistence.Convert 使用,在需要的地方指定 AttributeConverter,此時(shí)不會(huì)全局生效

本文選擇的是第二種方式,在需要的地方指定 AttributeConverter,具體代碼如下:

@Getter
@Setter
@Entity
@Table(name = "ourse_meta")
public class CourseMeta {
    private Integer id;

    @Convert(converter = CourseTypeEnumConverter.class)
    private CourseType type;
}

JSON 序列化

到這里,我們已經(jīng)解決了 SpringMVC 和 ORM 對(duì)自定義枚舉的支持,那是不是這樣就足夠了呢?還有什么問題呢?

SpringMVC 的枚舉轉(zhuǎn)化器只能支持 GET 請(qǐng)求的參數(shù)轉(zhuǎn)化,如果前端提交 JSON 格式的 POST 請(qǐng)求,那還是不支持的。

另外,在給前端輸出 VO 時(shí),默認(rèn)情況下,還是要手動(dòng)把枚舉類型映射成 Integer 類型,并不能在 VO 中直接使用枚舉輸出。

@Data
public class CourseMetaShowVO {
    private Integer id;
    private Integer type;

    public static CourseMetaShowVO of(CourseMeta courseMeta) {
        if (courseMeta == null) {
            return null;
        }
        CourseMetaShowVO vo = new CourseMetaShowVO();
        vo.setId(courseMeta.getId());
        // 手動(dòng)轉(zhuǎn)化枚舉
        vo.setType(courseMeta.getType().getIndex());
        return vo;
    }
}

@JsonValue 和 @JsonCreator

Jackson 是一個(gè)非常強(qiáng)大的 JSON 序列化工具,SpringMVC 默認(rèn)也是使用 Jackson 作為其 JSON 轉(zhuǎn)換器。

Jackson 為我們提供了兩個(gè)注解,剛好可以解決這個(gè)問題。

  • @JsonValue: 在序列化時(shí),只序列化 @JsonValue 注解標(biāo)注的值
  • @JsonCreator:在反序列化時(shí),調(diào)用 @JsonCreator 標(biāo)注的構(gòu)造器或者工廠方法來創(chuàng)建對(duì)象

最后的代碼如下:

@Getter
@AllArgsConstructor
public enum CourseType {

    PICTURE(102, "圖文"),
    AUDIO(103, "音頻"),
    VIDEO(104, "視頻"),
    ;

    @JsonValue
    private final int index;
    private final String name;

    private static final Map<Integer, CourseType> mappings;

    static {
        Map<Integer, CourseType> temp = new HashMap<>();
        for (CourseType courseType : values()) {
            temp.put(courseType.index, courseType);
        }
        mappings = Collections.unmodifiableMap(temp);
    }

    @EnumConvertMethod
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    @Nullable
    public static CourseType resolve(int index) {
        return mappings.get(index);
    }
}

擴(kuò)展 swagger 對(duì)枚舉的支持

經(jīng)過上述的一些自定義轉(zhuǎn)換器,基本解決了在代碼中使用枚舉的一些痛點(diǎn)。但是,你以為這就夠了嗎?

現(xiàn)在大部分的代碼都在使用 swagger 來編寫文檔,不知道大家有沒有這樣的痛點(diǎn):

在編寫文檔時(shí),需要告訴前端枚舉類型有哪些取值,每次增加取值之后,不僅要改代碼,還要找到對(duì)應(yīng)的取值在哪里使用了,然后修改 swagger 文檔。

反正小黑我覺得這樣做很不爽,那有沒有什么辦法可以讓 swagger 框架來幫我們自動(dòng)列舉出所有的枚舉數(shù)值呢?辦法當(dāng)然是有的啦!

怎么做呢?emmm... 這個(gè)我們下期揭曉~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容