Spring 配置自定义 Converter 转换接收枚举类型参数
Spring 配置自定义 Converter 转换接收枚举类型参数
1、背景
在数据库表结构设计中,经常会存在类似于 type 等用来标识固定数据状态的字段,当对应到 Java 实体类时,会成为 int 类型的字段;但是问题来了,这种字段的值类型太难记忆了,且不容易统一管理,即使使用常量或枚举去指定该值,那也不太方便;那何不直接把实体中的这个字段定义为枚举类型呢?好,那接下来就要考虑两个问题:
(1)如果把 type 定义为枚举,那和数据库做交互的时候,能转换成数据库认识的数据吗?
答:巧了,如果使用的是
MyBatis Plus或其他MyBatis框架,他们提供了这种 字段转换机制,如MyBatis Plus的“通用枚举”。
(2)实体对应的 DTO 或 VO 视图对象,在使用参数传递数据时,这种枚举类型的字段能否实现 json 序列化 及 json 反序列化?
答:经过我的亲身测试,在一系列的配置之后,正常传递参数和 MVC 映射参数是没有问题的。
2、转换方案
注意:Spring MVC 或 Spring Boot 本身是不能直接映射枚举类型参数的,需要自定义数据转换方式。下面就全程以一个简单的例子来进行演示操作。
2.1、方式一:实现 Spring 的 Converter 接口
(1)性别枚举:
@Getter
public enum GenderEnum {
MEN(1, "男"), WOMEN(0, "女");
private int code;
private String msg;
private GenderEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public static GenderEnum getEnum(int code) {
for (GenderEnum item : GenderEnum.values()) {
if (item.getCode() == code) {
return item;
}
}
return null;
}
}(2)用户对象(这里使用 DTO 对象演示)
@Data
public class UserDto implements Serializable {
private static final long serialVersionUID = 2965899347474942734L;
private Long id;
private String name;
private GenderEnum gender;
}(3)StringToGenderConverter 类:
特别注意:
- 实现接口后要使用 @Component 或者 @Bean 的形式把该类交给 Spring 去管理,这样转换才会起作用
- 在
Spring MVC和Spring Boot中,由于从客户端接收到的请求都被视为 String 类型,所以只能用 String 转枚举的 converter。 - 如果是
Spring CloudFeign调用,以下第(4)步的方法应为 POST 方式,且 UserDto 前要使用 @RequestBody 的形式接收(其实如果使用 @RequestBody 的形式接收参数,不用编写Converter都可以自动映射成功,本人也无法解释,百度一番后猜测可能是 Jakson 或 fastjson 在反序列化时自动解析成枚举,见 https://blog.csdn.net/alinyua/article/details/86383254#SpringRequestBody__63,EnumDeserializer 类) - 如果调用失败,请再三确认 converter 实现类是否真正被 Spring 管理了
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import com.alibaba.druid.util.StringUtils;
import com.marry.common.enums.GenderEnum;
@Component
public class StringToGenderConverter implements Converter<String, GenderEnum> {
@Override
public GenderEnum convert(String source) {
if (!StringUtils.isEmpty(source)) {
return GenderEnum.getEnum(Integer.parseInt(source));
}
return null;
}
}(4)接下来就要测试我们的参数能否正常映射
@RestController
@RequestMapping("/")
public class TestController {
@GetMapping("demo")
public UserDto demo(UserDto dto) {
System.out.println("---->user:" + dto);
return dto;
}
}访问:http://localhost:8890/demo?id=1&name=zhangsan&gender=1
后台:---->user:UserDto(id=1, name=zhangsan, gender=MEN)
浏览器:{"id":1,"name":"zhangsan","gender":"MEN"}
2.2、方式二:实现 ConverterFactory 接口(推荐)
Converter 可以让我们把 String 转换为某一个枚举对象,但如果有多个枚举岂不是要写多个 Converter?No No No,好在 Spring 为我们提供了 Converter 的工厂接口,让我们来统一管理并产生 Converter。
(1)定义一个枚举接口 BaseEnum,用来约束通用的接口行为
/**
* 枚举基类
* <p>
* 适用于有Code编码的枚举场景(如:<code>MEN(1,"男")</code>),值为字符串的简单枚举无需继承此类(如:<code>TEST("xxx")</code>)
* </p>
*
* @author Jastar Wang
* @date 2019-06-14
* @version 1.0
*/
public interface BaseEnum {
/**
* 获取code值
* <li>可用于Controller枚举类型参数接收时转换</li>
* <li>可用于统一根据Code值获取枚举对象</li>
*
* @return 数字值
*/
int getCode();
}(2)让我们的 GenderEnum 实现 BaseEnum
(没看到重写方法?偷偷告诉你:@Getter 无缝实现了)
@Getter
public enum GenderEnum implements BaseEnum{
MEN(1, "男"), WOMEN(0, "女");
private int code;
private String msg;
private GenderEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
}(3)最重要的来了,实现工厂接口
/**
* Code数字字符串转枚举工厂
*
* @author Jastar Wang
* @date 2019-06-14
* @version 1.0
*/
@Component
@Slf4j
@SuppressWarnings({ "rawtypes", "unchecked" })
public class CodeStringToEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
private static final Map<Class, Converter> CONVERTER_MAP = new WeakHashMap<>();
@Override
public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
Converter converter = CONVERTER_MAP.get(targetType);
if (converter == null) {
converter = new CodeStringToEnum<T>(targetType);
CONVERTER_MAP.put(targetType, converter);
}
return converter;
}
private class CodeStringToEnum<T extends BaseEnum> implements Converter<String, T> {
private final Class<T> enumType;
private Map<String, T> enumMap = new HashMap<>();
public CodeStringToEnum(Class<T> enumType) {
this.enumType = enumType;
T[] enums = enumType.getEnumConstants();
if (enums != null && enums.length > 0) {
for (T e : enums) {
enumMap.put(e.getCode() + "", e);
}
}
}
@Override
public T convert(String source) {
try {
if (!source.isEmpty()) {
return enumMap.get(source.trim());
}
return null;
} catch (Exception e) {
log.error("Cannot convert to Enum[" + enumType + "] from value:[" + source + "]!", e);
return null;
}
}
}
}(4)集成至 Spring Boot
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new CodeStringToEnumConverterFactory());
}
}3、扩展:MyBatis Plus 实现通用枚举
3.1、坑一
一定要在配置文件中配置:
mybatis-plus.configuration.default-enum-type-handler=com.baomidou.mybatisplus.extension.handlers.EnumTypeHandler参考官网说明,他的作用是用来识别实体中的那些枚举字段,配置这个可以省略配置“枚举包扫描”
3.2、坑二
如果枚举字段对应数据库中的列类型为 tinyint,那么在查询实体数据的时候,该枚举字段会发现查不到值,因为 tinyint 会被识别成 Java 中的 boolean 类型。三种解决方案:
- 使用 ifnull(column, 0)处理该字段,个人测试过可以;
- 在 jdbcUrl 添加参数:
tinyInt1isBit=false(默认为 true); - 避免使用长度为 1 的 tinyint 类型字段存储数字格式的数据,如使用
tinyint(4),虽然长度是 4,但是tinyint都是占用 1 个字节。

