SpringBoot + Mybatis统一枚举字典常量

  • 1、需求背景
  • 2、Demo前提
  • 3、定义通用的枚举类
  • 4、枚举常量JSON序列化、反序列化
  • 5、枚举常量之Spring类型转换器
  • 6、枚举类型之Mybatis数据表字段与常量互转
  • 7、扩展IEnumEndpoint
  • 8、在Swagger中优化展示


1、需求背景

        在实际项目开发的过程中,会经常需要常量或枚举常量的比较、转换。不同开发水平人员写的代码参差不齐,常出现代码中使用if中使用魔法值做比较判断,有的人使用常量判断,有的人使用枚举判断,造成整体项目代码参差不齐、质量偏低。

        即使使用了枚举,大家编写的风格有可能又是各有各的风格,代码充斥着大量值与枚举互转,值与枚举常量比较等情况,有些缺少注释等各式各样问题。

例如:

// 枚举转换成值
entity.setPayStatus(OrderInfoEnum.PAY_STATUS_0.getValue());
// 值转换成枚举
ProductTypeEnum typeEnum = ProductTypeEnum.select(0);
// 代码中比较
if (OrderLogEnum.EXTEND_ORDER.getValue().equals("status_0") {...do something...}  
// 参数类型缺少注解说明订单类型有哪些,如果换成 private OrderTypeEnum orderType则会非常清晰
@ApiModelProperty("订单类型")
private String orderType;

本文主要讲解如何通过枚举来实现统一的常量应用。

2、Demo前提

以下的代码会在后面讲解中使用到。

ConfirmStatusEnum

@Getter
@IEnumDesc("确认状态")
public enum ConfirmStatusEnum implements IEnum<String> {
    UNCONFIRMED("0", "待确认"),
    CONFIRMED("1", "已确认"),
    CANCELED("2", "已取消");

    @EnumValue
    private final String code;

    private String desc;

    CommissionStatusEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

ResponseDto

public class ResponseDto {
 	//...
        
    @ApiModelProperty("确认状态")    
    private ConfirmStatusEnum status;

    //...
}

RequestParam

public class RequestParam {
 	//...
        
    @ApiModelProperty("确认状态")    
    private ConfirmStatusEnum status;

    //...
}

Entity

public class Entity extends BaseModel<PtSubCommission> {
 	...
        
    @TableField("status")
    private ConfirmStatusEnum status;

    ...
}

3、定义通用的枚举类

主要包含code、desc描述、selectByCode根据值和枚举类型获取对应的枚举常量,指定序列化和反序列化

@JsonSerialize(using = IEnumJsonSerializer.class)
@JsonDeserialize(using = IEnumJsonDeserializer.class)
public interface IEnum<T> {
    T getCode();

    default String getDesc() {
        return null;
    }

    static <T, K extends Enum<K> & IEnum<T>> K selectByCode(T code, Class<K> clazz) {
        return Stream.of(clazz.getEnumConstants())
                .filter(em -> Objects.equals(code, em.getCode()))
                .findFirst()
                .orElse(null);
    }
}

4、枚举常量JSON序列化、反序列化

枚举序列化:IEnumJsonDeserializer

@SuppressWarnings("rawtypes")
public class IEnumJsonSerializer extends JsonSerializer<IEnum> {
    @Override
    public void serialize(IEnum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value != null) {
            String desc = value.getDesc();
            if (desc == null) {
                desc = ((Enum)value).name();
            }
            Map<String, Object> map = new HashMap<>();
            map.put("code", value.getCode());
            map.put("desc", desc);
            gen.writeObject(map);
        }
    }
}

IEnumJsonSerializer将枚举常量序列化为Map对象通过Json序列化返回前端,这样的好处是前端无需关注code值应该翻译成为什么样的中文,仅适用desc展示即可。

{
    //其他省略...
    "status": {
        "code": "2",
        "desc": "已取消"	
    }
}



枚举反序列化:IEnumJsonDeserializer ,当前通过Json传参到后端需要反序列化为枚举常量

@Slf4j
@SuppressWarnings("ALL")
public class IEnumJsonDeserializer extends JsonDeserializer<IEnum> {
    @Override
    public IEnum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        String currentName = jp.currentName();
        Object currentValue = jp.getCurrentValue();
        @SuppressWarnings("rawtypes")
        Class findPropertyType = BeanUtils.findPropertyType(currentName, currentValue.getClass());
        JsonFormat annotation = (JsonFormat) findPropertyType.getAnnotation(JsonFormat.class);
        IEnum<?> valueOf;
        if(annotation == null || annotation.shape() != JsonFormat.Shape.OBJECT) {
            valueOf = (IEnum) valueOf(node.asText(), findPropertyType);
        }else {
            valueOf = (IEnum<?>) valueOf(node.get("code").asText(),findPropertyType);
        }
        return valueOf;
    }

    static <E extends Enum<E> & IEnum> E valueOf(String enumCode, Class<E> clazz) {
        E[] enumConstants = clazz.getEnumConstants();
        for (E enumObj : enumConstants) {
            if (enumObj.name().equalsIgnoreCase(enumCode) || enumObj.getCode().equals(enumCode)) {
                return enumObj;
            }
        }
        log.warn("Invalid enum value:{} of {}", enumCode, clazz.getName());
        return null;

        //return Enum.valueOf(clazz, enumCode);
    }
}

5、枚举常量之Spring类型转换器

当传入的参数非json格式的时候,会走Spring的类型转换器,将参数转换成为枚举对象。

@SuppressWarnings("ALL")
public class IEnumConvertFactory implements ConverterFactory<String, IEnum> {

    @Override
    public <T extends IEnum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToIEum<>(targetType);
    }

    private static class StringToIEum<T extends IEnum> implements Converter<String, T> {
        private Class<T> targetType;

        public StringToIEum(Class<T> targetType) {
            this.targetType = targetType;
        }

        @Override
        public T convert(String source) {
            if (StringUtils.isEmpty(source)) {
                return null;
            }
            for (T enumObj : this.targetType.getEnumConstants()) {
                if (source.equals(String.valueOf(enumObj.getCode())) || source.equalsIgnoreCase(((Enum) enumObj).name())) {
                    return enumObj;
                }
            }
            return null;
        }
    }
}

转换器编写完成后,需要注册,下面有两种方式注册转换器:
1、通过RequestMappingHandlerAdapter注册(推荐)
2、通过继承WebSecurityConfigurerAdapter,然后实现addFormatters(FormatterRegistry registry)

public class SecurityAutoConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    @PostConstruct
    public void addConversionConfig() {
        // 参数转换器,不支持Json格式数据的转换,需要使用@JsonSerialize、@JsonDeserialize实现
        ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) requestMappingHandlerAdapter.getWebBindingInitializer();
        if (initializer != null && initializer.getConversionService() != null) {
            GenericConversionService genericConversionService = (GenericConversionService)initializer.getConversionService();
            genericConversionService.addConverterFactory(new IEnumConvertFactory());
            genericConversionService.addConverter(new DateConverter());
        }
    }

	// 第二种
    // implements WebMvcConfigurer
    //@Override
    //public void addFormatters(FormatterRegistry registry) {
    //    IEnumConvertFactory enumConvertFactory = new IEnumConvertFactory();
    //    registry.addConverterFactory(enumConvertFactory);
    //}
}

6、枚举类型之Mybatis数据表字段与常量互转

MyBatis内置了两个枚举转换器分别是:org.apache.ibatis.type.EnumTypeHandlerorg.apache.ibatis.type.EnumOrdinalTypeHandler
EnumTypeHandler,这是默认的枚举转换器,该转换器将枚举实例转换为实例名称的字符串
EnumOrdinalTypeHandler,顾名思义这个转换器将枚举实例的ordinal属性作为取值
使用它的方式是在MyBatis配置文件中定义:

<typeHandlers>
    <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.example.entity.enums.ComputerState"/>
</typeHandlers>

如果是Mybatis-plus,见之前Entity代码中,我们只需要在code字段上添加@EnumValue,然后配置enum包的扫描路径即可。之后数据库字段和枚举常量就可以自动互相转换,配置参考
application.yml

mybatis-plus:
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: isDel
      logic-delete-value: 1
      logic-not-delete-value: 0
  type-enums-package: com.xiaoyun.xcloud.cxb.enums

7、扩展IEnumEndpoint

其实,枚举常量可以作为参数字典使用,仅需要扫描枚举类存储到Map中,通过Restful api暴露给前端使用,如下:
先创建一个注解类

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IEnumDesc {
    /**
     * 枚举说明
     *
     * @return 说明
     */
    String value();
}

创建一个Endpoint

@Slf4j
@Api(tags = "I:枚举字典终端")
@RequestMapping("/enum")
@RestController
public class IEnumEndpoint {
    private final Map<String, EnumType> enumTypeMap;

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public IEnumEndpoint(String enumPackage) {
        if (StringUtils.isBlank(enumPackage)) {
            enumTypeMap = Collections.emptyMap();
            log.info("Enum package not config, please config with setting xcloud.enum-package");
            return;
        }
        Set<Class<?>> classes = ClassUtil.scanPackageBySuper(enumPackage, IEnum.class);
        enumTypeMap = new HashMap<>(classes.size());
        String removeSuffix = "Enum";
        for (Class<?> aClass : classes) {
            if (aClass.isEnum()) {
                Object[] enumConstants = aClass.getEnumConstants();
                if (enumConstants.length == 0) {
                    continue;
                }
                String simpleName = aClass.getSimpleName();
                String enumName = simpleName.endsWith(removeSuffix) ?
                        simpleName.substring(0, simpleName.indexOf(removeSuffix)) : simpleName;
                String enumDesc = Optional.ofNullable(aClass.getAnnotation(IEnumDesc.class)).map(IEnumDesc::value).orElse(null);
                EnumType enumType = new EnumType();
                enumType.setName(StrUtil.toUnderlineCase(enumName));
                enumType.setDesc(enumDesc);
                enumType.setFullName(aClass.getName());
                enumType.setFields(new ArrayList<>(enumConstants.length));
                EnumType put = enumTypeMap.put(enumType.getName(), enumType);
                if (put != null) {
                    throw new IllegalStateException("Enum name of " + enumType.getFullName() + " is repeated with:" + put.getFullName());
                }
                for (Object enumConstant : enumConstants) {
                    IEnum<?> iEnum = (IEnum<?>)enumConstant;
                    enumType.getFields().add(new EnumField(((Enum<?>)iEnum).name().toLowerCase(), iEnum.getDesc(), iEnum.getCode()));
                }
            }
        }
    }

    @ApiOperation("获取指定字典")
    @AnonymousAccess
    @GetMapping("/{name}")
    public IResponse<EnumType> getOne(@PathVariable("name") String name) {
        return IResponse.succeed(enumTypeMap.get(name));
    }

    @ApiOperation("获取全部字典")
    @AnonymousAccess
    @GetMapping("/all")
    public IResponse<Collection<EnumType>> getAll() {
        return IResponse.succeed(enumTypeMap.values());
    }

    @Data
    public static class EnumType {
        String name;
        String desc;
        @JsonIgnore
        String fullName;
        List<EnumField> fields;
    }

    @Data
    @AllArgsConstructor
    public static class EnumField {
        String name;
        String desc;
        Object code;
    }
}
  • 查询所有字典数组
    接口:[GET] /enum/all
  • 查询指定字典
    接口:[GET] /enum/{name}

结果示例

{
  "code": 200,
  "msg": "成功",
  "payload": [
    {
      "name": "confirm_status",
      "desc": "确认状态",
      "fields": [
        {
          "name": "un_confirmed",
          "desc": "待确认",
          "code": "0"
        },
        {
          "name": "confirmed",
          "desc": "已确认",
          "code": "1"
        },
        {
          "name": "canceled",
          "desc": "已取消",
          "code": "2"
        }
      ]
    }
  ]
}

8、在Swagger中优化展示

方案:通过实现Plguin:ExpandedParameterBuilderPlugin、ModelPropertyBuilderPlugin、ParameterBuilderPlugin等,优化在Swagger文档中的展示方式,将枚举的code、desc字段展示出来。