在SpringMVC 转换时候报如下错误
ERROR com.xxxx.common.exception.BaseExceptionHandler:56 - org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'XXXXPO' on field 'state': rejected value [0]; codes [typeMismatch.XXXXPO.state,typeMismatch.state,typeMismatch.com.xxxx.constant.enums.State,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [XXXXPO.state,state]; arguments []; default message [state]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.xxxx.constant.enums.State' for property 'state'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [com.xxxx.constant.enums.State] for value '0'; nested exception is java.lang.IllegalArgumentException: No enum constant com.xxxx.constant.enums.State.0]
如果变量值仅有有限的可选值,那么用枚举类来定义常量是一个很常规的操作。
但是在业务代码中,我们不希望依赖 ordinary()
进行业务运算,而是自定义数字属性,避免枚举值的增减调序造成影响。
@Getter
@AllArgsConstructor
public enum CourseType {
PICTURE(102, "图文"),
AUDIO(103, "音频"),
VIDEO(104, "视频"),
;
private final int index;
private final String name;
}
但也正是因为使用了自定义的数字属性,很多框架自带的枚举转化功能也就不再适用了。因此,我们需要自己来扩展相应的转化机制,这其中包括:
以上文的 CourseType
为例,我们希望达到的效果是:
前端传参时给我们枚举的 index
值,在 controller 中,我们可以直接使用 CourseType
来接收,由框架负责完成 index
到 CourseType
的转换。
@GetMapping("/list")
public void list(@RequestParam CourseType courseType) {
// do something
}
SpringMVC 自带了两个和枚举相关的转换器:
这两个转换器是通过调用枚举的 valueOf
方法来进行转换的,感兴趣的同学可以自行查阅源码。
虽然这两个转换器不能满足我们的需求,但它也给我们带来了思路,我们可以通过模仿这两个转换器来实现我们的需求:
废话不多说,上源码:
/**
* springMVC 枚举类的转换器
* 如果枚举类中有工厂方法(静态方法)被标记为{@link EnumConvertMethod },则调用该方法转为枚举对象
*/
@SuppressWarnings("all")
public class EnumMvcConverterFactory implements ConverterFactory> {
private final ConcurrentMap>, EnumMvcConverterHolder> holderMapper = new ConcurrentHashMap<>();
@Override
public > Converter getConverter(Class targetType) {
EnumMvcConverterHolder holder = holderMapper.computeIfAbsent(targetType, EnumMvcConverterHolder::createHolder);
return (Converter) holder.converter;
}
@AllArgsConstructor
static class EnumMvcConverterHolder {
@Nullable
final EnumMvcConverter> converter;
static EnumMvcConverterHolder createHolder(Class> targetType) {
List methodList = MethodUtils.getMethodsListWithAnnotation(targetType, EnumConvertMethod.class, false, true);
if (CollectionUtils.isEmpty(methodList)) {
return new EnumMvcConverterHolder(null);
}
Assert.isTrue(methodList.size() == 1, "@EnumConvertMethod 只能标记在一个工厂方法(静态方法)上");
Method method = methodList.get(0);
Assert.isTrue(Modifier.isStatic(method.getModifiers()), "@EnumConvertMethod 只能标记在工厂方法(静态方法)上");
return new EnumMvcConverterHolder(new EnumMvcConverter<>(method));
}
}
static class EnumMvcConverter> implements Converter {
private final Method method;
public EnumMvcConverter(Method method) {
this.method = method;
this.method.setAccessible(true);
}
@Override
public T convert(String source) {
if (source.isEmpty()) {
// reset the enum value to null.
return null;
}
try {
return (T) method.invoke(null, Integer.valueOf(source));
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
}
}
EnumMvcConverterFactory :工厂类,用于创建 EnumMvcConverter
EnumMvcConverter:自定义枚举转换器,完成自定义数字属性到枚举类的转化
EnumConvertMethod:自定义注解,在自定义枚举类的工厂方法上标记该注解,用于 EnumMvcConverter 来进行枚举转换
EnumConvertMethod 的具体源码如下:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnumConvertMethod {
}
1、注册 EnumMvcConverterFactory
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Bean
public EnumMvcConverterFactory enumMvcConverterFactory() {
return new EnumMvcConverterFactory();
}
@Override
public void addFormatters(FormatterRegistry registry) {
// org.springframework.core.convert.support.GenericConversionService.ConvertersForPair.add
// this.converters.addFirst(converter);
// 所以我们自定义的会放在前面
registry.addConverterFactory(enumMvcConverterFactory());
}
}
2、在自定义枚举中提供一个工厂方法,完成自定义数字属性到枚举类的转化,同时在该工厂方法上添加 @EnumConvertMethod 注解
@Getter
@AllArgsConstructor
public enum CourseType {
PICTURE(102, "图文"),
AUDIO(103, "音频"),
VIDEO(104, "视频"),
;
private final int index;
private final String name;
private static final Map mappings;
static {
Map temp = new HashMap<>();
for (CourseType courseType : values()) {
temp.put(courseType.index, courseType);
}
mappings = Collections.unmodifiableMap(temp);
}
@EnumConvertMethod
@Nullable
public static CourseType resolve(int index) {
return mappings.get(index);
}
}
还是以上述的 CourseType 枚举为例,一般业务代码的数据都要持久化到 DB 中的。假设,现在有一张课程元数据表,用于记录当前课程所属的类型,我们的 entity 对象可能是这样的:
@Getter
@Setter
@Entity
@Table(name = "course_meta")
public class CourseMeta {
private Integer id;
/**
* 课程类型,{@link CourseType}
*/
private Integer type;
}
上述做法是通过 javadoc 注释的方式来告诉使用方 type 的取值类型是被关联到了 CourseType。
但是,我们希望通过更清晰的代码来避免注释,让代码不言自明。
因此,能不能让 ORM 在映射的时候,直接把 Integer 类型的 type 映射成 CourseType 枚举呢?答案是可行的。
我们当前系统使用的是 Spring Data JPA 框架,是对 JPA 的进一步封装。因此,本文只提供在 JPA 环境下的解决方案。
在 JPA 规范中,提供了 javax.persistence.AttributeConverter 接口,用于扩展对象属性和数据库字段类型的映射。
public class CourseTypeEnumConverter implements AttributeConverter {
@Override
public Integer convertToDatabaseColumn(CourseType attribute) {
return attribute.getIndex();
}
@Override
public CourseType convertToEntityAttribute(Integer dbData) {
return CourseType.resolve(dbData);
}
}
怎么生效呢?有两种方式
本文选择的是第二种方式,在需要的地方指定 AttributeConverter,具体代码如下:
@Getter
@Setter
@Entity
@Table(name = "ourse_meta")
public class CourseMeta {
private Integer id;
@Convert(converter = CourseTypeEnumConverter.class)
private CourseType type;
}
到这里,我们已经解决了 SpringMVC 和 ORM 对自定义枚举的支持,那是不是这样就足够了呢?还有什么问题呢?
SpringMVC 的枚举转化器只能支持 GET 请求的参数转化,如果前端提交 JSON 格式的 POST 请求,那还是不支持的。
另外,在给前端输出 VO 时,默认情况下,还是要手动把枚举类型映射成 Integer 类型,并不能在 VO 中直接使用枚举输出。
@Data
public class CourseMetaShowVO {
private Integer id;
private Integer type;
public static CourseMetaShowVO of(CourseMeta courseMeta) {
if (courseMeta == null) {
return null;
}
CourseMetaShowVO vo = new CourseMetaShowVO();
vo.setId(courseMeta.getId());
// 手动转化枚举
vo.setType(courseMeta.getType().getIndex());
return vo;
}
}
Jackson 是一个非常强大的 JSON 序列化工具,SpringMVC 默认也是使用 Jackson 作为其 JSON 转换器。
Jackson 为我们提供了两个注解,刚好可以解决这个问题。
最后的代码如下:
@Getter
@AllArgsConstructor
public enum CourseType {
PICTURE(102, "图文"),
AUDIO(103, "音频"),
VIDEO(104, "视频"),
;
@JsonValue
private final int index;
private final String name;
private static final Map mappings;
static {
Map temp = new HashMap<>();
for (CourseType courseType : values()) {
temp.put(courseType.index, courseType);
}
mappings = Collections.unmodifiableMap(temp);
}
@EnumConvertMethod
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
@Nullable
public static CourseType resolve(int index) {
return mappings.get(index);
}
}