SpringBoot 项目统一枚举转换实践
1 现有问题
目前的项目中,有些枚举字段,在传递的时候,需要经常对枚举进行对应的转换,有如下场景:
- 存储进数据库的时候,需要存储为 int;
- 查询出来的时候,需要对该数值进行转换;
- 接收前端参数的时候,需要将数字转换为我们系统的枚举;
- 响应的参数包含枚举的时候,需要将枚举转换成 int;
- 发送或接收 MQ 消息时,又得对枚举进行转换。
可以看到,我们在系统中需要做大量的枚举转换工作,那么是不是有什么方法对枚举转换进行简化呢?
2 数据库转换枚举
我们这边的系统使用的持久层框架是 Spring Data jpa,底层实现是 Hibernate,对于数据库枚举的转化提供了 AttributeConverter 接口,可以实现枚举和 int 自动转换。
例如我们有一个枚举类
@Getter
@RequiredArgsConstructor
public enum SexEnum {
/**
* 1 男
*/
MALE(1, "男"),
/**
* 2 女
*/
FEMALE(2, "女"),
;
private final Integer value;
}
那么,我们就可以实现 AttributeConverter 接口,来实现枚举的自动转换。
@Converter(autoApply = true)
public class SexEnumConverter implements AttributeConverter<SexEnum, Integer> {
@Override
public Integer convertToDatabaseColumn(SexEnum attribute) {
return attribute == null ? null : attribute.getValue();
}
@Override
public SexEnum convertToEntityAttribute(Integer dbData) {
return Arrays.stream(SexEnum.class)
.filter(sexEnum -> sexEnum.getValue().equals(dbData))
.findFirst()
.orElse(null);
}
}
但是这样子每一个枚举类又必须写对应的枚举转换的代码,很麻烦,因此,接下来进行优化。
我们可以先定义一个父类的枚举接口
public interface IEnum {
Integer getValue();
}
然后优化我们的枚举转换类
public class EnumConverter<E extends IEnum> implements AttributeConverter<E, Integer> {
private final Class<E> clazz;
@SuppressWarnings("unchecked")
public EnumConverter() {
this.clazz = (Class<E>) (((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments())[0];
}
@Override
public Integer convertToDatabaseColumn(E attribute) {
return Optional.ofNullable(attribute)
.map(IEnum::getValue)
.orElse(null);
}
@Override
public E convertToEntityAttribute(Integer dbData) {
return Arrays.stream(clazz.getEnumConstants())
.filter(iEnum -> iEnum.getValue().equals(dbData))
.findFirst()
.orElse(null);
}
}
然后枚举类继承我们的父类枚举接口,再定义一个子类实现,就可以实现枚举的自动转换
@Getter
@RequiredArgsConstructor
public enum SexEnum implements IEnum {
/**
* 1 男
*/
MALE(1, "男"),
/**
* 2 女
*/
FEMALE(2, "女"),
;
private final Integer value;
@Converter(autoApply = true)
public static class SexEnumConverter extends EnumConverter<SexEnum> {
}
}
这样子的话,对于数据库的查询或者保存,就可以直接使用枚举进行操作,而不用再去手动转换。
3 请求参数转换枚举
3.1 @Param 参数转换成枚举
对于请求参数,我们分为两种,先看第一种,携带在 URL 后面的参数。
spring 提供了一个 ConverterFactory 接口,我们通过实现他,便可以实现对枚举的自动转换,这里同样要使用到我们的父类接口 IEnum。
@Component
public class IEnumConverterFactory implements ConverterFactory<String, IEnum> {
@Override
public <T extends IEnum> Converter<String, T> getConverter(Class<T> targetType) {
return source ->
Arrays.stream(targetType.getEnumConstants())
.filter(x -> x.getValue().toString().equals(source))
.findFirst()
.orElse(null);
}
}
然后实现 WebMvcConfigurer,添加该类型转换器
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final IEnumConverterFactory iEnumConverterFactory;
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(iEnumConverterFactory);
}
}
然后我们便可以在在方法上使用枚举,MVC 框架会自动帮我们把数值转成对应的枚举。
3.2 @RequestBody 参数转换成枚举
使用 @RequestBody 接收请求参数,底层其实是使用了 Jackson 的反序列化功能。
我们可以继承 Jackson 的 JsonDeserializer 抽象类,自定义我们的反序列化策略。
public class IEnumDeserializer extends JsonDeserializer<IEnum> implements ContextualDeserializer {
private Class<? extends IEnum> clazz;
public IEnumDeserializer() {
}
public IEnumDeserializer(Class<? extends IEnum> clazz) {
this.clazz = clazz;
}
@SneakyThrows
@Override
public IEnum deserialize(JsonParser jsonParser, DeserializationContext ctxt) {
final String param = jsonParser.getText();
return Arrays.stream(clazz.getEnumConstants())
.filter(x -> x.getValue().toString().equals(param))
.findFirst()
.orElse(null);
}
@SuppressWarnings({"unchecked"})
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
JavaType type = property.getType();
// 如果是容器,则返回容器内部枚举类型
while (type.isContainerType()) {
type = type.getContentType();
}
return new IEnumDeserializer((Class<? extends IEnum>) type.getRawClass());
}
}
然后在我们的 IEnum 类上,添加 @JsonDeserialize 注解,指定反序列化。
@JsonDeserialize(using = IEnumDeserializer.class)
public interface IEnum {
Integer getValue();
}
这样子,就可以在 @RequestBody 注解的实体类里面使用枚举。
4 响应参数转换枚举
我们返回响应参数时,经常需要把枚举值转成对应的 int,因为Spring boot框架响应参数的时候,采用的也是 Jackson 序列化,因此我们可以采用类似上面的做法,继承 JsonSerializer 抽象类,实现自定义转换。
public class IEnumSerializer extends JsonSerializer<IEnum> {
@Override
public void serialize(IEnum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (Objects.isNull(value)) {
gen.writeNull();
return;
}
gen.writeNumber(value.getValue());
}
}
然后在我们的 IEnum 类上,添加 @JsonDeserialize 注解,指定序列化。
@JsonSerialize(using = IEnumSerializer.class)
@JsonDeserialize(using = IEnumDeserializer.class)
public interface IEnum {
Integer getValue();
}
这样子,我们在给前端返回枚举的时候,就可以不用自己手动进行转换,让框架自动帮我们进行转换。
5 消息参数转换枚举
其实通过我们上面的配置,已经自定义了枚举的序列化和反序列化,只需要在我们的消息接收和发送的时候,采用 Jackson 序列化的形式,就可以实现枚举的自动转换。
6 总结
通过以上的配置,基本上可以实现我们整个项目的枚举统一,消除我们在项目内频繁需要对枚举和数值互相转换的操作。
如果是前端页面有相关的枚举下拉框选择查询,我们还可以根据顶层的枚举接口,对所有枚举进行扫描并存储到内存中,然后提供枚举查询的接口给前端,这样子的话,后续新增枚举参数的话,可以直接就从接口获取到更新的值。