TECH NOTES / Java 与 Spring

Spring Boot 中的枚举使用

原笔记 2021-10-09 · 整理于 2026-10-05

让枚举参与 Spring Boot 的请求参数转换、JSON 序列化和数据库字段映射。

示例说明:代码包含请求转换、Jackson 和 MyBatis-Plus 等不同层的用法,枚举与业务类需配套使用;不是单文件即可运行的完整项目。

前后端交互,序列化与反序列化

序列化(将枚举对象序列化为json给前端)

推荐将枚举统一序列化为 json ,REST的风格接口都是序列化为json

直接在枚举类添加@JsonFormat***(***shape = JsonFormat.Shape.OBJECT) 注解即可

如下:

java
package com.leigq.www.mybatisplusstudy.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 性别枚举
 * @author leiguoqing
 */
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
@Getter
@AllArgsConstructor
public enum GenderEnum implements IEnum<Integer> {

    /**
     * 男
     */
    MALE(1, "男"),

    /**
     * 女
     */
    FEMALE(2, "女"),


    /**
     * 未知
     */
    UNKNOWN(3, "未知"),


    ;

    @EnumValue
    private final Integer value;


    private final String description;
}

返回值:

java
{
  "code": 2000,
  "msg": "请求/处理成功!",
  "data": [
    {
      "id": 1446888414960676866,
      "createdBy": 0,
      "createdTime": 1633800055000,
      "updatedBy": 0,
      "updatedTime": 1633800055000,
      "name": "jack",
      "age": 10,
      "gender": {
        "value": 2,
        "description": "女"
      },
      "workAge": 5,
      "wages": 10000.00,
      "hobbies": [
        "唱歌",
        "跳舞"
      ],
      "version": 1,
      "deleted": false,
      "did": 1
    },
    {
      "id": 1446888838371414018,
      "createdBy": 0,
      "createdTime": 1633800156000,
      "updatedBy": 0,
      "updatedTime": 1633800156000,
      "name": "jack",
      "age": 10,
      "gender": {
        "value": 2,
        "description": "女"
      },
      "workAge": 5,
      "wages": 10000.00,
      "hobbies": [
        "唱歌",
        "跳舞"
      ],
      "version": 1,
      "deleted": false,
      "did": 1
    },
    {
      "id": 1446889136909377538,
      "createdBy": 0,
      "createdTime": 1633800227000,
      "updatedBy": 0,
      "updatedTime": 1633800227000,
      "name": "jack",
      "age": 10,
      "gender": {
        "value": 2,
        "description": "女"
      },
      "workAge": 5,
      "wages": 10000.00,
      "hobbies": [
        "唱歌",
        "跳舞"
      ],
      "version": 1,
      "deleted": false,
      "did": 1
    },
    {
      "id": 1446889611947855873,
      "createdBy": 0,
      "createdTime": 1633800340000,
      "updatedBy": 0,
      "updatedTime": 1633800340000,
      "name": "jack",
      "age": 10,
      "gender": {
        "value": 2,
        "description": "女"
      },
      "workAge": 5,
      "wages": 10000.00,
      "hobbies": [
        "唱歌",
        "跳舞"
      ],
      "version": 1,
      "deleted": false,
      "did": 1
    }
  ]
}

反序列化(从前端接收枚举值反序列化为枚举对象)

需要分情况处理

普通请求

对于诸如Get请求,Post表单请求的普通请求,可以使用自定义的转换器工厂进行处理,我们需要继承ConverterFactory类并指定想要处理的类型。

自定义的转换器
java
package com.leigq.www.mybatisplusstudy.web.config;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.sun.istack.internal.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * String 类型参数转为 Enum 类型,适用 Get请求,Post表单请求的普通请求,不适用 POST json 请求
 * <pre>
 *     IEnum 是 MybatisPlus 中的自定义枚举接口,也可以自己写一个
 *     需要转换的枚举实现 com.baomidou.mybatisplus.annotation.IEnum 接口 或者 在需要转换的属性上加 @com.baomidou.mybatisplus.annotation.EnumValue 注解
 * </pre>
 * @author leiguoqing
 */
@Slf4j
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum<?>> {
    /**
     * The constant CONVERTER_MAP.
     */
    private static final Map<Class<?>, Converter<String, ? extends Enum<?>>> CONVERTER_MAP = new ConcurrentHashMap<>();

    /**
     * The constant TABLE_METHOD_OF_ENUM_TYPES.
     */
    private static final Map<Class<?>, Method> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();

    /**
     * Gets converter.
     *
     * @param <T>        the type parameter
     * @param targetType the target type
     * @return the converter
     */
    @Override
    @SuppressWarnings("unchecked cast")
    public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
        // 缓存转换器
        Converter<String, T> converter = (Converter<String, T>) CONVERTER_MAP.get(targetType);
        if (converter == null) {
            converter = new StringToEnumConverter<>(targetType);
            CONVERTER_MAP.put(targetType, converter);
        }
        return converter;
    }

    /**
     * The type String to enum converter.
     *
     * @param <T> the type parameter
     */
    static class StringToEnumConverter<T extends Enum<?>> implements Converter<String, T> {

        /**
         * The Enum map.
         */
        private final Map<String, T> enumMap = new ConcurrentHashMap<>();

        /**
         * Instantiates a new String to enum converter.
         *
         * @param enumType the enum type
         */
        StringToEnumConverter(Class<T> enumType) {
            Method method = getMethod(enumType);
            T[] enums = enumType.getEnumConstants();

            // 将值与枚举对象对应并缓存
            for (T e : enums) {
                try {
                    enumMap.put(method.invoke(e).toString(), e);
                } catch (IllegalAccessException | InvocationTargetException ex) {
                    log.error("获取枚举值错误!!! ", ex);
                }
            }
        }


        /**
         * Convert t.
         *
         * @param source the source
         * @return the t
         */
        @Override
        public T convert(@NotNull String source) {
            // 获取
            T t = enumMap.get(source);
            if (t == null) {
                throw new IllegalArgumentException("该字符串找不到对应的枚举对象 字符串:" + source);
            }
            return t;
        }
    }

    /**
     * Gets method.
     *
     * @param <T>      the type parameter
     * @param enumType the enum type
     * @return the method
     */
    public static <T> Method getMethod(Class<T> enumType) {
        Method method;
        // 找到取值的方法
        if (IEnum.class.isAssignableFrom(enumType)) {
            try {
                method = enumType.getMethod("getValue");
            } catch (NoSuchMethodException e) {
                throw new IllegalArgumentException(String.format("类:%s 找不到 getValue方法", enumType.getName()));
            }
        } else {
            method = TABLE_METHOD_OF_ENUM_TYPES.computeIfAbsent(enumType, k -> {
                Field field = dealEnumType(enumType)
                        .orElseThrow(
                                () -> new IllegalArgumentException(String.format("类:%s 找不到 EnumValue注解", enumType.getName()))
                        );
                return ReflectionUtils.findMethod(enumType, "get" + StringUtils.capitalize(field.getName()));
            });
        }
        return method;
    }

    /**
     * Deal enum type optional.
     *
     * @param clazz the clazz
     * @return the optional
     */
    private static Optional<Field> dealEnumType(Class<?> clazz) {
        return clazz.isEnum() ?
                Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst() : Optional.empty();
    }
}

使自定义的转换器工厂生效

需要实现WebMvcConfigurer接口并在addFormatters方法中进行追加。

java
package com.leigq.www.mybatisplusstudy.web.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


/**
 * MVC配置
 * <pre>
 *     有两种方式配置,推荐使用 "实现 WebMvcConfigurer 接口 " 实现, 参考: https://blog.csdn.net/cp026la/article/details/86518655
 *     <code>@SpringBootConfiguration</code>继承自 <code>@Configuration</code>,二者功能也一致,标注当前类是配置类,
 *     并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到spring容器中,并且实例名就是方法名。
 *     <code>@EnableWebMvc</code> 启用此注解,SpringBoot 关于 MVC 的自动装载会失效,交由开发者自定义
 * </pre>
 * @author leiguoqing
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    /**
     * Add formatters.
     *
     * @param registry the registry
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 前端传 String 类型,后端自动转为枚举,需要转换的枚举实现 com.baomidou.mybatisplus.annotation.IEnum 接口 或者 在需要转换的属性上加 @com.baomidou.mybatisplus.annotation.EnumValue 注解
        registry.addConverterFactory(new StringToEnumConverterFactory());
    }

}

这样就可以了,接下来处理 json 请求。

看下效果

GET请求:

如何在springboot中优雅的使用枚举配图 1

如何在springboot中优雅的使用枚举配图 2

普通POST请求:

如何在springboot中优雅的使用枚举配图 3

如何在springboot中优雅的使用枚举配图 4

POST json 请求

json请求使用RequestBody注解标记接收,处理器为项目中指定的消息转换器。在Springboot中默认为Jackson。

Jackson对枚举的默认行为为按枚举名或其所在的位置(从0开始计算),例如当传入0时获取的是枚举类中的第一个对象。

通用的解决方案,需要实现ContextualDeserializer辅助获取转换时的类信息(createContextual每个枚举类只会触发一次,之后都使用该方法返回的反序列化器处理)。

注意: 其中 StringToEnumConverterFactory.getMethod(clazz) 调用的是上面那个类中的方法

java
package com.leigq.www.mybatisplusstudy.web.config;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;

/**
 * jackson 枚举类型反序列化,从前端接收枚举值反序列化为枚举对象,适用 POST json 请求,不适用 Get请求,Post表单请求的普通请求
 * <pre>
 *     IEnum 是 MybatisPlus 中的自定义枚举接口,也可以自己写一个
 *     需要转换的枚举实现 com.baomidou.mybatisplus.annotation.IEnum 接口 或者 在需要转换的属性上加 @com.baomidou.mybatisplus.annotation.EnumValue 注解
 * </pre>
 *
 * @author leiguoqing
 */
@Slf4j
@Setter
public class JacksonEnumDeserializer extends JsonDeserializer<Enum<?>> implements ContextualDeserializer {

    /**
     * The Clazz.
     */
    private Class<?> clazz;


    /**
     * Deserialize enum.
     *
     * @param jsonParser the json parser
     * @param ctx        the ctx
     * @return the enum
     * @throws IOException             the io exception
     */
    @Override
    public Enum<?> deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
        Class<?> enumType = clazz;
        if (Objects.isNull(enumType) || !enumType.isEnum()) {
            return null;
        }
        String text = jsonParser.getText();
        Method method = StringToEnumConverterFactory.getMethod(clazz);
        Enum<?>[] enumConstants = (Enum<?>[]) enumType.getEnumConstants();

        // 将值与枚举对象对应并缓存
        for (Enum<?> e : enumConstants) {
            try {
                if (Objects.equals(method.invoke(e).toString(), text)) {
                    return e;
                }
            } catch (IllegalAccessException | InvocationTargetException ex) {
                log.error("获取枚举值错误!!! ", ex);
            }
        }
        return null;
    }

    /**
     * 为不同的枚举获取合适的解析器
     *
     * @param ctx      ctx
     * @param property property
     * @return the json deserializer
     */
    @Override
    public JsonDeserializer<Enum<?>> createContextual(DeserializationContext ctx, BeanProperty property) {
        Class<?> rawCls = ctx.getContextualType().getRawClass();
        JacksonEnumDeserializer converter = new JacksonEnumDeserializer();
        converter.setClazz(rawCls);
        return converter;
    }
}

局部配置
java
    /**
     * 性别
     * <br/>
     * {@link @JsonDeserialize}枚举类型反序列化局部配置,从前端接收枚举值反序列化为枚举对象,前端传 value 值
     */
    @JsonDeserialize(using = JacksonEnumDeserializer.class)
    private GenderEnum gender;

全局配置

全局配置了的话,就不用在枚举属性上面写 @JsonDeserialize***(using = JacksonEnumDeserializer.class)*** 注解了。

java
package com.leigq.www.mybatisplusstudy.web;

import com.fasterxml.jackson.databind.module.SimpleModule;
import com.leigq.basaltools.util.JacksonUtils;
import com.leigq.www.mybatisplusstudy.web.config.JacksonEnumDeserializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/**
 * ObjectMapper配置,在使用 ObjectMapper 的地方,方便直接注入 ObjectMapper 进行使用,
 * 但是推荐统一使用 {@link JacksonUtils} 工具类
 *
 * @author leiguoqing
 * @date 2020 -07-22 21:03:08
 */
@Configuration
public class JacksonConfig {

    /**
     * jackson 2 ObjectMapper 构建定制器
     * <br/>
     * 该段代码并未覆盖SpringBoot自动装配的ObjectMapper对象,而是加强其配置.
     * 详情请参考: <a href='https://www.jianshu.com/p/68fce8b23341'>SpringBoot2.x下的ObjectMapper配置原理</a>
     *
     * @return the jackson 2 object mapper builder customizer
     * @author leiguoqing
     * @date 2020 -07-22 21:23:18
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customJackson() {
        return jacksonObjectMapperBuilder -> {
            // 枚举反序列化全局处理,局部配置使用 @JsonDeserialize(using = JacksonEnumDeserializer.class),详见 com.leigq.www.mybatisplusstudy.param.AddStaffsParam
            // 序列化的话不做全局处理,在枚举对象上面使用 @JsonFormat(shape = JsonFormat.Shape.OBJECT) 注解即可
            SimpleModule simpleModule = new SimpleModule();
            simpleModule.addDeserializer(Enum.class, new JacksonEnumDeserializer());

            /* 注册模块 */
            jacksonObjectMapperBuilder.modules(simpleModule);
        };
    }
}

看下效果

如何在springboot中优雅的使用枚举配图 5

如何在springboot中优雅的使用枚举配图 6

数据库交互,入库与出库

借助MyBatis-Plus可以很容易的实现这一点。

首先需要在配置文件中加入type-enums-package指定枚举的扫描包,MyBatis-Plus将为包内(包含子包)所有枚举进行适配,可以使用逗号或封号分隔多个包名。

java
mybatis-plus:
  type-enums-package: [枚举包][,|;][枚举包]

接着在枚举类中指定数据库值所对应的属性。这里可以采用两种方式。

方式一:

  • 实现官方提供的IEnum接口,接口中的getValue方法与数据库值对应的属性。
java
@Getter//实现getValue
public enum StatusEnum implements IEnum<Integer> {
    VALID(1, "有效"),
    INVALID(0, "无效");

    StatusEnum(Integer value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    //标记数据库存的值是value
    private final Integer value;
    private final String desc;
}

方式二:

  • 将属性使用EnumValue注解标记数据库值对应的属性。
java
@Getter//实现getValue
public enum StatusEnum {
    VALID(1, "有效"),
    INVALID(0, "无效");

    StatusEnum(Integer value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    //标记数据库存的值是value
    @EnumValue
    private final Integer value;
    private final String desc;
}

在类的属性声明上直接将字段类型标记为枚举类型,读取时将自动转换数据库值为枚举对象。

java
@Data
@Accessors(chain = true)
@TableName("test")
public class TestDO {

    private Integer id;

    private String username;

    private StatusEnum status;
}

参考:

文章配图