如何处理枚举类型(下)

作者简介:大家好,我是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> {

    @Override
    public <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
         */
        @Override
        public 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 {

    @Override
    public 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
     */
    @Override
    public String getValue() {
        return this.desc;
    }
}

改写MyEnumConverterFactory:

/**
 * 自定义枚举转换器,配合统一枚举接口IEnum
 *
 * @author mx
 */
public final class MyEnumConverterFactory implements ConverterFactory<String, IEnum> {

    @Override
    public <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;
        }
        
        @Override
        public 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> {

    @Override
    public <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;
        }

        @Override
        public 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);
                            // 读取对应的字段value
                            Object fieldValue = declaredField.get(enumObject);
                            // 匹配并返回对于的Enum
                            if (source.equals(String.valueOf(fieldValue))) {
                                return enumObject;
                            }
                        }
                    }
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return null;
        }
    }

}

在UserTypeEnum中使用:

@Getter
public enum UserTypeEnum {

    STUDENT(1, "学生"),
    TEACHER(2, "老师"),
    ;

    @MyJsonCreator
    private 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指定序列化字段,后面再介绍,不用管
     */
    @JsonValue
    private final Integer type;
    private final String desc;

    UserTypeEnum(Integer type, String desc) {
        this.type = type;
        this.desc = desc;
    }

    /**
     * 静态方法+@JsonCreator指定根据哪个字段反序列化
     *
     * @param desc
     * @return
     */
    @JsonCreator
    public 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;
    }

    @Override
    public 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表单请求根据哪个字段反序列化
     */
    @MyJsonCreator
    private final String desc;

    UserTypeEnum(Integer type, String desc) {
        this.type = type;
        this.desc = desc;
    }

    /**
     * 静态方法+@JsonCreator指定POST JSON请求根据哪个字段反序列化
     *
     * @param desc
     * @return
     */
    @JsonCreator
    public 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
     */
    @Override
    public String toString() {
        return String.valueOf(this.type);
    }
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    /**
     * 自定义GET/POST表单提交方式的入参反序列化规则
     *
     * @param registry
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 把我们自定义的枚举转换器添加到Spring容器,Spring容器会把它加入到SpringMVC的拦截链路中
        registry.addConverterFactory(new MyEnumConverterFactory());
    }

    /**
     * 自定义JSON响应时枚举字段的序列化行为:调用toString()
     *
     * @return
     */
    @Bean
    public 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> {

    @Override
    public <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;
        }

        @Override
        public 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);
                            // 读取对应的字段value
                            Object fieldValue = declaredField.get(enumObject);
                            // 匹配并返回对于的Enum
                            if (source.equals(String.valueOf(fieldValue))) {
                                return enumObject;
                            }
                        }
                    }
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            return null;
        }
    }

}

我个人实际开发时无论是Controller层还是DAO层,都习惯手动转换枚举。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/191825.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue3使用TinyMCE富文本

TinyMCE 介绍 TinyMCE 是一个功能强大的富文本编辑器&#xff0c;它允许您在网页应用程序中创建和编辑具有丰富格式的内容。官网 github项目地址 文档地址 下载tinymce文件 从网页下载最新版zip&#xff0c;也可以打开下面链接下载。 打开网页 tinymce.zip zh-Hans 将下载…

基于PLC的果园灌溉系统设计(论文+源码)

1.系统设计 系统示意图如图2-1所示。某一果园 共有3个灌溉区域&#xff0c;分别为灌溉1#区&#xff0c;灌溉2#区&#xff0c;灌溉3#区&#xff0c;分别使用不同湿度传感器检测湿度&#xff0c;用于各区域控制湿度&#xff0c;进行灌溉&#xff0c;使用相应的灌溉阀进行灌溉。这…

Mybatis学习笔记-映射文件,标签,插件,万字长文,史上最全

目录 概述 mybatis做了什么 原生JDBC存在什么问题 MyBatis组成部分 Mybatis工作原理 mybatis和hibernate区别 使用mybatis&#xff08;springboot&#xff09; mybatis核心-sql映射文件 基础标签说明 1.namespace&#xff0c;命名空间 2.select&#xff0c;insert&a…

AI智能人机对话小程序系统源码 附带完整的搭建教程

移动互联网的普及和快速发展&#xff0c;小程序已经成为了一种非常流行的应用形态。小程序具有即用即走、轻量级的特点&#xff0c;非常适合用于提供各种便捷服务。下面罗峰来给大家分享一款AI智能人机对话小程序系统源码&#xff0c;带有完整的搭建教程。 以下是部分代码示例…

C++学习之路(六)C++ 实现简单的工具箱系统命令行应用 - 示例代码拆分讲解

简单的工具箱系统示例介绍: 这个示例展示了一个简单的工具箱框架&#xff0c;它涉及了几个关键概念和知识点&#xff1a; 面向对象编程 (OOP)&#xff1a;使用了类和继承的概念。Tool 是一个纯虚类&#xff0c;CalculatorTool 和 FileReaderTool 是其派生类。 多态&#xff1…

【非监督学习 | 聚类】聚类算法类别大全 距离度量单位大全

&#x1f935;‍♂️ 个人主页: AI_magician &#x1f4e1;主页地址&#xff1a; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 &#x1f468;‍&#x1f4bb;景愿&#xff1a;旨在于能和更多的热爱计算机的伙伴一起成长&#xff01;&#xff01;&…

Elastic Search的RestFul API入门:初识mapping

本节课旨在探讨Elasticsearch中Mapping的使用。在Elasticsearch中&#xff0c;Mapping是定义索引中字段&#xff08;Field&#xff09;的数据类型和属性的过程。它为Elasticsearch提供了一种途径&#xff0c;以解析和处理文档中的各个字段&#xff0c;以便在搜索、排序和聚合等…

kali系统复现环境:Vulfocus 提示服务器内部错误,请联系管理员的解决方法

Linux-kali系统复现环境&#xff1a;Vulfocus&&提示服务器内部错误&#xff0c;请练习管理员的解决方法 第一步&#xff1a; 先下载docker和docker-compose apt-get update apt-get install docker apt-get install docker-compose输入如下图命令&#xff0c;有版本…

【ceph】如何打印一个osd的op流程,排查osd在干什么

本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》&#xff1a;python零基础入门学习 《python运维脚本》&#xff1a; python运维脚本实践 《shell》&#xff1a;shell学习 《terraform》持续更新中&#xff1a;terraform_Aws学习零基础入门到最佳实战 《k8…

2023年【通信安全员ABC证】考试题及通信安全员ABC证证考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年【通信安全员ABC证】考试题及通信安全员ABC证证考试&#xff0c;包含通信安全员ABC证考试题答案和解析及通信安全员ABC证证考试练习。安全生产模拟考试一点通结合国家通信安全员ABC证考试最新大纲及通信安全员A…

免费分享一套基于springboot的餐饮美食分享平台系统,挺漂亮的

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的基于springboot的餐饮美食分享平台系统&#xff0c;分享下哈。 项目视频演示 【免费】基于springboot的餐饮美食分享平台 Java毕业设计_哔哩哔哩_bilibili【免费】基于springboot的餐饮美食分享平台 Java毕…

BetaFlight模块设计之三十六:SoftSerial

BetaFlight模块设计之三十六&#xff1a;SoftSerial 1. 源由2. API接口2.1 openSoftSerial2.2 onSerialRxPinChange2.3 onSerialTimerOverflow2.4 processTxState2.5 processRxState 3. 辅助函数3.1 applyChangedBits3.2 extractAndStoreRxByte3.3 prepareForNextRxByte 4. 总结…

美食网站基本结构

代码&#xff1a; <!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>美食网站首页</title> <link rel"stylesheet" href"https://cdn.staticfile.org/layui/2.5.6/css/layui.min.c…

基于OGG实现MySQL实时同步

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

Linux常用命令——bc命令

在线Linux命令查询工具 bc 算术操作精密运算工具 补充说明 bc命令是一种支持任意精度的交互执行的计算器语言。bash内置了对整数四则运算的支持&#xff0c;但是并不支持浮点运算&#xff0c;而bc命令可以很方便的进行浮点运算&#xff0c;当然整数运算也不再话下。 语法 …

MATLAB中corrcoef函数用法

目录 语法 说明 示例 矩阵的随机列 两个随机变量 矩阵的 P 值 相关性边界 NaN 值 corrcoef函数的功能是返回数据的相关系数。 语法 R corrcoef(A) R corrcoef(A,B) [R,P] corrcoef(___) [R,P,RL,RU] corrcoef(___) ___ corrcoef(___,Name,Value) 说明 R corrc…

RH2288H V3服务器使用ISO安装系统

1.配置和服务器相同网段地址&#xff0c;RH2288H V3服务器bmc管理网口默认IP是192.168.2.100/24&#xff0c;默认用户root&#xff0c;默认Huawei12#$&#xff0c;网线连接BMC口&#xff0c;登录。默认密码可以在开机时按del键进入配置页面修改 2.配置raid&#xff0c;生产环境…

【Java+SQL Server】前后端连接小白教程

目录 &#x1f4cb; 流程总览 ⛳️【SQL Server】数据库操作 1. 新建数据库text 2. 新建表 3. 编辑表 ⛳️【IntelliJ IDEA】操作 1. 导入jar包 2. 运行显示错误 &#x1f4cb; 流程总览 ⛳️【SQL Server】数据库操作 打开SQL Server数据库-->sa登录-->新建数据库…

《Effective Modern C++》全书内容提炼总结

个人博客地址: https://cxx001.gitee.io 前言 C程序员都应该是对性能执着的人&#xff0c;想要彻底理解C11和C14&#xff0c;不可止步于熟悉它们引入的语言特性&#xff08;例如&#xff0c;auto型别推导、移动语义、lambda表达式&#xff0c;以及并发支持&#xff09;。挑战在…

geemap学习笔记014:加载本地的tif文件

前言 Colab中似乎没法直接加载云盘中的数据&#xff0c;但是可以先上传到GEE中的assets中&#xff0c;再加载本地的数据。下面是以这个数据为例进行展示。 1 上传数据 首先将本地的tif数据上传到Asset中&#xff0c;得到独一的Image ID。 2 加载数据 使用ee.Image加载数据 …