一站式解决使用枚举的各种痛点
如果變量值僅有有限的可選值,那么用枚舉類來定義常量是一個很常規的操作。
但是在業務代碼中,我們不希望依賴?ordinary()?進行業務運算,而是自定義數字屬性,避免枚舉值的增減調序造成影響。
@Getter @AllArgsConstructor public?enum?CourseType?{PICTURE(102,?"圖文"),AUDIO(103,?"音頻"),VIDEO(104,?"視頻"),;private?final?int?index;private?final?String?name; }但也正是因為使用了自定義的數字屬性,很多框架自帶的枚舉轉化功能也就不再適用了。因此,我們需要自己來擴展相應的轉化機制,這其中包括:
SpringMVC 枚舉轉換器
ORM 枚舉映射
JSON 序列化和反序列化
自定義 SpringMVC 枚舉轉換器
明確需求
以上文的?CourseType?為例,我們希望達到的效果是:
前端傳參時給我們枚舉的?index?值,在 controller 中,我們可以直接使用?CourseType?來接收,由框架負責完成?index?到?CourseType?的轉換。
@GetMapping("/list") public?void?list(@RequestParam?CourseType?courseType)?{//?do?something }SpringMVC 自帶枚舉轉換器
SpringMVC 自帶了兩個和枚舉相關的轉換器:
-
org.springframework.core.convert.support.StringToEnumConverterFactory
-
org.springframework.boot.convert.StringToEnumIgnoringCaseConverterFactory
這兩個轉換器是通過調用枚舉的?valueOf?方法來進行轉換的,感興趣的同學可以自行查閱源碼。
實現自定義枚舉轉換器
雖然這兩個轉換器不能滿足我們的需求,但它也給我們帶來了思路,我們可以通過模仿這兩個轉換器來實現我們的需求:
實現 ConverterFactory 接口,該接口要求我們返回 Converter,這是一個典型的工廠設計模式
實現 Converter 接口,完成自定義數字屬性到枚舉類的轉化
廢話不多說,上源碼:
/***?springMVC?枚舉類的轉換器*?如果枚舉類中有工廠方法(靜態方法)被標記為{@link?EnumConvertMethod?},則調用該方法轉為枚舉對象*/ @SuppressWarnings("all") public?class?EnumMvcConverterFactory?implements?ConverterFactory<String,?Enum<?>>?{private?final?ConcurrentMap<Class<??extends?Enum<?>>,?EnumMvcConverterHolder>?holderMapper?=?new?ConcurrentHashMap<>();@Overridepublic?<T?extends?Enum<?>>?Converter<String,?T>?getConverter(Class<T>?targetType)?{EnumMvcConverterHolder?holder?=?holderMapper.computeIfAbsent(targetType,?EnumMvcConverterHolder::createHolder);return?(Converter<String,?T>)?holder.converter;}@AllArgsConstructorstatic?class?EnumMvcConverterHolder?{@Nullablefinal?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?只能標記在一個工廠方法(靜態方法)上");Method?method?=?methodList.get(0);Assert.isTrue(Modifier.isStatic(method.getModifiers()),?"@EnumConvertMethod?只能標記在工廠方法(靜態方法)上");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);}@Overridepublic?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 :工廠類,用于創建 EnumMvcConverter
-
EnumMvcConverter:自定義枚舉轉換器,完成自定義數字屬性到枚舉類的轉化
-
EnumConvertMethod:自定義注解,在自定義枚舉類的工廠方法上標記該注解,用于 EnumMvcConverter 來進行枚舉轉換
EnumConvertMethod 的具體源碼如下:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public?@interface?EnumConvertMethod?{ }怎么使用
1、注冊 EnumMvcConverterFactory
@Configuration public?class?MvcConfiguration?implements?WebMvcConfigurer?{@Beanpublic?EnumMvcConverterFactory?enumMvcConverterFactory()?{return?new?EnumMvcConverterFactory();}@Overridepublic?void?addFormatters(FormatterRegistry?registry)?{//?org.springframework.core.convert.support.GenericConversionService.ConvertersForPair.add//?this.converters.addFirst(converter);//?所以我們自定義的會放在前面registry.addConverterFactory(enumMvcConverterFactory());} }2、在自定義枚舉中提供一個工廠方法,完成自定義數字屬性到枚舉類的轉化,同時在該工廠方法上添加 @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@Nullablepublic?static?CourseType?resolve(int?index)?{return?mappings.get(index);} }自定義 ORM 枚舉映射
遇到什么問題
還是以上述的 CourseType 枚舉為例,一般業務代碼的數據都要持久化到 DB 中的。假設,現在有一張課程元數據表,用于記錄當前課程所屬的類型,我們的 entity 對象可能是這樣的:
@Getter @Setter @Entity @Table(name?=?"course_meta") public?class?CourseMeta?{private?Integer?id;/***?課程類型,{@link?CourseType}*/private?Integer?type; }上述做法是通過 javadoc 注釋的方式來告訴使用方 type 的取值類型是被關聯到了 CourseType。
但是,我們希望通過更清晰的代碼來避免注釋,讓代碼不言自明。
因此,能不能讓 ORM 在映射的時候,直接把 Integer 類型的 type 映射成 CourseType 枚舉呢?答案是可行的。
AttributeConverter
我們當前系統使用的是 Spring Data JPA 框架,是對 JPA 的進一步封裝。因此,本文只提供在 JPA 環境下的解決方案。
在 JPA 規范中,提供了 javax.persistence.AttributeConverter 接口,用于擴展對象屬性和數據庫字段類型的映射。
public?class?CourseTypeEnumConverter?implements?AttributeConverter<CourseType,?Integer>?{@Overridepublic?Integer?convertToDatabaseColumn(CourseType?attribute)?{return?attribute.getIndex();}@Overridepublic?CourseType?convertToEntityAttribute(Integer?dbData)?{return?CourseType.resolve(dbData);} }怎么生效呢?有兩種方式
將 AttributeConverter 注冊到全局 JPA 容器中,此時需要與 javax.persistence.Converter 配合使用
第二種方式是配合 javax.persistence.Convert 使用,在需要的地方指定 AttributeConverter,此時不會全局生效
本文選擇的是第二種方式,在需要的地方指定 AttributeConverter,具體代碼如下:
@Getter @Setter @Entity @Table(name?=?"ourse_meta") public?class?CourseMeta?{private?Integer?id;@Convert(converter?=?CourseTypeEnumConverter.class)private?CourseType?type; }JSON 序列化
到這里,我們已經解決了 SpringMVC 和 ORM 對自定義枚舉的支持,那是不是這樣就足夠了呢?還有什么問題呢?
SpringMVC 的枚舉轉化器只能支持 GET 請求的參數轉化,如果前端提交 JSON 格式的 POST 請求,那還是不支持的。
另外,在給前端輸出 VO 時,默認情況下,還是要手動把枚舉類型映射成 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());//?手動轉化枚舉vo.setType(courseMeta.getType().getIndex());return?vo;} }@JsonValue 和 @JsonCreator
Jackson 是一個非常強大的 JSON 序列化工具,SpringMVC 默認也是使用 Jackson 作為其 JSON 轉換器。
Jackson 為我們提供了兩個注解,剛好可以解決這個問題。
-
@JsonValue:在序列化時,只序列化 @JsonValue 注解標注的值
-
@JsonCreator:在反序列化時,調用 @JsonCreator 標注的構造器或者工廠方法來創建對象
最后的代碼如下:
@Getter @AllArgsConstructor public?enum?CourseType?{PICTURE(102,?"圖文"),AUDIO(103,?"音頻"),VIDEO(104,?"視頻"),;@JsonValueprivate?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)@Nullablepublic?static?CourseType?resolve(int?index)?{return?mappings.get(index);} }擴展 swagger 對枚舉的支持
經過上述的一些自定義轉換器,基本解決了在代碼中使用枚舉的一些痛點。但是,你以為這就夠了嗎?
現在大部分的代碼都在使用 swagger 來編寫文檔,不知道大家有沒有這樣的痛點:
在編寫文檔時,需要告訴前端枚舉類型有哪些取值,每次增加取值之后,不僅要改代碼,還要找到對應的取值在哪里使用了,然后修改 swagger 文檔。
反正小黑我覺得這樣做很不爽,那有沒有什么辦法可以讓 swagger 框架來幫我們自動列舉出所有的枚舉數值呢?辦法當然是有的啦!
怎么做呢?emmm... 這個我們下期揭曉~~
總結
以上是生活随笔為你收集整理的一站式解决使用枚举的各种痛点的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于序列化的 10 几个问题,你顶得住不
- 下一篇: 美团命名服务的挑战与演进