您好,登錄后才能下訂單哦!
背景描述
為了表達(dá)某一個(gè)屬性,具備一組可選的范圍,我們一般會采用兩種方式。枚舉類和數(shù)據(jù)字典,兩者具有各自的優(yōu)點(diǎn)。枚舉類寫在Java代碼中,方便編寫相應(yīng)的判斷邏輯,代碼可讀性高,枚舉類中的屬性是可提前預(yù)估和確定的。數(shù)據(jù)字典,一般保存在數(shù)據(jù)庫,不便于編寫判斷和分支邏輯,因?yàn)閿?shù)據(jù)如果有所變動,那么對應(yīng)的代碼邏輯很有可能失效,強(qiáng)依賴數(shù)據(jù)庫數(shù)據(jù)的正確性,數(shù)據(jù)字典中對應(yīng)的屬性對業(yè)務(wù)影響并不大,日常開發(fā)中常用做分類,打標(biāo)簽使用,屬性的多少無法估計(jì)。
目前基本上沒有一個(gè)很好的全局處理枚舉類的方案,所以我就自己綜合各方面資料寫了一個(gè)。
代碼
架構(gòu)還在不斷完善中,代碼不一定可以跑起來,不過關(guān)于枚舉的配置已經(jīng)完成,大家可以閱讀并參考借鑒:pretty-demo
前言
大多數(shù)公司處理枚舉的時(shí)候,會自定義一個(gè)枚舉轉(zhuǎn)換工具類,或者在枚舉類中編寫一個(gè)靜態(tài)方法實(shí)現(xiàn)Integer轉(zhuǎn)換枚舉的方式。
比如:
// 靜態(tài)方法方式 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);
這種方式很麻煩,或者需要手動編寫對應(yīng)的靜態(tài)方法,或者需要手動調(diào)用工具類進(jìn)行轉(zhuǎn)換。
解決方案
為了方便起見,我做了一個(gè)全局枚舉值轉(zhuǎn)換的方案,這個(gè)方案可以實(shí)現(xiàn)前端通過傳遞int到服務(wù)端,服務(wù)端自動轉(zhuǎn)換成枚舉類,進(jìn)行相應(yīng)的業(yè)務(wù)判斷之后,再以數(shù)字的形式存到數(shù)據(jù)庫;我們在查數(shù)據(jù)的時(shí)候,又能將數(shù)據(jù)庫的數(shù)字轉(zhuǎn)換成java枚舉類,在處理完對應(yīng)的業(yè)務(wù)邏輯之后,將枚舉和枚舉類對應(yīng)的展示信息一起傳遞到前臺,前臺不需要維護(hù)這個(gè)枚舉類和展示信息的對應(yīng)關(guān)系,同時(shí)展示信息支持國際化處理,具體的方案如下:
1、基于約定大于配置的原則,制定統(tǒng)一的枚舉類的編寫規(guī)則。大概規(guī)則如下:
下面是枚舉接口和一個(gè)枚舉示例:
public interface Enumerable<E extends Enumerable> { /** * 獲取在i18n文件中對應(yīng)的 key * @return key */ @Nonnull String getKey(); /** * 獲取最終保存到數(shù)據(jù)庫的值 * @return 值 */ @Nonnull int getValue(); /** * 獲取 key 對應(yīng)的文本信息 * @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; } }
我們要做的就是,每個(gè)我們編寫的枚舉類,都需要按這樣的方式進(jìn)行編寫,按照規(guī)范定義的枚舉類方便下面統(tǒng)一編寫。
2、我們分析下controller層面的數(shù)據(jù)進(jìn)和出,從而處理好枚舉類和int值的轉(zhuǎn)換,在Spring MVC中,框架幫我們做了數(shù)據(jù)類型的轉(zhuǎn)換,所以我們以 Spring MVC作為切入點(diǎn)。前臺發(fā)送到服務(wù)端的請求,一般有參數(shù)在url中和body中兩種方式為主,分別以get請求和post請求配合@RequestBody為代表。
【入?yún)ⅰ縢et方法為代表,請求的MediaType為"application/x-www-form-urlencoded",此時(shí)將 int 轉(zhuǎn)換成枚舉,我們注冊一個(gè)新的Converter,如果spring MVC判斷到一個(gè)值要轉(zhuǎn)換成我們定義的枚舉類對象時(shí),調(diào)用我們設(shè)定的這個(gè)轉(zhuǎn)換器
@Configuration public class MvcConfiguration implements WebMvcConfigurer, WebBindingInitializer { /** * [get]請求中,將int值轉(zhuǎn)換成枚舉類 * @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); } } }
【入?yún)ⅰ縫ost為代表,將 int 轉(zhuǎn)換成枚舉。這塊我們和前臺達(dá)成一個(gè)約定( Ajax中applicationType),所有在body中的數(shù)據(jù)必須為json格式。同樣后臺@RequestBody對應(yīng)的參數(shù)的請求的MediaType為"application/json",spring MVC中對于Json格式的數(shù)據(jù),默認(rèn)使用 Jackson2HttpMessageConverter。在Jackson轉(zhuǎn)換成實(shí)體時(shí)候,有@JsonCreator和@JsonValue兩個(gè)注解可以用,但是感覺還是有點(diǎn)麻煩。為了統(tǒng)一處理,我們需要修改Jackson對枚舉類的序列化和反序列的支持。配置如下:
@Configuration @Slf4j public class JacksonConfiguration { /** * Jackson的轉(zhuǎn)換器 * @return */ @Bean @Primary @SuppressWarnings({"rawtypes", "unchecked"}) public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() { final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = converter.getObjectMapper(); // Include.NON_EMPTY 屬性為 空("") 或者為 NULL 都不序列化,則返回的json是沒有這個(gè)字段的。這樣對移動端會更省流量 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); // 反序列化時(shí)候,遇到多余的字段不失敗,忽略 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 允許出現(xiàn)特殊字符和轉(zhuǎn)義符 objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); // 允許出現(xiàn)單引號 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()); } }
【出參】當(dāng)我們查詢出結(jié)果,要展示給前臺的時(shí)候,我們會對結(jié)果集增加@ResponseBody注解,這時(shí)候會調(diào)用Jackson的序列化方法,所以我們增加了枚舉類的序列配置。如果我們只簡單的將枚舉轉(zhuǎn)換成 int 給前臺,那么前臺需要維護(hù)這個(gè)枚舉類的 int 和對應(yīng)展示信息的關(guān)系。所以這塊我們將值和展示信息一同返給前臺,減輕前臺的工作壓力。
// 注冊枚舉類序列化處理類 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(); } }
這樣關(guān)于入?yún)⒑统鰠⒌呐渲枚纪瓿闪?,我們可以保證,所有前臺傳遞到后臺的 int 都會自動轉(zhuǎn)換成枚舉類。如果返回的數(shù)據(jù)有枚舉類,枚舉類也會包含值和展示文本,方便簡單。
3、存儲層關(guān)于枚舉類的轉(zhuǎn)換。這里選的 ORM 框架為 Mybatis ,但是你如果翻看官網(wǎng),官網(wǎng)的資料只提供了兩個(gè)方案,就是通過枚舉隱藏字段name和ordinal的轉(zhuǎn)換,沒有一個(gè)通用枚舉的解決方案。但是通過翻看 github 中的 issue 和 release 記錄,發(fā)現(xiàn)在 3.4.5版本中就提供了對應(yīng)的自定義枚舉處理配置,這塊不需要我們做過多的配置,我們直接增加 mybatis-spring-boot-starter 的依賴,直接配置對應(yīng)的Yaml 文件就實(shí)現(xiàn)了功能。
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); } }
這樣我們就完成了從前臺頁面到業(yè)務(wù)代碼到數(shù)據(jù)庫的存儲,從數(shù)據(jù)庫查詢到業(yè)務(wù)代碼再到頁面的枚舉類轉(zhuǎn)換。整個(gè)項(xiàng)目中完全不需要再手動去處理枚舉類了。我們的開發(fā)流程簡單了很多。
結(jié)語
一個(gè)好的方案并不需要多么高大上的技術(shù),比如各種反射,各種設(shè)計(jì)模式,只要設(shè)計(jì)合理,就是簡單易用,類似中國古代的榫卯。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。