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.EnumTypeHandler
和org.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字段展示出来。