前言

最近遇到一个小伙伴问前端枚举转换问题,才意识到可以通过转换器(Converter)自动将前端传入的字段值使用枚举接收。

我自己捣鼓了一番,现在记录笔记分享一下!有兴趣的小伙伴可以自己尝试一下!

这里使用的是 MyBatis-Plus 和 SpringBoot 2.3.4.RELEASE

实现过程

配置转换器

  1. /**
  2. * @author liuzhihang
  3. * @date 2021/8/31 16:29
  4. */
  5. @Configuration
  6. public class WebConfig implements WebMvcConfigurer {
  7. @Override
  8. public void addFormatters(FormatterRegistry registry) {
  9. registry.addConverterFactory(new ConverterFactory<Object, BaseEnum>() {
  10. @Override
  11. public <T extends BaseEnum> Converter<Object, T> getConverter(Class<T> targetType) {
  12. T[] enums = targetType.getEnumConstants();
  13. return source -> {
  14. for (T e : enums) {
  15. if (e.getCode().equals(source)) {
  16. return e;
  17. }
  18. }
  19. throw new IllegalArgumentException("枚举 Code 不正确");
  20. };
  21. }
  22. });
  23. }
  24. }

直接在 WebMvcConfigurer 里实现 addFormatters 方法即可,然后 new 一个 ConverterFactory。

WebMvcConfigurer 相信大家都不陌生,一般添加一些拦截器,通用校验 token、日志等等都会用到。具体可以参考这篇文章:几行代码轻松实现跨系统传递 traceId,再也不用担心对不上日志了!,里面有一些其他的应用。

就这些,很简单的实现。下面介绍下项目的内容和代码,方便理解。

项目代码

  • 请求参数:
  1. POST http://localhost:8818/user/listByStatus
  2. Content-Type: application/json
  3. {
  4. "orderStatus": 1
  5. }
  • Controller
  1. /**
  2. * @author liuzhihang
  3. * @date 2021/8/30 11:08
  4. */
  5. @Slf4j
  6. @RestController
  7. @RequestMapping("/user")
  8. public class UserController {
  9. @Autowired
  10. private OrderService orderService;
  11. @PostMapping(value = "/listByStatus")
  12. public ResultVO<UserResponse> listByStatus(@Validated @RequestBody UserRequest request) {
  13. log.info("请求参数:{}", request);
  14. List<TransOrder> orderList = orderService.getByOrderStatus(request.getOrderStatus());
  15. UserResponse response = new UserResponse();
  16. response.setRecords(orderList);
  17. log.info("返回参数:{}", response);
  18. return ResultVO.success(response);
  19. }
  20. }
  • Entity
  1. @Data
  2. public class UserRequest {
  3. private OrderStatusEnum orderStatus;
  4. private ViewStatusEnum viewStatus;
  5. }
  6. @Data
  7. public class UserResponse {
  8. private List<TransOrder> records;
  9. }

Web 传入 orderStatus 为 1,而后端接收对象是 UserRequest 的 orderStatus 字段是个 OrderStatusEnum 类型的枚举。

这里就需要自动将数字类型的字段转换为枚举字段。这个枚举会直接通过 MyBatis-Plus 查询。

为什么要这么用呢?

其实原因很简单,使用枚举限制数据库字段的类型,比如数据库状态只有 0、1、2,那就和代码里的枚举对应起来。防止传入其他值。

  • 枚举
  1. public interface BaseEnum {
  2. Object getCode();
  3. }
  1. public enum OrderStatusEnum implements BaseEnum {
  2. INIT(0, "初始状态"),
  3. SUCCESS(1, "成功"),
  4. FAIL(2, "失败");
  5. @EnumValue
  6. @JsonValue
  7. private final int code;
  8. private final String desc;
  9. OrderStatusEnum(int code, String desc) {
  10. this.code = code;
  11. this.desc = desc;
  12. }
  13. @Override
  14. public Integer getCode() {
  15. return code;
  16. }
  17. public String getDesc() {
  18. return desc;
  19. }
  20. }

这里先声明接口 BaseEnum,所有的枚举都继承这个接口,并实现 getCode 方法。

@EnumValue:MyBatis-Plus 的枚举,和数据库字段映射用的

@JsonValue:返回给前端时,这个枚举字段序列化时,返回参数只显示 code。

这样就可以实现效果,请求参数为数字,接收对象字段为枚举,返回字段也是 code。

效果

使用 SpringBoot 转换器将前端参数转换为枚举 - 图1

测试结果经过验证,是可以胜任传入数值和字符串的。

也可以结合异常处理器,返回通用异常。具体怎么用查一查 @ExceptionHandler 就知道了。

具体说明

在 addFormatters 方法中可以看到 registry.addConverterFactory() 接收的是一个 ConverterFactory 对象。

  1. public interface ConverterFactory<S, R> {
  2. <T extends R> Converter<S, T> getConverter(Class<T> targetType);
  3. }
  1. S 就是传入的字段类型(数字,字符串)
  2. R 是要转换为的类型(枚举)
  3. T 继承了 R,其实就是参数对象中字段的类型

在 ConverterFactory 的 getConverter 方法则需要返回一个实际的转换器 Converter

  1. @FunctionalInterface
  2. public interface Converter<S, T> {
  3. @Nullable
  4. T convert(S source);
  5. }

convert 方法的入参是一个 source,就是要转换为什么类型的,这里就是数字/字符串,然后返回一个枚举即可。

注意这里加了 @FunctionalInterface 就意味着这里是可以用 lambda 表达式的。

优化

一般 WebConfig 中除了实现 addFormatters 方法外,还会实现 addInterceptors 等等,这样写难免会很长,所以可以改为下面这种。

  1. @Configuration
  2. public class WebConfig implements WebMvcConfigurer {
  3. @Autowired
  4. private LogInterceptor logInterceptor;
  5. @Autowired
  6. private AppTokenInterceptor appTokenInterceptor;
  7. @Autowired
  8. private EnumConverterFactory enumConverterFactory;
  9. @Override
  10. public void addInterceptors(InterceptorRegistry registry) {
  11. // 日志
  12. registry.addInterceptor(logInterceptor)
  13. .addPathPatterns("/**");
  14. // app token校验
  15. registry.addInterceptor(appTokenInterceptor)
  16. .addPathPatterns("/app/**");
  17. }
  18. @Override
  19. public void addFormatters(FormatterRegistry registry) {
  20. // 枚举转换
  21. registry.addConverterFactory(enumConverterFactory);
  22. }
  23. }

这种就需要咱们创建 EnumConverterFactory 类并实现 ConverterFactory 接口了,还得注入到 Spring 容器中

  1. @Component
  2. public class EnumConverterFactory implements ConverterFactory<Object, BaseEnum> {
  3. @Override
  4. public <T extends BaseEnum> Converter<Object, T> getConverter(Class<T> targetType) {
  5. T[] enums = targetType.getEnumConstants();
  6. return source -> {
  7. for (T e : enums) {
  8. if (e.getCode().equals(source)) {
  9. return e;
  10. }
  11. }
  12. throw new IllegalArgumentException("枚举 Code 不正确");
  13. };
  14. }
  15. }

要是实在觉得 lambda 看不惯,并且也不够优雅,那可以使用下面的方式。

  1. @Component
  2. public class EnumConverterFactory implements ConverterFactory<Object, BaseEnum> {
  3. @Override
  4. public <T extends BaseEnum> Converter<Object, T> getConverter(Class<T> targetType) {
  5. return new EnumConverter<>(targetType);
  6. }
  7. }
  8. public class EnumConverter<T extends BaseEnum> implements Converter<Object, T> {
  9. private final Class<T> targetType;
  10. public EnumConverter(Class<T> targetType) {
  11. this.targetType = targetType;
  12. }
  13. @Override
  14. public T convert(Object source) {
  15. for (T e : targetType.getEnumConstants()) {
  16. if (e.getCode().equals(source)) {
  17. return e;
  18. }
  19. }
  20. throw new IllegalArgumentException("枚举 Code 不正确");
  21. }
  22. }

总结

当然这里也有一些其他的优化点,比如可以使用缓存将 Convert 缓存起来。

不过我也遇到一个其他的问题,就是我 debug 断点竟然一直没有断到转换器中,不知道有没有小伙伴尝试过?