如果變量值僅有有限的可選值,那么用枚舉類來定義常量是一個(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ī)制,這其中包括:
- SpringMVC 枚舉轉(zhuǎn)換器
- ORM 枚舉映射
- JSON 序列化和反序列化
自定義 SpringMVC 枚舉轉(zhuǎn)換器
明確需求
以上文的 CourseType 為例,我們希望達(dá)到的效果是:
前端傳參時(shí)給我們枚舉的 index 值,在 controller 中,我們可以直接使用 CourseType 來接收,由框架負(fù)責(zé)完成 index 到 CourseType 的轉(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)我們的需求:
- 實(shí)現(xiàn) ConverterFactory 接口,該接口要求我們返回 Converter,這是一個(gè)典型的工廠設(shè)計(jì)模式
- 實(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);
}
}
怎么生效呢?有兩種方式
- 將 AttributeConverter 注冊(cè)到全局 JPA 容器中,此時(shí)需要與 javax.persistence.Converter 配合使用
- 第二種方式是配合 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è)我們下期揭曉~~