如何在springboot優雅的使用枚舉


如何在springboot優雅的使用枚舉

從數據庫中讀取枚舉值

使用Mybatis-Plus讀取

借助MyBatis-Plus可以很容易的實現這一點。

首先需要在配置文件中加入type-enums-package指定枚舉的掃描包,MyBatis-Plus將為包內(包含子包)所有枚舉進行適配,可以使用逗號或封號分隔多個包名。

mybatis-plus: type-enums-package: [枚舉包][,|;][枚舉包] 

 

接着在枚舉類中指定數據庫值所對應的屬性。這里可以采用兩種方式。

    • 實現官方提供的IEnum接口,接口中的getValue方法與數據庫值對應的屬性。

      @Getter//實現getValue public enum StatusEnum implements IEnum<Integer> { VALID(1, "有效"), INVALID(0, "無效"); StatusEnum(Integer value, String desc) { this.value = value; this.desc = desc; } //標記數據庫存的值是value private final Integer value; private final String desc; } 

    •  將屬性使用EnumValue注解標記數據庫值對應的屬性。
@Getter//實現getValue public enum StatusEnum { VALID(1, "有效"), INVALID(0, "無效"); StatusEnum(Integer value, String desc) { this.value = value; this.desc = desc; } //標記數據庫存的值是value @EnumValue private final Integer value; private final String desc; } 
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

在類的屬性聲明上直接將字段類型標記為枚舉類型,讀取時將自動轉換數據庫值為枚舉對象。

@Data @Accessors(chain = true) @TableName("test") public class TestDO { private Integer id; private String username; private StatusEnum status; } 
  •  

讀取數據庫中的兩條數據進行測試,可以看到值被成功轉換為了枚舉。

庫數據

測試

MyBatis-Plus的實現

從MyBatis-Plus MybatisSqlSessionFactoryBean中可以找到它是如何實現的。

buildSqlSessionFactory方法中可以看到,在配置了type-enums-package的情況下,
MyBatis-Plus將為該包下滿足處理條件的枚舉注冊MybatisEnumTypeHandler類型轉換處理器

枚舉處理器注冊

在MybatisEnumTypeHandler中將取出實現IEnum接口的枚舉的getValue方法或使用EnumValue標記的字段的getter方法進行數據庫值處理。

使用Mybatis實現

Mybatis提供了default-enum-type-handler配置用於改寫默認的枚舉處理器,這里簡單粗暴的直接替換了默認處理器(MyBatis-Plus是滿足條件的類才注冊為該處理器處理,實際情況也應該如此)。

mybatis: configuration: default-enum-type-handler: com.baomidou.mybatisplus.extension.handlers.MybatisEnumTypeHandler 
  •  

同樣,這樣也能完成枚舉映射。

測試

或者在原有的Mybatis配置下追加類似的處理器注冊操作。

@Configuration public class MybatisConfig implements InitializingBean { private final SqlSessionFactory sqlSessionFactory; public MybatisConfig(SqlSessionFactory sqlSessionFactory) { this.sqlSessionFactory = sqlSessionFactory; } @Override public void afterPropertiesSet() throws Exception { sqlSessionFactory.getConfiguration().getTypeHandlerRegistry().register(StatusEnum.class, MybatisEnumTypeHandler.class); } } 
  •  

將請求值轉換為枚舉對象

雖然成功的將數據庫值讀為枚舉屬性,但如果不做處理,實體上的枚舉類型將會成為累贅使請求值無法轉換,因此需要處理使請求的字符串或數字值能夠轉換為枚舉對象。

普通請求

對於諸如Get請求,Post表單請求的普通請求,可以使用自定義的轉換器工廠進行處理,我們需要繼承ConverterFactory類並指定想要處理的類型。

@Slf4j public class StringToEnumConverterFactory implements ConverterFactory<String, Enum<?>> { private static final Map<Class<?>, Converter<String, ? extends Enum<?>>> CONVERTER_MAP = new ConcurrentHashMap<>(); private static final Map<Class<?>, Method> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>(); @Override @SuppressWarnings("unchecked cast") public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) { // 緩存轉換器 Converter<String, T> converter = (Converter<String, T>) CONVERTER_MAP.get(targetType); if (converter == null) { converter = new StringToEnumConverter<>(targetType); CONVERTER_MAP.put(targetType, converter); } return converter; } static class StringToEnumConverter<T extends Enum<?>> implements Converter<String, T> { private final Map<String, T> enumMap = new ConcurrentHashMap<>(); StringToEnumConverter(Class<T> enumType) { Method method = getMethod(enumType); T[] enums = enumType.getEnumConstants(); // 將值與枚舉對象對應並緩存 for (T e : enums) { try { enumMap.put(method.invoke(e).toString(), e); } catch (IllegalAccessException | InvocationTargetException ex) { log.error("獲取枚舉值錯誤!!! ", ex); } } } @Override public T convert(@NotNull String source) { // 獲取 T t = enumMap.get(source); if (t == null) { throw new IllegalArgumentException("該字符串找不到對應的枚舉對象 字符串:" + source); } return t; } } public static <T> Method getMethod(Class<T> enumType) { Method method; // 找到取值的方法 if (IEnum.class.isAssignableFrom(enumType)) { try { method = enumType.getMethod("getValue"); } catch (NoSuchMethodException e) { throw new IllegalArgumentException(String.format("類:%s 找不到 getValue方法", enumType.getName())); } } else { method = TABLE_METHOD_OF_ENUM_TYPES.computeIfAbsent(enumType, k -> { Field field = dealEnumType(enumType).orElseThrow(() -> new IllegalArgumentException(String.format( "類:%s 找不到 EnumValue注解", enumType.getName()))); return ReflectionKit.getMethod(enumType, field); }); } return method; } private static Optional<Field> dealEnumType(Class<?> clazz) { return clazz.isEnum() ? Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst() : Optional.empty(); } } 
  •  

要使自定義的轉換器工廠生效,需要實現WebMvcConfigurer接口並在addFormatters方法中進行追加。

@Configuration public class ConverterFactoryConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(new StringToEnumConverterFactory()); } } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

轉換器生效,請求中的字符串類型,如果發現需要轉換為枚舉,則會觸發自定義的轉換器,並轉換為對應的枚舉。

@RestController @RequestMapping("/test") public class TestController{ @GetMapping public String getEnum(StatusEnum status) { return status.getDesc(); } @PostMapping("post") public String postEnum(StatusEnum status) { return status.getDesc(); } } 

請求測試

Json請求

Jackson

json請求使用RequestBody注解標記接收,處理器為項目中指定的消息轉換器。在Springboot中默認為Jackson。

Jackson對枚舉的默認行為為按枚舉名或其所在的位置(從0開始計算),例如當傳入0時獲取的是枚舉類中的第一個對象。

這顯然不是我們要的,使用JsonCreator注解可以自定義枚舉創建的方式。

增加枚舉類方法:

@JsonCreator public static StatusEnum getItem(int code){ for(StatusEnum item : values()){ if(item.getValue() == code){ return item; } } return null; } 

 

此時反序列化時將調用此方法創建枚舉對象。

如果需要將轉換的范圍局限在某個實體字段,可以選擇自定義JsonDeserializer。

@Slf4j public class JacksonStatusEnumDeserializer extends JsonDeserializer<StatusEnum> { @Override public StatusEnum deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { String text = jsonParser.getText(); for (StatusEnum value : StatusEnum.values()) { if (Objects.equals(text, value.getValue().toString())) { return value; } } return null; } } 
  •  

然后在實體字段上使用注解注明反序列化器。

@JsonDeserialize(using = JacksonStatusEnumConverter.class) private StatusEnum status; 
  •  

由於在JsonDeserializer中DeserializationContext無法獲取到實際的類信息,這意味着單獨使用JsonDeserializer無法作為枚舉的通用解決方案,我們必須為每一個枚舉類定制一個反序列化處理方案。

要實現通用的解決方案,需要實現ContextualDeserializer輔助獲取轉換時的類信息(createContextual每個枚舉類只會觸發一次,之后都使用該方法返回的反序列化器處理)。

@Slf4j @Setter public class JacksonEnumDeserializer extends JsonDeserializer<Enum<?>> implements ContextualDeserializer { private Class<?> clazz; // ctx.getContextualType() 獲取不到類信息 @Override public Enum<?> deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException, JsonProcessingException { Class<?> enumType = clazz; if (Objects.isNull(enumType) || !enumType.isEnum()) { return null; } String text = jsonParser.getText(); Method method = StringToEnumConverterFactory.getMethod(clazz); Enum<?>[] enumConstants = (Enum<?>[]) enumType.getEnumConstants(); // 將值與枚舉對象對應並緩存 for (Enum<?> e : enumConstants) { try { if (Objects.equals(method.invoke(e).toString(), text)) { return e; } } catch (IllegalAccessException | InvocationTargetException ex) { log.error("獲取枚舉值錯誤!!! ", ex); } } return null; } /** * 為不同的枚舉獲取合適的解析器 * * @param ctx ctx * @param property property */ @Override public JsonDeserializer<Enum<?>> createContextual(DeserializationContext ctx, BeanProperty property) throws JsonMappingException { Class<?> rawCls = ctx.getContextualType().getRawClass(); JacksonEnumDeserializer converter = new JacksonEnumDeserializer(); converter.setClazz(rawCls); return converter; } } 
  •  
  •  

使用此序列化器備注的字段將能夠被正確處理。

如果需要更大范圍的采用此序列化器,將所有的枚舉類型默認都委托給JacksonEnumConverter處理,可以修改默認的HttpMessageConverter。

@Configuration public class JacksonConfig { @Bean public HttpMessageConverter<?> httpMessageConverter(ObjectMapper objectMapper) { SimpleModule simpleModule = new SimpleModule(); simpleModule.addDeserializer(Enum.class, new JacksonEnumConverter()); objectMapper.registerModule(simpleModule); return new MappingJackson2HttpMessageConverter(objectMapper); } } 
  •  

或者在JacksonEnumConverter上使用JsonComponent注解標記。

@JsonComponent public class JacksonEnumDeserializer extends JsonDeserializer<Enum<?>> implements ContextualDeserializer 
  •  

此時所有的枚舉類型都將委托給該反序列化器處理。

FastJson

FastJson對枚舉的默認行為同樣為按枚舉名或其所在的位置(從0開始計算),唯一不同的地方在於它強制要求按位置計算則需要傳入的類型為數字類型(即不使用""包裹)。例如:

{ "fastJsonStatusEnum": 1 } 
  •  

FastJson同樣支持為枚舉指定反序列化方式。

反序列化器需要實現ObjectDeserializer接口。

@Slf4j public class FastJsonEnumDeserializer implements ObjectDeserializer { @Override public <T> T deserialze(DefaultJSONParser parser, Type type, Object o) { final JSONLexer lexer = parser.lexer; Class<?> cls = (Class<?>) type; Object[] enumConstants = cls.getEnumConstants(); Method method = StringToEnumConverterFactory.getMethod(cls); if (!Enum.class.isAssignableFrom(cls)) { return null; } for (Object item : enumConstants) { try { String value = method.invoke(item).toString(); if (Objects.equals(value, lexer.stringVal()) || Objects.equals(Integer.valueOf(value), lexer.intValue())) { return (T)item; } } catch (IllegalAccessException | InvocationTargetException ex) { log.error("獲取枚舉值錯誤!!! ", ex); } } return null; } @Override public int getFastMatchToken() { return JSONToken.LITERAL_INT; } } 
  •  

在枚舉類上增加JSONType即可指定反序列化方式。

@JSONType(deserializer = FastJsonEnumDeserializer.class) 
  •  

反序列化器同樣可以借助JSONField注解使其僅在實體字段生效。

@JSONField(deserializeUsing = FastJsonEnumDeserializer.class)
  •  

通過修改ParserConfig配置可以修改指定類的反序列化器,但由於FastJson獲取序列化器時是直接從deserializers鏈表中直接按類型讀取,並未做根類型的特殊處理,這意味着我們無法通過Enum類的配置覆蓋所有枚舉類,需要自行掃描所有枚舉並加入配置,示例中借助hutool掃描指定包下的類。

<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> 
  •  
@Configuration public class FastJsonConfig { private static final String ENUM_BASE_PKG = "com.maple.enumeration.enums"; @Bean public HttpMessageConverter<?> httpMessageConverter() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); ParserConfig parserConfig = fastConverter.getFastJsonConfig().getParserConfig(); // 此方式不會生效 // parserConfig.putDeserializer(Enum.class, new FastJsonEnumDeserializer()); ClassUtil.scanPackage(ENUM_BASE_PKG).stream().filter(Class::isEnum).forEach(item -> parserConfig.putDeserializer(item, new FastJsonEnumDeserializer())); return fastConverter; } } 
  •  

將枚舉對象序列化為字符串

搞定了反序列化后,需要處理的便是序列化操作了。

Jackson

Jackson支持三個序列化枚舉的SerializationFeature配置。

// 直接根據toString方法的返回值序列化
SerializationFeature.WRITE_ENUMS_USING_TO_STRING
// 寫出枚舉序號
SerializationFeature.WRITE_ENUMS_USING_INDEX
// 寫出枚舉名 默認
SerializationFeature.WRITE_ENUM_KEYS_USING_INDEX
  •  

配置返回值

配置文件配置:

spring: jackson: serialization: WRITE_ENUMS_USING_TO_STRING: true # WRITE_ENUMS_USING_INDEX: true # WRITE_ENUM_KEYS_USING_INDEX: true 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

或者通過Bean配置:

@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder -> builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); } } 
  •  

若配置不滿足需求,還可以使用JsonValue注解標記需要序列化返回的值(同一個類中不允許多個標記,標記字段則取字段實際值,標記方法則取方法返回值)。

@JsonValue private final String desc; 
  •  

使用JsonFormat標記枚舉類可以使枚舉被序列化為對象形式。

@JsonFormat(shape = JsonFormat.Shape.OBJECT) 
  • 1

響應

同樣我們也可以通過繼承JsonSerializer實現一個限定范圍的序列化器,這里實現了序列化為對象形式的敷衍版本(工具類來自hutool)。

// 實體標記 @JsonSerialize(using = JacksonEnumSerializer.class) private StatusEnum status; @Slf4j public class JacksonEnumSerializer extends JsonSerializer<Enum<?>> { @Override public void serialize(Enum<?> value, JsonGenerator gen, SerializerProvider serializers) throws IOException { Method[] methods = ReflectUtil.getMethods(value.getClass(), item -> StrUtil.startWith(item.getName(), "get")); gen.writeStartObject(); for (Method method : methods) { String name = StrUtil.subAfter(method.getName(), "get", false); // 首字母小寫 name = name.substring(0, 1).toLowerCase() + name.substring(1); String invokeStr = Objects.toString(ReflectUtil.invoke(value, method)); // 非數值類型寫入字符串 if (!NumberUtil.isNumber(invokeStr)) { gen.writeStringField(name, invokeStr); continue; } // 是否小數 if (invokeStr.contains(".")) { gen.writeNumberField(name, Double.parseDouble(invokeStr)); continue; } gen.writeNumberField(name, Long.parseLong(invokeStr)); } gen.writeEndObject(); } } 
  •  
  •  

將序列化器全局化:

@JsonComponent // 注冊json序列化組件 public class JacksonEnumSerializer extends JsonSerializer<Enum<?>> // 或者bean配置 @Configuration public class JacksonConfig { @Bean public HttpMessageConverter<?> httpMessageConverter(ObjectMapper objectMapper) { SimpleModule simpleModule = new SimpleModule(); simpleModule.addSerializer(Enum.class, new JacksonEnumSerializer()); objectMapper.registerModule(simpleModule); return new MappingJackson2HttpMessageConverter(objectMapper); } } 
  •  

我們也可以將JsonFormat全局化,但此方式只支持到具體類,因此如果有需要也只能通過包掃描的形式進行進行全局定義。

@Configuration public class JacksonConfig { private static final String ENUM_BASE_PKG = "com.maple.enumeration.enums"; @Bean public HttpMessageConverter<?> httpMessageConverter(ObjectMapper objectMapper) { ClassUtil.scanPackage(ENUM_BASE_PKG).forEach(item -> objectMapper.configOverride(item) .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.OBJECT))); return new MappingJackson2HttpMessageConverter(objectMapper); } } 
  •  

附:各種場景的序列化優先級:

實體字段上的JsonSerialize配置 > 枚舉上的JsonSerialize配置 > 全局JsonSerializer注冊 > 枚舉上的JsonValue配置 > 實體字段的JsonFormat配置> 全局的configOverride配置覆蓋 > 枚舉上的JsonFormat配置

FastJson

Jackson支持兩個序列化枚舉的SerializationFeature配置。

// 直接根據toString方法的返回值序列化
SerializerFeature.WriteEnumUsingToString
// 默認 根據枚舉名序列化
SerializerFeature.WriteEnumUsingName
  •  

bean配置:

@Bean public HttpMessageConverter<?> httpMessageConverter() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); fastConverter.getFastJsonConfig().setSerializerFeatures(SerializerFeature.WriteEnumUsingToString); return fastConverter; } 
  •  

實現一個類似的序列化為對象的序列化器(還是敷衍版)。

@Slf4j public class FastJsonEnumSerializer implements ObjectSerializer { @Override public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException { Method[] methods = ReflectUtil.getMethods(object.getClass(), item -> StrUtil.startWith(item.getName(), "get")); Map<String, Object> objectMap = new HashMap<>(methods.length); for (Method method : methods) { String name = StrUtil.subAfter(method.getName(), "get", false); // 首字母小寫 name = name.substring(0, 1).toLowerCase() + name.substring(1); objectMap.put(name, ReflectUtil.invoke(object, method)); } serializer.write(objectMap); } } 
  •  

可以在類上使用JSONType注解標記或在類字段用JSONField標記。

@JSONField(serializeUsing = FastJsonEnumSerializer.class) private FastJsonStatusEnum fastJsonStatusEnum; @JSONType(serializer = FastJsonEnumSerializer.class) public enum FastJsonStatusEnum 
  •  

配置為全局生效同樣需要自行掃描添加。

@Bean public HttpMessageConverter<?> httpMessageConverter() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); ClassUtil.scanPackage(ENUM_BASE_PKG).stream().filter(Class::isEnum).forEach(item -> fastConverter.getFastJsonConfig().getSerializeConfig().put(FastJsonStatusEnum.class, new FastJsonEnumSerializer())); return fastConverter; } 
  •  

附:各種場景的序列化優先級:

實體字段上的JSONField配置 > 全局SerializeConfig配置 > 枚舉上的JSONType配置


以上為本文全部內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM