如何处理枚举类型(下)
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
上一篇我们通过编写MyBatis的转换器最终完成枚举在DAO层和数据库之间的转换:

现在让我们把目光往前移,思考一下如何编写SpringMVC的转换器完成前端与Controller层的枚举转换。
环境准备
目录结构

pom.xml(小册使用的版本都是2.3.4,但今天遇到坑了,后面会提到)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.4.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>springboot_enum</artifactId><version>0.0.1-SNAPSHOT</version><name>springboot_enum</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
POJO
@Data
public class UserDTO {/*** 姓名*/private String name;/*** 年龄*/private Integer age;/*** 用户类型,枚举*/private UserTypeEnum userType;
}
UserTypeEnum
@Getter
public enum UserTypeEnum {STUDENT(1, "学生"),TEACHER(2, "老师"),;private final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type = type;this.desc = desc;}
}
Controller
@Slf4j
@RestController
@RequestMapping("/api/web/user")
public class UserController {@GetMapping("/get")public void get(UserDTO userDTO) {log.info(userDTO.toString());}@PostMapping("/postForm")public void postForm(UserDTO userDTO) {log.info(userDTO.toString());}@PostMapping("/postJson")public void postJson(@RequestBody UserDTO userDTO) {log.info(userDTO.toString());}}
关于GET与POST
首先,要和大家交代一下,常见的请求方式分两大类(不算REST风格):
- GET
- POST
GET和POST有个很大区别是:GET请求的参数放在请求行,而POST请求的参数放在请求体(Body)。
另外,POST请求又细分很多种:
- form-data
- x-www-form-urlencoded
- json
如果你足够细心,平时使用Postman时就会注意到以上三种POST请求(形式虽不同,但参数都在Body):



我们会在JavaWeb章节详细介绍它们的区别,这里按下不表。
需要注意的是,从后端接口参数的格式看,POST请求中的表单提交方式和GET请求是很相似的:

所以本文在测试时分为两个阵营:
- GET与POST表单
- POST JSON
测试的方向分为:
- 请求(入)
- 响应(出)
开始测试之前,再来回顾一下我们写的枚举:

枚举名称(name)分别叫"STUDENT","TEACHER",之前分析过,所有的枚举类默认继承Enum,而Enum重写了toString():

所以当我们打印STUDENT或TEACHER对象时,最终会打印: "STUDENT"、"TEACHER"。
- UserTypeEnum有两个字段:type和desc
- 抽象父类Enum也有两个字段:ordinal(序号,从0开始)和name(枚举名称)
OK,接下来让我们开始测试。
请求(反序列化)
测试请求时,我们的关注点是:前端传入"userType:STUDENT",后端是如何变成UserTypeEnum对象的。
测试GET与POST表单
传入Enum.name,转换成功
GET请求

POST表单


很明显,前端传"STUDENT"、"TEACHER"等枚举名称(name)时,SpringMVC能自动帮我们转为对应的枚举对象,而在实际打印时由于调用了toString(),所以显示userType=STUDENT。
那么,为什么枚举名称name为什么会自动转为枚举对象UserTypeEnum呢?我们先不管SpringMVC怎么做到的,通过断点,很容易发现SpringMVC在解析"STUDENT"这个字符串时最终调用了Enum#valueOf(),然后根据name获取枚举对象:

传入Enum.ordinal,转换失败

无论是GET还是POST表单,传入0或1都失败了(ordinal从0开始),也就是说SpringMVC默认不支持根据ordinal转换:
Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'userDTO' on field 'userType': rejected value [1]; codes [typeMismatch.userDTO.userType,typeMismatch.userType,typeMismatch.com.bravo.demo.enums.UserTypeEnum,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userDTO.userType,userType]; arguments []; default message [userType]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.bravo.demo.enums.UserTypeEnum' for property 'userType'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [com.bravo.demo.enums.UserTypeEnum] for value '1'; nested exception is java.lang.IllegalArgumentException: No enum constant com.bravo.demo.enums.UserTypeEnum.1]]
特别注意最后的异常信息,似乎在哪见到过:

也就是说,对于GET/POST表单请求,SpringMVC都是根据valueOf()来匹配枚举对象的。
也即是说,对于GET和POST表单请求而言,如果想正确的反序列化(String转为Enum对象),前端只能传Enum.name。
传入UserTypeEnum.type,转换失败
理由如上
传入UserTypeEnum.desc,转换失败
理由如上
默认的ConverterFactory
对于前端来说,他们可能更喜欢传递枚举内部的字段,比如UserTypeEnum.type,而不是Enum.name。有没有办法更改SpringMVC的默认行为,当前端传递userType=1时,把1转为UserTypeEnum的“学生”对象呢?
要解决这个问题,可以分两步:
- 了解GET/POST表单请求时,SpringMVC默认的转换机制
- 改写这个机制
由于我们已经知道整个请求链路的终点是调用Enum#valueOf()进行转换,于是给valueOf()打上断点:

省略中间的步骤,根据调用链进行反推,很快定位到AbstractPropertyAccessor#setPropertyValues():

这是个for循环,它拿到了UserDTO的所有属性并逐个进行赋值。比如截图的代码显示SpringMVC正在给UserDTO.userType字段赋值。
再往下走几步会看到GenericConversionService#convert():

找到converter后调用converter的convert()方法进行值转换:

最终把转换后的值设置给UserDTO.userType。
我们发现,SpringMVC默认的枚举转换器是StringToEnumConverterFactory:

它的convert()方法正好调用了Enum.valueOf(),所以GET/POST表单请求时只能传Enum.name,至此真相大白。
整个流程是:
- 前端发起请求,传递userType="STUDENT"
- 从Tomcat的Servlet到SpringMVC的Controller,中间要经过很多类和方法
- SpringMVC会解析入参对象的每一个字段,选取合适的ConverterFactory为其进行转换
- 默认使用StringToEnumConverterFactory为枚举类型进行转换,即调用Enum.valueOf(name)
有了上面的铺垫,关于GET/POST表单请求时如何自定义枚举入参转换器已经很明确了。
自定义EnumConverterFactory
/*** 自定义枚举转换器(直接抄StringToEnumConverterFactory)** @author mx*/
public final class MyEnumConverterFactory implements ConverterFactory<String, Enum> {@Overridepublic <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {return new StringToEnum(targetType);}private static class StringToEnum<T extends Enum> implements Converter<String, T> {private final Class<T> enumType;public StringToEnum(Class<T> enumType) {this.enumType = enumType;}/*** StringToEnumConverterFactory默认是调用Enum.valueOf(),也就是根据Enum.name匹配* 我们改成根据Enum.ordinal匹配** @param source* @return*/@Overridepublic T convert(String source) {if (source.isEmpty()) {// It's an empty enum identifier: reset the enum value to null.return null;}for (T enumObject : enumType.getEnumConstants()) {if (source.equals(String.valueOf(enumObject.ordinal()))) {return enumObject;}}return null;}}}
把它加到请求链路中:
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addFormatters(FormatterRegistry registry) {// 把我们自定义的枚举转换器添加到Spring容器,Spring容器会把它加入到SpringMVC的拦截链路中registry.addConverterFactory(new MyEnumConverterFactory());}
}
测试:


特别特别注意,把MyEnumConverterFactory加入调用链后,jackson原本的StringToEnumConverterFactory就不起作用了,此时前端传入"STUDENT"、"TEACHER"将无法成功解析。
改进自定义枚举转换器
上面这样还是无法满足我们的需求:我们只是把原先默认支持Enum.name改为Enum.ordinal。
部分同学可能有疑问:你刚才为什么不直接在上面的ConverterFactory中调用getType()或者getDesc()呢?

不是我不想,而是不好这样做。两点理由:
- getType()/getDesc不够通用,项目中其他枚举可能叫getValue()/getDescription()
- 最重要的是,class MyEnumConverterFactory implements ConverterFactory<String, Enum>使用Enum限定,内部元素只能使用父类Enum的方法,无法直接调用getType()等方法
解决办法有两个:
- 抽取公共的IEnum接口,强制指定按哪个字段反序列化
- 使用注解+反射
方案1:抽取IEnum接口,强制指定反序列化字段
IEum接口
/*** 统一的枚举接口** @author mx*/
public interface IEnum<T> {/*** 强制指定按哪个字段进行反序列化** @return*/T getValue();}
让UserTypeEnum实现IEnum:
@Getter
public enum UserTypeEnum implements IEnum<String> {STUDENT(1, "学生"),TEACHER(2, "老师"),;private final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type = type;this.desc = desc;}/*** 强制指定按哪个字段进行反序列化** @return*/@Overridepublic String getValue() {return this.desc;}
}
改写MyEnumConverterFactory:
/*** 自定义枚举转换器,配合统一枚举接口IEnum** @author mx*/
public final class MyEnumConverterFactory implements ConverterFactory<String, IEnum> {@Overridepublic <T extends IEnum> Converter<String, T> getConverter(Class<T> targetType) {return new StringToEnum(targetType);}private static class StringToEnum<T extends IEnum> implements Converter<String, T> {private final Class<T> enumType;public StringToEnum(Class<T> enumType) {this.enumType = enumType;}@Overridepublic T convert(String source) {if (source.isEmpty()) {// It's an empty enum identifier: reset the enum value to null.return null;}for (T enumObject : enumType.getEnumConstants()) {// 默认项目中所有Enum都实现了IEnum,那么必然有getValue()if (source.equals(String.valueOf(enumObject.getValue()))) {return enumObject;}}return null;}}}
测试:


方案2:注解+反射
/*** 指定反序列化字段** @author mx*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyJsonCreator {
}
/*** 自定义枚举转换器,还是用原生的Enum* 使用分三步:* 1.自定义一个注解,假设叫@MyJsonCreator* 2.读取注解* 3.解析注解字段的值,找到匹配的枚举对象* <p>* MyEnumConverterFactory主要负责第2、3步** @author mx*/
public final class MyEnumConverterFactory implements ConverterFactory<String, Enum> {@Overridepublic <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {return new StringToEnum(targetType);}private static class StringToEnum<T extends Enum> implements Converter<String, T> {private final Class<T> enumType;public StringToEnum(Class<T> enumType) {this.enumType = enumType;}@Overridepublic T convert(String source) {if (source.isEmpty()) {// It's an empty enum identifier: reset the enum value to null.return null;}try {for (T enumObject : enumType.getEnumConstants()) {Field[] declaredFields = enumObject.getClass().getDeclaredFields();for (Field declaredField : declaredFields) {// 读取@MyJsonCreator标注的字段if (declaredField.isAnnotationPresent(MyJsonCreator.class)) {declaredField.setAccessible(true);// 读取对应的字段valueObject fieldValue = declaredField.get(enumObject);// 匹配并返回对于的Enumif (source.equals(String.valueOf(fieldValue))) {return enumObject;}}}}} catch (IllegalAccessException e) {e.printStackTrace();}return null;}}}
在UserTypeEnum中使用:
@Getter
public enum UserTypeEnum {STUDENT(1, "学生"),TEACHER(2, "老师"),;@MyJsonCreatorprivate final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type = type;this.desc = desc;}}
测试POST JSON
为了不干扰后续的实验,请大家先把自定义的枚举转换器注释掉:

传入Enum.name、Enum.ordinal,转换成功
测试POST JSON:




我们惊奇的发现:SpringMVC默认就支持了Enum.name和Enum.ordinal的转换,但对于子类UserTypeEnum的特有字段(type、desc)是不识别的。
HttpMessageConverter与jackson
有部分同学可能有点晕了,来捋一捋:
- GET/POST表单,默认使用StringToEnumConverterFactory,只支持Enum.name
- POST JSON默认支持Enum.name、Enum.ordinal
很明显POST JSON和GET/POST表单使用的不是同一个转换器,并且从上面的异常信息可以捕捉到一丝丝信息:
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `com.bravo.demo.enums.UserTypeEnum` from String "学生": not one of the values accepted for Enum class: [TEACHER, STUDENT]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `com.bravo.demo.enums.UserTypeEnum` from String "学生": not one of the values accepted for Enum class: [TEACHER, STUDENT]
at [Source: (PushbackInputStream); line: 4, column: 17] (through reference chain: com.bravo.demo.pojo.UserDTO["userType"])]
市面上常见的3种JSON转换工具:
- jackson(SpringBoot内置)
- fastjson(阿里)
- gson(谷歌)
SpringBoot默认使用jackson作为JSON转换工具,比如我们经常会用的ObjectMapper其实就是jackson的。
而JSON转换工具的作用点有两个:
- JSON请求:@RequestBody
- 响应:@ResponseBody
GET或POST表单请求由于参数并不是JSON形式,所以用不到jackson,只需要实现ConverterFactory:

而POST JSON请求则需要实现HttpMessageConverter(jackson已经提供):

请注意,ConverterFactory和HttpMessageConverter两个接口的包路径都不一样,并没有什么关联。
SpringMVC如何处理JSON请求
由于JSON请求本质是字符串,所以必须要有反序列化的过程。SpringMVC对外提供了HttpMessageConverter接口用于处理JSON,而SpringBoot内置的jackson提供了该接口的实现类MappingJackson2HttpMessageConverter:

当一个JSON请求达到SpringMVC,容器会根据为当前请求参数挑选合适的Converter:

此时就轮到jackson的MappingJackson2HttpMessageConverter出场了。如果你跟着debug,就会发现实际上大部分工作都是AbstractJackson2HttpMessageConverter干的,jackson的主要贡献是提供了ObjectMapper实例及各种Serializer、Deserializer用于序列化和反序列化:

AbstractJackson2HttpMessageConverter内部的ObjectMapper被赋值后(通过构造器),如果有请求到达SpringMVC,它会调用ObjectMapper(Serializer、Deserializer)对参数进行转换。
比如EnumDeserializer默认支持转换Enum.name、Enum.ordinal:

具体的源码就不在这里带大家跟读了,我们会在Spring章节分析@RequestBody时解释,目前大家可以像下面截图一样打上断点,然后用Postman分别传递数字(Enum.ordinal)或字符串(Enum.name)体会一下:

你会发现,jackson的EnumDeserializer默认的解析策略是:
- 如果是字符串,默认作为Enum.name解析:

- 如果是数字,则按ordinal解析 :

如果你刚好是从上一篇文章过来的,就会发现jackson的策略和MyBatis很像,都支持了Enum.name和Enum.ordinal的转换。那么,如果前端传递的是UserTypeEnum.type或者UserTypeEnum.desc呢?
@JsonCreator自定义反序列化字段
好在,jackson还提供了@JsonCreator注解让我们自己指定反序列化的字段:
@Slf4j
@Getter
public enum UserTypeEnum {STUDENT(1, "学生"),TEACHER(2, "老师"),;/*** 用@JsonValue指定序列化字段,后面再介绍,不用管*/@JsonValueprivate final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type = type;this.desc = desc;}/*** 静态方法+@JsonCreator指定根据哪个字段反序列化** @param desc* @return*/@JsonCreatorpublic static UserTypeEnum getEnum(String desc) {for (UserTypeEnum item : values()) {if (item.getDesc().equals(desc)) {log.info("进来了, desc:{}, item:{}", desc, item.toString());return item;}}return null;}public static void main(String[] args) throws IOException {// 模拟Postman发送JSON请求ObjectMapper objectMapper = new ObjectMapper();String json = "{\n" +" \"name\": \"bravoPostJson\",\n" +" \"age\": 18,\n" +" \"userType\": \"老师\"\n" +"}";System.out.println(json);// 请求:反序列化UserDTO userDTO = objectMapper.readValue(json, UserDTO.class);System.out.println(userDTO);// 响应:序列化String returnJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(userDTO);System.out.println(returnJson);}
}
结果
请求:
{
"name": "bravoPostJson",
"age": 18,
"userType": "老师"
}
接收:
进来了, desc:老师, item:TEACHER
UserDTO(name=bravoPostJson, age=18, userType=TEACHER)
响应:
{
"name" : "bravoPostJson",
"age" : 18,
"userType" : 2
}
看起来很完美啊,但是你用Postman去请求就会报错。我调试了一晚上(坑爹),结果发现是当前SpringBoot版本问题(2.3.3),SpringBoot2.0.x是可以的(估计2.3.3修改了jackson的默认设置):


响应(序列化)
介绍完前端如何传入枚举参数(入),最后讲讲枚举如何响应给前端(出)。其实答案已经呼之欲出:@JsonValue,但方案不止一种。
在测试前,请大家修改Controller,让接口返回UserDto:
@Slf4j
@RestController
@RequestMapping("/api/web/user")
public class UserController {@GetMapping("/get")public UserDTO get(UserDTO userDTO) {log.info(userDTO.toString());return userDTO;}@PostMapping("/postForm")public UserDTO postForm(UserDTO userDTO) {log.info(userDTO.toString());return userDTO;}@PostMapping("/postJson")public UserDTO postJson(@RequestBody UserDTO userDTO) {log.info(userDTO.toString());return userDTO;}}
把之前请求相关的配置先注释掉,并把SpringBoot版本改为2.0.5:


OK,我们自定义MyEnumConverterFacotry注释后,对于GET/POST表单请求重新使用默认的StringToEnumConverterFactory,仅支持Enum.name反序列化。而POST JSON请求默认支持Enum.name和Enum.ordinal。
现在你可以认为代码都回到了最初创建SpringBoot项目的状态。由于这回是测试响应形式,我们不关心入参,所以统一传递大家都支持的Enum.name。
需要注意的是,无论GET/POST表单还是POST JSON请求,它们只是请求方式不同,而响应形式其实都是JSON,因为我们使用了@RestController = @Controller + @ResponseBody。

所以,对于响应只需测试其中任意一组即可。
至于使用了@ResponseBody后SpringMVC如何处理返回值,由于篇幅已经太长,留到Spring部分再聊。但有一点可以肯定,正如JSON请求那样,JSON响应也会经过jackson的处理,而且必然调用HttpMessageConverter的write()。
中间复杂的调用就跳过了,直接看AbstractJackson2HttpMessageConverter#writeInternal():

即最终会调用objectWriter.writeValue(generator, value)进行序列化写入response缓冲区。我们注意到,在调用writeValue()之前,userType字段还是个UserTypeEnum对象:

而writeValue()本身已经没有什么好分析了:

所以为什么UserTypeEnum最终会变成userType = "STUDENT"?这和SpringMVC本身没什么关系,取决于JSON转换工具怎么设计的,而jackson默认就是调用Enum.name()。
如何改变jackson对枚举类型的默认序列化规则呢?

方案1:@JsonValue
在需要序列化的字段上加@JsonValue即可。特别注意,对于POST JSON请求,使用@JsonValue必须配合使用@JsonCreator,否则会报错(很难受):

方案2:全局设置SerializationFeature

做了上面的设置,相当于告诉jackson序列化响应时调用对象的toString()即可,相应地我们要重写toString():
/*** 自定义JSON响应时枚举字段的序列化行为:调用toString()** @return*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {return builder -> builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
}
@Getter
public enum UserTypeEnum {STUDENT(1, "学生"),TEACHER(2, "老师"),;private final Integer type;private final String desc;UserTypeEnum(Integer type, String desc) {this.type = type;this.desc = desc;}@Overridepublic String toString() {return "没想到吧,UserTypeEnum序列化后竟然是完全无关的文字~";}
}
测试GET响应:

测试POST JSON响应:

推荐方案
SpringMVC对请求和响应的处理原本就复杂,再加上枚举,使得整篇文章难度加大不少。很多同学可能有点晕,这里总结一下,并尝试给出我推荐的方案。

个人推荐的方案
请求
- POST JSON:@JsonCreator
- GET/POST:@MyJsonCreator
响应
- 全局设置toString()作为序列化的值
/*** 指定GET/POST表单请求反序列化字段* POST JSON请求反序列字段请用jackson原生注解@JsonCreator** @author mx*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyJsonCreator {
}
@Slf4j
@Getter
public enum UserTypeEnum {STUDENT(1, "学生"),TEACHER(2, "老师"),;private final Integer type;/*** MyEnumConvertFactory+@MyJsonCreator指定GET/POST表单请求根据哪个字段反序列化*/@MyJsonCreatorprivate final String desc;UserTypeEnum(Integer type, String desc) {this.type = type;this.desc = desc;}/*** 静态方法+@JsonCreator指定POST JSON请求根据哪个字段反序列化** @param desc* @return*/@JsonCreatorpublic static UserTypeEnum getEnum(String desc) {for (UserTypeEnum item : values()) {if (item.getDesc().equals(desc)) {log.info("进来了, desc:{}, item:{}", desc, item.toString());return item;}}return null;}/*** 统一序列化字段,调用toString()返回** @return*/@Overridepublic String toString() {return String.valueOf(this.type);}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {/*** 自定义GET/POST表单提交方式的入参反序列化规则** @param registry*/@Overridepublic void addFormatters(FormatterRegistry registry) {// 把我们自定义的枚举转换器添加到Spring容器,Spring容器会把它加入到SpringMVC的拦截链路中registry.addConverterFactory(new MyEnumConverterFactory());}/*** 自定义JSON响应时枚举字段的序列化行为:调用toString()** @return*/@Beanpublic Jackson2ObjectMapperBuilderCustomizer customizer() {return builder -> builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);}}
/*** 自定义枚举转换器,还是用原生的Enum* 使用分三步:* 1.自定义一个注解,假设叫@JsonCreator* 2.读取注解* 3.解析注解字段的值,找到匹配的枚举对象* <p>* MyEnumConverterFactory主要负责第2、3步** @author mx*/
public final class MyEnumConverterFactory implements ConverterFactory<String, Enum> {@Overridepublic <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {return new StringToEnum(targetType);}private static class StringToEnum<T extends Enum> implements Converter<String, T> {private final Class<T> enumType;public StringToEnum(Class<T> enumType) {this.enumType = enumType;}@Overridepublic T convert(String source) {if (source.isEmpty()) {// It's an empty enum identifier: reset the enum value to null.return null;}try {for (T enumObject : enumType.getEnumConstants()) {Field[] declaredFields = enumObject.getClass().getDeclaredFields();for (Field declaredField : declaredFields) {// 读取@MyJsonCreator标注的字段if (declaredField.isAnnotationPresent(MyJsonCreator.class)) {declaredField.setAccessible(true);// 读取对应的字段valueObject fieldValue = declaredField.get(enumObject);// 匹配并返回对于的Enumif (source.equals(String.valueOf(fieldValue))) {return enumObject;}}}}} catch (IllegalAccessException e) {e.printStackTrace();}return null;}}}
我个人实际开发时无论是Controller层还是DAO层,都习惯手动转换枚举。
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬
相关文章:
如何处理枚举类型(下)
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO 联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬 上一篇我们通过编写MyB…...
wsj0数据集原始文件.wv1.wv2转换成wav文件
文章目录 准备一、获取WSJO数据集二、安装sph2pipe三、转换代码四、结果展示 最近做语音分离实验需要用到wsj0-2mix数据集,但是从李宏毅语音分离教程里面获取的wsj0-2mix只有一部分。从网上获取到了完整的WSJO数据集后,由于原始的语音文件后缀是wv1或…...
Flask Session 登录认证模块
Flask 框架提供了强大的 Session 模块组件,为 Web 应用实现用户注册与登录系统提供了方便的机制。结合 Flask-WTF 表单组件,我们能够轻松地设计出用户友好且具备美观界面的注册和登录页面,使这一功能能够直接应用到我们的项目中。本文将深入探…...
【运维】hive 高可用详解: Hive MetaStore HA、hive server HA原理详解;hive高可用实现
文章目录 一. hive高可用原理说明1. Hive MetaStore HA2. hive server HA 二. hive高可用实现1. 配置2. beeline链接测试3. zookeeper相关操作 一. hive高可用原理说明 1. Hive MetaStore HA Hive元数据存储在MetaStore中,包括表的定义、分区、表的属性等信息。 hi…...
C#开发的OpenRA游戏之属性SelectionDecorations(13)
C#开发的OpenRA游戏之属性SelectionDecorations(13) 在前面分析SelectionDecorations属性类时,会发现它有下面这个属性: public class SelectionDecorations : SelectionDecorationsBase, IRender { readonly Interactable interactable; 它是定义了一个Interactabl…...
接手了一个外包开发的项目,我感觉我的头快要裂开了~
嗨,大家好,我是飘渺。 最近,我和小伙伴一起接手了一个由外包团队开发的微服务项目,这个项目采用了当前流行的Spring Cloud Alibaba微服务架构,并且是基于一个“大名鼎鼎”的微服务开源脚手架(附带着模块代…...
git常规使用方法,常规命令
Git是一种分布式版本控制系统,它可以记录软件的历史版本,并提供了多人协作开发、版本回退等功能。以下是Git的基本使用方法: 安装Git:下载安装包并进行安装,安装完成后在命令行中输入 git --version 进行验证。 初始化…...
【JavaScript】3.3 JavaScript工具和库
文章目录 1. 包管理器2. 构建工具3. 测试框架4. JavaScript 库总结 在你的 JavaScript 开发之旅中,会遇到许多工具和库。这些工具和库可以帮助你更有效地编写和管理代码,提高工作效率。在本章节中,我们将探讨一些常见的 JavaScript 工具和库&…...
开发基于 ChatGPT 分析热点事件并生成文章的网站应用【热点问天】把百度等热点用chatGPT来对热点事件分析海量发文章 开发步骤 多种方式获取利润
这样做的优点: 1.不用每个人都问chatGPT同样的问题。 2.已经生成的,反应快速。 3.内容分析的客观,真实,基于数据,无法造假。 4.无其它目的这种基于 ChatGPT 分析热点事件并生成文章的网站,可以通过多种方式…...
龙迅LT8668SXC适用于TPYE-C/DP/HDMI转EDP/VBO同时环出一路HDMI/DP,支持分辨率缩放功能。
1.描述 应用功能:LT8668SXC适用于TYPE-C/DP1.4/HDMI2.1转EDP/VBO同时环出一路HDMI/DP应用方案 分辨率:高达8K30HZ, 工作温度范围:−40C to 85C 产品封装:QFN88 (10*10)最小包装数:1680pcs 2.产品应用 •视频…...
跳板机原理
跳板机原理 跳板机(Jump Server)是一种网络安全设备或计算机,用于管理和保护内部网络中的其他计算机或系统。跳板机通常位于内部网络和外部网络之间,充当连接这两个网络的中间节点或跳板。以下是跳板机的主要功能和用途࿱…...
璞华大数据产品入选中国信通院“铸基计划”
武汉璞华大数据技术有限公司HawkEye设备数字化管理平台产品,凭借优秀的产品技术能力,通过评估后,入选中国信通院“铸基计划”《高质量数字化转型产品及服务全景图(2023)》的工业数字化领域。 “铸基计划”是中国信通院…...
1146. 新的开始,prim算法,超级原点
发展采矿业当然首先得有矿井,小 FF 花了上次探险获得的千分之一的财富请人在岛上挖了 n 口矿井,但他似乎忘记了考虑矿井供电问题。 为了保证电力的供应,小 FF 想到了两种办法: 在矿井 i 上建立一个发电站,费用为 vi&…...
HTTP常见响应码
HTTP(Hypertext Transfer Protocol)是用于在客户端和服务器之间传输资源的协议。HTTP响应码(HTTP status code)用来表示服务器对请求的处理结果。以下是常见的HTTP响应码及其概要: 1. 响应码大类: 主要分…...
物联网边缘计算是什么?如何实现物联网边缘计算?
物联网边缘计算是一种在物联网设备和网络中实施计算和数据处理的技术。它允许在物联网设备或网络边缘进行数据分析和处理,而不需要将所有数据传输到远程数据中心或云端进行处理。物联网边缘计算将计算和数据处理的能力迁移到物联网设备的边缘,使得设备能…...
带着GPT-4V(ision)上路,自动驾驶新探索
On the Road with GPT-4V(ision): Early Explorations of Visual-Language Model on Autonomous Driving GitHub | https://github.com/PJLab-ADG/GPT4V-AD-Exploration arXiv | https://arxiv.org/abs/2311.05332 自动驾驶技术的追求取决于对感知、决策和控制系统的复杂集成。…...
19. Python 数据处理之 Pandas
目录 1. 认识 Pandas2. 安装和导入 Pandas3. Pandas 数据结构4. Pandas 基本功能5. Pandas 数据分析 1. 认识 Pandas Pandas 是 Python 的核心数据分析支持库,提供了快速、灵活、明确的数据结构,旨在简单、直观地处理关系型、标记型数据。 Pandas 的出…...
【计网 可靠数据传输RDT】 中科大笔记 (十 一)
目录 0 引言1 RDT的原理RDT的原理: 2 RDT的机制与作用2.1 重要协议停等协议(Stop-and-Wait):连续ARQ协议: 2.2 机制与作用实现机制:RDT的作用: 🙋♂️ 作者:海码007📜 专栏&#x…...
ubuntu下训练自己的yolov5数据集
参考文档 yolov5-github yolov5-github-训练文档 csdn训练博客 一、配置环境 1.1 安装依赖包 前往清华源官方地址 选择适合自己的版本替换自己的源 # 备份源文件 sudo cp /etc/apt/sources.list /etc/apt/sources.list_bak # 修改源文件 # 更新 sudo apt update &&a…...
ROC及曲线面积汇总学习
目录 ROC基础 生成模拟数据 率的计算 R语言计算测试 ROCR: pROC ROC绘制 单个ROC 两个ROC Logistic回归的ROC曲线 timeROC ROC基础 ROC曲线的横坐标是假阳性率,纵坐标是真阳性率,需要的结果是这个率表示疾病阳性的率(…...
【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
应用升级/灾备测试时使用guarantee 闪回点迅速回退
1.场景 应用要升级,当升级失败时,数据库回退到升级前. 要测试系统,测试完成后,数据库要回退到测试前。 相对于RMAN恢复需要很长时间, 数据库闪回只需要几分钟。 2.技术实现 数据库设置 2个db_recovery参数 创建guarantee闪回点,不需要开启数据库闪回。…...
MFC内存泄露
1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...
Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务
通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
【p2p、分布式,区块链笔记 MESH】Bluetooth蓝牙通信 BLE Mesh协议的拓扑结构 定向转发机制
目录 节点的功能承载层(GATT/Adv)局限性: 拓扑关系定向转发机制定向转发意义 CG 节点的功能 节点的功能由节点支持的特性和功能决定。所有节点都能够发送和接收网格消息。节点还可以选择支持一个或多个附加功能,如 Configuration …...
Python训练营-Day26-函数专题1:函数定义与参数
题目1:计算圆的面积 任务: 编写一个名为 calculate_circle_area 的函数,该函数接收圆的半径 radius 作为参数,并返回圆的面积。圆的面积 π * radius (可以使用 math.pi 作为 π 的值)要求:函数接收一个位置参数 radi…...
在 Visual Studio Code 中使用驭码 CodeRider 提升开发效率:以冒泡排序为例
目录 前言1 插件安装与配置1.1 安装驭码 CodeRider1.2 初始配置建议 2 示例代码:冒泡排序3 驭码 CodeRider 功能详解3.1 功能概览3.2 代码解释功能3.3 自动注释生成3.4 逻辑修改功能3.5 单元测试自动生成3.6 代码优化建议 4 驭码的实际应用建议5 常见问题与解决建议…...
2025年低延迟业务DDoS防护全攻略:高可用架构与实战方案
一、延迟敏感行业面临的DDoS攻击新挑战 2025年,金融交易、实时竞技游戏、工业物联网等低延迟业务成为DDoS攻击的首要目标。攻击呈现三大特征: AI驱动的自适应攻击:攻击流量模拟真实用户行为,差异率低至0.5%,传统规则引…...
