凡是只給規范,不給封裝和工具的都是耍流氓。
規范是靠不住的,如果想保障落地質量必須對最佳實踐進行封裝!
枚舉僅提供了 name 和 ordrial 兩個特性,而這兩個特性在重構時都會發生變化,為了更好的解決枚舉的副作用,我們通過接口為其添加了新能力:
圖片
image
為此,還定義了兩個接口
在這兩個接口基礎之上,可以構建出通用枚舉 CommonEnum,定義如下:
// 定義統一的枚舉接口public interface CommonEnum extends CodeBasedEnum, SelfDescribedEnum{}
整體結構如下:
圖片
在定義枚舉時便可以直接實現 CommonEnum 這個接口。示例代碼如下:
public enum CommonOrderStatus implements CommonEnum { CREATED(1, "待支付"), TIMEOUT_CANCELLED(2, "超時取消"), MANUAL_CANCELLED(5, "手工取消"), PAID(3, "支付成功"), FINISHED(4, "已完成"); private final int code; private final String description; CommonOrderStatus(int code, String description) { this.code = code; this.description = description; } @Override public String getDescription() { return description; } @Override public int getCode() { return this.code; }}
使用 CommonEnum 最大的好處便是可以進行統一管理,對于統一管理,第一件事便是找到并注冊所有的 CommonEnum 實現。整體架構如下:
圖片
核心處理流程如下:
備注:此處萬萬不可直接使用反射技術,反射會觸發類的自動加載,將對眾多不需要的類進行加載,從而增加 metaspace 的壓力。
在需要 CommonEnum 時,只需注入 CommonEnumRegistry Bean 便可以方便的獲得 CommonEnum 的全部實現。
有了統一的 CommonEnum,便可以對枚舉進行統一管理,由框架自動完成與 Spring MVC 的集成。集成內容包括:
整體架構如下:
圖片
核心就是以 code 作為枚舉的唯一標識,自動完成 code 到枚舉的轉化。
Spring MVC 存在兩種參數轉化擴展:
基于 CommonEnumRegistry 提供的 CommonEnum 信息,對 matches 和 getConvertibleTypes方法進行重寫
根據目標類型獲取所有的 枚舉值,并根據 code 和 name 進行轉化
遍歷 CommonEnumRegistry 提供的所有 CommonEnum,依次進行注冊
從 Json 中讀取信息,根據 code 和 name 轉化為確定的枚舉值
兩種擴展核心實現見:
@Order(1)@Componentpublic class CommonEnumConverter implements ConditionalGenericConverter { @Autowired private CommonEnumRegistry enumRegistry; @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { Class<?> type = targetType.getType(); return enumRegistry.getClassDict().containsKey(type); } @Override public Set<ConvertiblePair> getConvertibleTypes() { return enumRegistry.getClassDict().keySet().stream() .map(cls -> new ConvertiblePair(String.class, cls)) .collect(Collectors.toSet()); } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { String value = (String) source; List<CommonEnum> commonEnums = this.enumRegistry.getClassDict().get(targetType.getType()); return commonEnums.stream() .filter(commonEnum -> commonEnum.match(value)) .findFirst() .orElse(null); }}static class CommonEnumJsonDeserializer extends JsonDeserializer{ private final List<CommonEnum> commonEnums; CommonEnumJsonDeserializer(List<CommonEnum> commonEnums) { this.commonEnums = commonEnums; } @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { String value = jsonParser.readValueAs(String.class); return commonEnums.stream() .filter(commonEnum -> commonEnum.match(value)) .findFirst() .orElse(null); } }
默認情況下,對于枚舉類型在轉換為 Json 時,只會輸出 name,其他信息會出現丟失,對于展示非常不友好,對此,需要對 Json 序列化進行能力增強。
首先,需要定義 CommonEnum 對應的返回對象,具體如下:
@Value@AllArgsConstructor(access = AccessLevel.PRIVATE)@ApiModel(description = "通用枚舉")public class CommonEnumVO { @ApiModelProperty(notes = "Code") private final int code; @ApiModelProperty(notes = "Name") private final String name; @ApiModelProperty(notes = "描述") private final String desc; public static CommonEnumVO from(CommonEnum commonEnum){ if (commonEnum == null){ return null; } return new CommonEnumVO(commonEnum.getCode(), commonEnum.getName(), commonEnum.getDescription()); } public static List<CommonEnumVO> from(List<CommonEnum> commonEnums){ if (CollectionUtils.isEmpty(commonEnums)){ return Collections.emptyList(); } return commonEnums.stream() .filter(Objects::nonNull) .map(CommonEnumVO::from) .filter(Objects::nonNull) .collect(Collectors.toList()); }}
CommonEnumVO 是一個標準的 POJO,只是增加了 Swagger 相關注解。
CommonEnumJsonSerializer 是自定義序列化的核心,會將 CommonEnum 封裝為 CommonEnumVO 并進行寫回,具體如下:
static class CommonEnumJsonSerializer extends JsonSerializer{ @Override public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { CommonEnum commonEnum = (CommonEnum) o; CommonEnumVO commonEnumVO = CommonEnumVO.from(commonEnum); jsonGenerator.writeObject(commonEnumVO); } }
有了 CommonEnum 之后,可以提供統一的枚舉字典接口,避免重復開發,同時在新增枚舉時也無需編碼,系統自動識別并添加到字典中。
在 CommonEnumRegistry 基礎之上實現通用字典接口非常簡單,只需按規范構建 Controller 即可,具體如下:
@Api(tags = "通用字典接口")@RestController@RequestMapping("/enumDict")@Slf4jpublic class EnumDictController { @Autowired private CommonEnumRegistry commonEnumRegistry; @GetMapping("all") public RestResult<Map<String, List<CommonEnumVO>>> allEnums(){ Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict(); Map<String, List<CommonEnumVO>> dictVo = Maps.newHashMapWithExpectedSize(dict.size()); for (Map.Entry<String, List<CommonEnum>> entry : dict.entrySet()){ dictVo.put(entry.getKey(), CommonEnumVO.from(entry.getValue())); } return RestResult.success(dictVo); } @GetMapping("types") public RestResult<List<String>> enumTypes(){ Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict(); return RestResult.success(Lists.newArrayList(dict.keySet())); } @GetMapping("/{type}") public RestResult<List<CommonEnumVO>> dictByType(@PathVariable("type") String type){ Map<String, List<CommonEnum>> dict = this.commonEnumRegistry.getNameDict(); List<CommonEnum> commonEnums = dict.get(type); return RestResult.success(CommonEnumVO.from(commonEnums)); }}
該 Controller 提供如下能力:
存儲層并沒有提供足夠的擴展能力,并不能自動向框架注冊類型轉換器。但,由于邏輯都是想通的,框架提供了公共父類來實現復用。
CommonEnumTypeHandler 實現 MyBatis 的 BaseTypeHandler接口,為 CommonEnum 提供的通用轉化能力,具體如下:
public abstract class CommonEnumTypeHandler<T extends Enum<T> & CommonEnum> extends BaseTypeHandler<T> { private final List<T> commonEnums; protected CommonEnumTypeHandler(T[] commonEnums){ this(Arrays.asList(commonEnums)); } protected CommonEnumTypeHandler(List<T> commonEnums) { this.commonEnums = commonEnums; } @Override public void setNonNullParameter(PreparedStatement preparedStatement, int i, T t, JdbcType jdbcType) throws SQLException { preparedStatement.setInt(i, t.getCode()); } @Override public T getNullableResult(ResultSet resultSet, String columnName) throws SQLException { int code = resultSet.getInt(columnName); return commonEnums.stream() .filter(commonEnum -> commonEnum.match(String.valueOf(code))) .findFirst() .orElse(null); } @Override public T getNullableResult(ResultSet resultSet, int i) throws SQLException { int code = resultSet.getInt(i); return commonEnums.stream() .filter(commonEnum -> commonEnum.match(String.valueOf(code))) .findFirst() .orElse(null); } @Override public T getNullableResult(CallableStatement callableStatement, int i) throws SQLException { int code = callableStatement.getInt(i); return commonEnums.stream() .filter(commonEnum -> commonEnum.match(String.valueOf(code))) .findFirst() .orElse(null); }}
由于邏輯比較簡單,在此不做過多解釋。
CommonEnumAttributeConverter 實現 JPA 的 AttributeConverter 接口,為 CommonEnum 提供的通用轉化能力,具體如下:
public abstract class CommonEnumAttributeConverter<E extends Enum<E> & CommonEnum> implements AttributeConverter<E, Integer> { private final List<E> commonEnums; public CommonEnumAttributeConverter(E[] commonEnums){ this(Arrays.asList(commonEnums)); } public CommonEnumAttributeConverter(List<E> commonEnums) { this.commonEnums = commonEnums; } @Override public Integer convertToDatabaseColumn(E e) { return e.getCode(); } @Override public E convertToEntityAttribute(Integer code) { return (E) commonEnums.stream() .filter(commonEnum -> commonEnum.match(String.valueOf(code))) .findFirst() .orElse(null); }}
在有封裝后,業務代碼將變的非常簡單。
由于是在 lego 項目中進行的封裝,第一步便是引入lego依賴,具體如下:
<dependency> <groupId>com.geekhalo.lego</groupId> <artifactId>lego-starter</artifactId> <version>0.1.23</version></dependency>
為了能自動識別 CommonEnum 的實現,需要指定掃描包,具體如下:
baseEnum: basePackage: com.geekhalo.demo
項目啟動時,CommonEnumRegistry 會自動掃描包下的 CommonEnum 實現,并完成注冊。
最后,需要在啟動類上增加如下配置:
@ComponentScan(value = "com.geekhalo.lego.core.enums")
從而,讓 Spring完成核心組件的加載。
完成以上配置后,Spring MVC 就已經完成集成。新建一個 OrderStatus 枚舉,具體如下:
public enum CommonOrderStatus implements CommonEnum { CREATED(1, "待支付"), TIMEOUT_CANCELLED(2, "超時取消"), MANUAL_CANCELLED(5, "手工取消"), PAID(3, "支付成功"), FINISHED(4, "已完成"); private final int code; private final String description; CommonOrderStatus(int code, String description) { this.code = code; this.description = description; } @Override public String getDescription() { return description; } @Override public int getCode() { return this.code; }}
如下圖所示:
圖片
可見,status:3 系統自動轉換為 PAID,成功完成了 code 到 CommonOrderStatus 的轉換。
圖片
返回結果也不再是簡單的name,而是一個對象,返回字段包括:code、name、desc 等。
通過 swagger 可以看到增加一個字典Controller 如下:
圖片
/enumDict/types 返回已加載的所有字段類型,如下所示:
圖片
系統中只有一個實現類 CommonOrderStatus,新增實現類會自動出現在這里。
/enumDict/all 返回所有字典信息,如下所示:
圖片
一次性返回全部字典信息。
/enumDict/{type} 返回指定字典信息,如下所示:
圖片
指定返回 CommonOrderStatus 字典。
有了可復用的公共父類后,類型轉換器變的非常簡單。
MyBatis 類型轉換器只需繼承自 CommonEnumTypeHandler 即可,具體代碼如下:
@MappedTypes(CommonOrderStatus.class)public class CommonOrderStatusTypeHandler extends CommonEnumTypeHandler<CommonOrderStatus> { public CommonOrderStatusTypeHandler() { super(CommonOrderStatus.values()); }}
當然,別忘了添加 MyBatis 配置:
mybatis: type-handlers-package: com.geekhalo.demo.enums.code.fix
JPA 類型轉化器只需繼承自 CommonEnumAttributeConverter 即可,具體代碼如下:
@Converter(autoApply = true)public class CommonOrderStatusAttributeConverter extends CommonEnumAttributeConverter<CommonOrderStatus> { public CommonOrderStatusAttributeConverter() { super(CommonOrderStatus.values()); }}
如有必要,可以在實體類的屬性上增加 注解,具體如下:
/** * 指定枚舉的轉換器 */@Convert(converter = CommonOrderStatusAttributeConverter.class)private CommonOrderStatus status;
代碼倉庫:https://gitee.com/litao851025/learnFromBug
代碼地址:https://gitee.com/litao851025/learnFromBug/tree/master/src/main/java/com/geekhalo/demo/enums/support
本文鏈接:http://www.tebozhan.com/showinfo-26-54962-0.html我們一起聊聊枚舉規范化
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com