Spring Boot使用枚举类型作为请求参数

1. 场景

在我们实际开发中,枚举类型应用十分广泛,可以避免在项目中定义大量的『魔法值』变量。但是,在 web 开发中,如何将枚举对象作为请求参数传进 Controller,做到类型自动转换?直接使用 @RequestParam@RequestBody 断然是不够的,这里就需要我们自定义 Converter 来实现类型转化了。

2. 需求

比如一个用户对象,里面的性别属性,我们定义一个枚举类型 Gender

GenderEnum.java

/**
 * <p>
 * 性别枚举
 * </p>
 *
 * @package: com.xkcoding.springboottest.constants.enums
 * @description: 性别枚举
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:06
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
@Getter
public enum GenderEnum {
    /**
     * 男
     */
    MALE(0),
    /**
     * 女
     */
    FEMALE(1);

    /**
     * 性别编码
     */
    private Integer code;

    GenderEnum(int code) {
        this.code = code;
    }

}

请求对象封装 QueryRequest

QueryRequest.java

/**
 * <p>
 * 请求参数
 * </p>
 *
 * @package: com.xkcoding.springboottest.controller.enum_test
 * @description: 请求参数
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 14:02
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
@Data
public class QueryRequest {
    private GenderEnum gender;
}

在 Controller 层直接使用如下方式,期望参数自动进行类型转换

EnumTestController.java

/**
 * <p>
 * 测试参数是枚举类型,自动转换
 * </p>
 *
 * @package: com.xkcoding.springboottest.controller.enum_test
 * @description: 测试参数是枚举类型,自动转换
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 13:56
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
@Slf4j
@RestController
@RequestMapping("/enum")
public class EnumTestController {

    @GetMapping("/get")
    public Dict testGet(QueryRequest request) {
        log.info("【get-request】= {}", JSONUtil.toJsonStr(request));
        return Dict.create().set("get-request", request);
    }

    @PostMapping("/post")
    public Dict testPost(@RequestBody QueryRequest request) {
        log.info("【post-request】= {}", JSONUtil.toJsonStr(request));
        return Dict.create().set("post-request", request);
    }

}

这么写的时候,gender 只能接收到 MALEFEMALE 这样的参数,除此以外,均会报类型不匹配的错误信息,此时是无法处理 01 这样的参数的。

需求:

  1. 接收到 MALEFEMALE 这样的参数,可以自动转为对应的枚举值;
  2. 接收到 01 这样的参数,也可以自动转为对应的枚举值。

3. 解决

此时,本文的『主角』Converter 就可以隆重登场了。

注意,Converterorg.springframework.core.convert.converter.Converter ,别导错包了。

IntegerCodeToGenderEnumConverter.java

/**
 * <p>
 * 编码 -> 性别 转换器
 * </p>
 *
 * @package: com.xkcoding.springboottest.config.convert
 * @description: 编码 -> 性别 转换器
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:14
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
public class IntegerCodeToGenderEnumConverter implements Converter<Integer, GenderEnum> {
    private Map<Integer, GenderEnum> enumMap = Maps.newHashMap();

    public IntegerCodeToGenderEnumConverter() {
        for (GenderEnum genderEnum : GenderEnum.values()) {
            enumMap.put(genderEnum.getCode(), genderEnum);
        }
    }

    @Override
    public GenderEnum convert(Integer source) {
        GenderEnum genderEnum = enumMap.get(source);
        if (ObjectUtil.isNull(genderEnum)) {
            throw new IllegalArgumentException("无法匹配对应的枚举类型");
        }
        return genderEnum;
    }
}

其实这里已经可以实现类型转换了,但是引出另外一个问题,当我们的枚举类特别多的时候,我们就需要写很多个 自定义的 Converter 来满足类型转化。

所以我们不使用上面直接使用 Converter 的这种方法,我们引入 ConverterFactory 来解决这个问题。

3.1. 抽取公共枚举接口

以后的枚举类都需要实现这个接口

BaseEnum.java

/**
 * <p>
 * 通用枚举接口
 * </p>
 *
 * @package: com.xkcoding.springboottest.constants
 * @description: 通用枚举接口
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:04
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
public interface BaseEnum {
    /**
     * 获取枚举编码
     *
     * @return 编码
     */
    Integer getCode();
}

3.2. 调整 GenderEnum 枚举类

GenderEnum.java

/**
 * <p>
 * 性别枚举
 * </p>
 *
 * @package: com.xkcoding.springboottest.constants.enums
 * @description: 性别枚举
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:06
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
@Getter
public enum GenderEnum implements BaseEnum {
    /**
     * 男
     */
    MALE(0),
    /**
     * 女
     */
    FEMALE(1);

    /**
     * 性别编码
     */
    private Integer code;

    GenderEnum(int code) {
        this.code = code;
    }

}

3.3. 创建通用 Integer -> Enum 的 Converter 类

IntegerToEnumConverter.java

/**
 * <p>
 * 枚举编码 -> 枚举 转化器
 * </p>
 *
 * @package: com.xkcoding.springboottest.config.convert
 * @description: 枚举编码 -> 枚举 转化器
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:21
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
public class IntegerToEnumConverter<T extends BaseEnum> implements Converter<Integer, T> {
    private Map<Integer, T> enumMap = Maps.newHashMap();

    public IntegerToEnumConverter(Class<T> enumType) {
        T[] enums = enumType.getEnumConstants();
        for (T e : enums) {
            enumMap.put(e.getCode(), e);
        }
    }

    @Override
    public T convert(Integer source) {
        T t = enumMap.get(source);
        if (ObjectUtil.isNull(t)) {
            throw new IllegalArgumentException("无法匹配对应的枚举类型");
        }
        return t;
    }
}

3.4. 创建对应的自定义 ConverterFactory 工厂类

IntegerCodeToEnumConverterFactory.java

/**
 * <p>
 * 编码 -> 枚举 转化器工厂类
 * </p>
 *
 * @package: com.xkcoding.springboottest.config.convert.factory
 * @description: 编码 -> 枚举 转化器工厂类
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:09
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
public class IntegerCodeToEnumConverterFactory implements ConverterFactory<Integer, BaseEnum> {
    private static final Map<Class, Converter> CONVERTERS = Maps.newHashMap();

    /**
     * 获取一个从 Integer 转化为 T 的转换器,T 是一个泛型,有多个实现
     *
     * @param targetType 转换后的类型
     * @return 返回一个转化器
     */
    @Override
    public <T extends BaseEnum> Converter<Integer, T> getConverter(Class<T> targetType) {
        Converter<Integer, T> converter = CONVERTERS.get(targetType);
        if (converter == null) {
            converter = new IntegerToEnumConverter<>(targetType);
            CONVERTERS.put(targetType, converter);
        }
        return converter;
    }
}

3.5. 创建通用 String -> Enum 的 Converter 类和对应的 ConverterFactory 工厂类

因为 Post 请求可以对传入的 json 属性定义类型,但是 Get 请求后台接收到的参数都是 String 类型,因此需要在创建一个 Converter 类

StringToEnumConverter.java

/**
 * <p>
 * 枚举编码 -> 枚举 转化器
 * </p>
 *
 * @package: com.xkcoding.springboottest.config.convert
 * @description: 枚举编码 -> 枚举 转化器
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:21
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
public class StringToEnumConverter<T extends BaseEnum> implements Converter<String, T> {
    private Map<String, T> enumMap = Maps.newHashMap();

    public StringToEnumConverter(Class<T> enumType) {
        T[] enums = enumType.getEnumConstants();
        for (T e : enums) {
            enumMap.put(e.getCode().toString(), e);
        }
    }

    @Override
    public T convert(String source) {
        T t = enumMap.get(source);
        if (ObjectUtil.isNull(t)) {
            throw new IllegalArgumentException("无法匹配对应的枚举类型");
        }
        return t;
    }
}

StringCodeToEnumConverterFactory.java

/**
 * <p>
 * 编码 -> 枚举 转化器工厂类
 * </p>
 *
 * @package: com.xkcoding.springboottest.config.convert.factory
 * @description: 编码 -> 枚举 转化器工厂类
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:09
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
public class StringCodeToEnumConverterFactory implements ConverterFactory<String, BaseEnum> {
    private static final Map<Class, Converter> CONVERTERS = Maps.newHashMap();

    /**
     * 获取一个从 Integer 转化为 T 的转换器,T 是一个泛型,有多个实现
     *
     * @param targetType 转换后的类型
     * @return 返回一个转化器
     */
    @Override
    public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) {
        Converter<String, T> converter = CONVERTERS.get(targetType);
        if (converter == null) {
            converter = new StringToEnumConverter<>(targetType);
            CONVERTERS.put(targetType, converter);
        }
        return converter;
    }
}

3.6. 将转化器工厂添加进 Spring Boot 配置

WebMvcConfig.java

/**
 * <p>
 * MVC通用配置
 * </p>
 *
 * @package: com.xkcoding.springboottest.config
 * @description: MVC通用配置
 * @author: yangkai.shen
 * @date: Created in 2019-01-30 11:33
 * @copyright: Copyright (c) 2019
 * @version: V1.0
 * @modified: yangkai.shen
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 枚举类的转换器工厂 addConverterFactory
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(new IntegerCodeToEnumConverterFactory());
        registry.addConverterFactory(new StringCodeToEnumConverterFactory());
    }
}

这样我们就可以优雅的实现枚举对象参数的自动转换了,并且支持多个类型转换。其中的类型转换失败异常,可以使用全局异常拦截来处理。 @CodingDiary

Comments: 2

「人生在世,留句话给我吧」

提交评论