如何处理枚举类型(下)

news2024/11/26 15:01:57
作者简介:大家好,我是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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

百面深度学习-自然语言处理

自然语言处理 神经机器翻译模型经历了哪些主要的结构变化&#xff1f;分别解决了哪些问题&#xff1f; 神经机器翻译&#xff08;Neural Machine Translation, NMT&#xff09;是一种使用深度学习技术来实现自动翻译的方法。自从提出以来&#xff0c;NMT模型经历了几个重要的…

Couchdb 命令执行漏洞复现 (CVE-2017-12636)

Couchdb 命令执行漏洞复现 &#xff08;CVE-2017-12636&#xff09; 1、下载couchdb.py 2、修改目标和反弹地址 3、Python3调用执行即可 couchdb.py文件下载地址: https://github.com/vulhub/vulhub/blob/master/couchdb/CVE-2017-12636/exp.py ‍ 在VULFocus上开启环境 …

指针(1)

指针基本概念&#xff1a; 1.指针变量是一个变量&#xff0c;用来存放地址&#xff0c;地址唯一标识一块地址空间。 2.指针的大小固定是4/8个字节&#xff08;32位平台/64位平台&#xff09;。 3.指针有类型&#xff0c;指针的类型决定-整数的步长&#xff0c;以及解引用时的…

数据分析项目实战!Python分析员工为何离职

大家好&#xff0c;我是小F&#xff5e; 今天给大家介绍一个Python数据分析项目实战&#xff0c;不仅包含代码&#xff0c;还提供分析数据集。 员工流失或是员工离开公司的比率是公司关注的一个重要问题。它不仅会导致宝贵人才的流失&#xff0c;还会产生成本并破坏生产力。了解…

Nginx(资源压缩)

建立在动静分离的基础之上&#xff0c;如果一个静态资源的Size越小&#xff0c;那么自然传输速度会更快&#xff0c;同时也会更节省带宽&#xff0c;因此我们在部署项目时&#xff0c;也可以通过Nginx对于静态资源实现压缩传输&#xff0c;一方面可以节省带宽资源&#xff0c;第…

vim+xxd编辑十六进制的一个大坑:自动添加0x0a

问题描述 今天在做一个ctf题&#xff0c;它给了一个elf文件&#xff0c;我要做的事情是修复这个elf文件&#xff0c;最后执行它&#xff0c;这个可执行文件会计算它自身的md5作为这道题的flag。我把所有需要修复的地方都修复了&#xff0c;程序也能成功运行&#xff0c;但是fl…

打包SpringBoot 项目为本地应用

使用工具&#xff1a;exe4j、Inno Setup Compiler 步骤&#xff1a; 1&#xff0c;将dll包放入项目根路径下&#xff1b; 2&#xff0c;idea 使用Maven打jar包&#xff1b; 3&#xff0c;使用exe4j 工具进行打包&#xff1b; 打开工具首页不动&#xff08;直接 next&#xff…

论文阅读:C2VIR-SLAM: Centralized Collaborative Visual-Inertial-Range SLAM

前言 论文全程为C2VIR-SLAM: Centralized Collaborative Visual-Inertial-Range Simultaneous Localization and Mapping&#xff0c;是发表在MDPI drones&#xff08;二区&#xff0c;IF4.8&#xff09;上的一篇论文。这篇文章使用单目相机、惯性测量单元( IMU )和UWB设备作为…

【Amazon】创建Amazon EFS 文件系统并将其挂载到Amazon EC2实例

文章目录 1. Amazon EFS文件系统2. Amazon EFS文件系统工作原理图3. 创建Amazon EFS 文件系统操作步骤3.1 创建安全组3.2 创建 EFS 文件系统3.3 启动 EC2 实例并挂载文件系统 4.清理资源4.1 终止 EC2 实例4.2 删除 EFS 文件系统 5.参考链接 1. Amazon EFS文件系统 Amazon EFS …

水面倒影可视化渲染方法

水面材质在三维可视化场景中的使用非常广泛。水面材质非常重要的一个光学特性就是反射倒影&#xff0c;有了倒影的加持能使水面更加逼真的渲染出来。本文主要讨论水面材质中倒影的渲染方法。 要有倒影&#xff0c;必须先有水面&#xff0c;第一步要做的就是确定水面所在的平面…

关于DCDC电源中的PWM与PFM

在开关电源DCDC中&#xff0c;我们经常会听到PWM模式与PFM模式。 关于&#xff0c;这两种模式&#xff0c;小编在之前的文章中&#xff0c;做过简单的描述。今天就来针对性的就这两种模式展开讲讲。 PWM&#xff1a;脉冲宽度调制&#xff0c;即频率不变&#xff0c;不断调整脉…

静态路由配置过程

静态路由 静态路由简介 路由器在转发数据时&#xff0c;要先在路由表&#xff08;Routing Table&#xff09;中在找相应的路由&#xff0c;才能知道数据包应该从哪个端口转发出去。路由器建立路由表基本上有以下三种途径。 &#xff08;1&#xff09;直连路由&#xff1a;路由…

pytorch中的激活函数详解

1 激活函数介绍 1.1 什么是激活函数 激活函数是神经网络中引入的非线性函数&#xff0c;用于捕获数据中的复杂关系。它来自动物界的灵感&#xff0c;动物的神经元会接受来自对它有作用的其他神经元的信号&#xff0c;当然这些信号对该神经元的作用大小不同&#xff08;即具有不…

最火web大屏可视化编辑器

前言&#xff1a; 乐吾乐Le5le大屏可视化设计器&#xff0c;零代码实现物联网、工业智能制造等领域的可视化大屏、触摸屏端UI以及工控可视化的解决方案。同时也是一个Web组态工具&#xff0c;支持2D、3D等多种形式&#xff0c;用于构建具有实时数据展示、监控预警、丰富交互的组…

【nowcoder】BM4 合并两个排序的链表

题目&#xff1a; 题目分析&#xff1a; 题目分析转载 代码实现&#xff1a; package BMP4;import java.util.List;class ListNode {int val;ListNode next null;public ListNode(int val) {this.val val;} } public class BM4 {/*** 代码中的类名、方法名、参数名已经指定…

rust tokio select!宏详解

rust tokio select!宏详解 简介 本文介绍Tokio中select!的用法&#xff0c;重点是使用过程中可能遇到的问题&#xff0c;比如阻塞问题、优先级问题、cancel safe问题。在Tokio 中&#xff0c;select! 是一个宏&#xff0c;用于同时等待多个异步任务&#xff0c;并在其中任意一…

jenkins流水线(pipline)实例

1、pipline 语法介绍 声明式的pipeline语法格式 1. 所有的声明都必须包含在pipeline{}中 2. 块只能有节段&#xff0c;指令&#xff0c;步骤或者赋值语句组成 3. 阶段&#xff1a;agent&#xff0c;stages&#xff0c;post&#xff0c;steps 4. 指令&#xff1a;environment&a…

独乐乐不如众乐乐(二)-某汽车零部件厂商IC EMC企业规范

前言&#xff1a;该汽车零部件厂商关于IC EMC的规范可能是小编看过的企业标准里要求最明确的一份企业标准了&#xff0c;充分说明了标准方法不是死的&#xff0c;可以灵活应用。 先看看这份规范的抬头&#xff1a; 与其他企业规范一样&#xff0c;该汽车零部件厂商的IC EMC规范…

设计模式精讲:掌握单例模式的实现与优化

掌握单例模式的实现与优化 一、引言&#xff1a;如何学习设计模式&#xff1f;二、前置知识&#xff1a;对象的创建的销毁2.1、拷贝构造2.2、拷贝赋值构造2.3、移动构造2.4、移动赋值构造 三、单例模式的定义四、单例模式的实现与优化4.1、版本一4.2、版本二4.3、版本三4.4、版…

Java PriorityQueue

一般情况下, 我们使用队列是为了能够建造队列的先进先出 (First-In-First-Out) 模式的, 达到一种资源的公平分配, 先到达的任务 (元素) 先处理, 但有时需要在队列中基于优先级处理对象。 存入队列中的任务 (元素) 具有优先级, 需要根据优先级修复里面的数据。而在 JDK 1.5 引入…