【问题标题】:Spring's @RequestParam with EnumSpring 的 @RequestParam 与 Enum
【发布时间】:2017-02-08 00:33:06
【问题描述】:

我有这个枚举:

public enum SortEnum {
    asc, desc;
}

我想用作休息请求的参数:

@RequestMapping(value = "/events", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public List<Event> getEvents(@RequestParam(name = "sort", required = false) SortEnum sort) {

当我发送这些请求时它工作正常

/events 
/events?sort=asc
/events?sort=desc

但是当我发送时:

/events?sort=somethingElse

我在控制台中收到 500 响应和这条消息:

2016-09-29 17:20:51.600 DEBUG 5104 --- [  XNIO-2 task-6] com.myApp.aop.logging.LoggingAspect   : Enter: com.myApp.web.rest.errors.ExceptionTranslator.processRuntimeException() with argument[s] = [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type [java.lang.String] to required type [com.myApp.common.SortEnum]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam com.myApp.common.SortEnum] for value 'somethingElse'; nested exception is java.lang.IllegalArgumentException: No enum constant com.myApp.common.SortEnum.somethingElse]
2016-09-29 17:20:51.600 DEBUG 5104 --- [  XNIO-2 task-6] com.myApp.aop.logging.LoggingAspect   : Exit: com.myApp.web.rest.errors.ExceptionTranslator.processRuntimeException() with result = <500 Internal Server Error,com.myApp.web.rest.errors.ErrorVM@1e3343c9,{}>
2016-09-29 17:20:51.601  WARN 5104 --- [  XNIO-2 task-6] .m.m.a.ExceptionHandlerExceptionResolver : Resolved exception caused by Handler execution: org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type [java.lang.String] to required type [com.myApp.common.SortEnum]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam com.myApp.common.SortEnum] for value 'somethingElse'; nested exception is java.lang.IllegalArgumentException: No enum constant com.myApp.common.SortEnum.somethingElse

有没有办法防止spring抛出这些异常并将枚举设置为null?

编辑

Strelok 接受的答案有效。但是,我决定处理 MethodArgumentTypeMismatchException。

@ControllerAdvice
public class ExceptionTranslator {

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseBody
    public ResponseEntity<Object> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        Class<?> type = e.getRequiredType();
        String message;
        if(type.isEnum()){
            message = "The parameter " + e.getName() + " must have a value among : " + StringUtils.join(type.getEnumConstants(), ", ");
        }
        else{
            message = "The parameter " + e.getName() + " must be of type " + type.getTypeName();
        }
        return buildResponse(HttpStatus.UNPROCESSABLE_ENTITY, message);
    }

【问题讨论】:

  • 我检查了 422 的含义,它说:“请求实体的语法是正确的”,如果字符串与枚举不匹配,我认为不是这种情况。
  • 相应的错误代码应该是 400 ("Bad Request")

标签: java spring spring-boot spring-web


【解决方案1】:

您可以创建一个自定义转换器,当提供无效值时将返回null 而不是异常。

类似这样的:

@Configuration
public class MyConfig extends WebMvcConfigurationSupport {
   @Override
   public FormattingConversionService mvcConversionService() {
       FormattingConversionService f = super.mvcConversionService();
       f.addConverter(new MyCustomEnumConverter());
       return f;
   }
}

一个简单的转换器可能如下所示:

public class MyCustomEnumConverter implements Converter<String, SortEnum> {
    @Override
    public SortEnum convert(String source) {
       try {
          return SortEnum.valueOf(source);
       } catch(Exception e) {
          return null; // or SortEnum.asc
       }
    }
}

【讨论】:

  • 如果您希望对所有端点全局执行此行为,这是正确的答案。如果您希望这种行为仅适​​用于您的一个控制器,那么 satish chennupati 提供了正确的解决方案。
  • 在这样做之后,不知何故,我的 oauth2 端点搞砸了,用户无法进行身份验证
  • 那是com.fasterxml.jackson.databind.util.Converter吗?
  • @CharlesWood 不,那是org.springframework.core.convert.converter.Converter
  • 请注意扩展 WebMvcConfigurationSupport 可能有副作用,例如与 spring-boot-starter-actuator 一起使用会导致重复 bean 冲突。一种解决方案是使用 @Autowired 获取 FormattingConversionService bean 并将转换器添加到其中。
【解决方案2】:

如果你使用的是 Spring Boot,this is the reason 表示你不应该使用 WebMvcConfigurationSupport

最佳实践,你应该实现接口org.springframework.core.convert.converter.Converter,并带有注解@Component。然后 Spring Boot 将自动加载所有 Converter 的 bean。 Spring Boot code

@Component
public class GenderEnumConverter implements Converter<String, GenderEnum> {
    @Override
    public GenderEnum convert(String value) {
        return GenderEnum.of(Integer.valueOf(value));
    }
}

Demo Project

【讨论】:

【解决方案3】:

您需要执行以下操作

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
    dataBinder.registerCustomEditor(YourEnum.class, new YourEnumConverter());
}

参考以下内容:https://machiel.me/post/java-enums-as-request-parameters-in-spring-4/

【讨论】:

    【解决方案4】:

    到目前为止提供的答案并不完整。这是一个对我有用的逐步答案示例:-

    1st 在您的端点签名中定义枚举(订阅类型)。
    示例

    public ResponseEntity v1_getSubscriptions(@PathVariable String agencyCode,
                                              @RequestParam(value = "uwcompany", required = false) String uwCompany,
                                              @RequestParam(value = "subscriptiontype", required = false) SubscriptionType subscriptionType,
                                              @RequestParam(value = "alert", required = false) String alert,
    

    2nd 定义一个自定义属性编辑器,用于将字符串转换为枚举:

    import java.beans.PropertyEditorSupport;
    
    public class SubscriptionTypeEditor extends PropertyEditorSupport {
    
        public void setAsText(String text) {
            try {
                setValue(SubscriptionType.valueOf(text.toUpperCase()));
            } catch (Exception ex) {
                setValue(null);
            }
        }
    }
    

    3rd 向控制器注册属性编辑器:

    @InitBinder ("subscriptiontype")
    public void initBinder(WebDataBinder dataBinder) {
        dataBinder.registerCustomEditor(SubscriptionType.class, new SubscriptionTypeEditor());
    }
    

    从字符串到枚举的翻译现在应该很完美了。

    【讨论】:

      【解决方案5】:

      如果您有多个枚举,那么如果您遵循其他答案,您最终会为每个枚举创建一个转换器。

      这是适用于所有枚举的解决方案。

      Converter 或 PropertyEditorSupport 在这种情况下不合适,因为它们不允许我们知道目标类。

      在此示例中,我使用了 Jackson ObjectMapper,但您可以通过反射调用静态方法来替换这部分,或者将对 values() 的调用移至转换器。

      @Component
      public class JacksonEnumConverter implements GenericConverter {
      
          private ObjectMapper mapper;
      
          private Set<ConvertiblePair> set;
      
          @Autowired
          public JacksonEnumConverter(ObjectMapper mapper) {
              set = new HashSet<>();
              set.add(new ConvertiblePair(String.class, Enum.class));
              this.mapper = mapper;
          }
      
          @Override
          public Set<ConvertiblePair> getConvertibleTypes() {
              return set;
          }
      
          @Override
          public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
              if (source == null) {
                  return null;
              }
              try {
                  return mapper.readValue("\"" + source + "\"", targetType.getType());
              } catch (IOException e) {
                  throw new InvalidFieldException(targetType.getName(),source.toString());
              }
          }
      }
      

      在这种情况下,因为我使用的是 Jackson,所以枚举类必须有一个用 @JsonCreator 注释的静态方法,这样我就可以使用值而不是常量名进行映射:

      public enum MyEnum {
      
          VAL_1("val-1"), VAL_2("val-2");
      
          private String value;
      
          MyEnum(String value) {
              this.value = value;
          }
      
          @JsonValue
          public String getValue() {
              return value;
          }
      
          @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
          public static MyEnum fromValue(String value) {
              for (MyEnum e : values()) {
                  if (e.value.equalsIgnoreCase(value)) {
                      return e;
                  }
              }
              throw new InvalidFieldException("my-enum", value);
          }
      }
      

      与其返回 null,不如抛出异常。

      【讨论】:

      • 我正在寻找一种解决方案,我最终不会使用大量转换器。谢谢!
      • 我添加的一件好事是一个后备:如果我不能用 JSON 映射它,我会后备搜索枚举值。
      • 不返回空值。而是返回一个默认枚举,例如。 UNKNOWN 或抛出异常。
      • 好记。我已经更新了答案。无论如何,我认为如果将此评论放在问题中会更有价值,因为这就是它所要求的。
      【解决方案6】:

      如果您已经在实现 WebMvcConfigurer,而不是 WebMvcConfigurationSupport,您可以通过实现 addFormatters 方法来添加新转换器

        @Override
        public void addFormatters(FormatterRegistry registry) {
          registry.addConverter(new MyCustomEnumConverter());
        }
      

      【讨论】:

        【解决方案7】:

        您可以在 ENUM 中使用@JsonValue 注释。检查这个 - https://www.baeldung.com/jackson-serialize-enums

        【讨论】:

          【解决方案8】:

          使用一些最新的 Spring 版本,根据 2021 年 8 月,以下代码最简单并且可以正常工作。唯一棘手的部分是方法 deserializeByName(),需要添加到 enum 中,其余代码正常。

          public enum WorkplanGenerationMode {
          
              AppendWorktypes("AppendWorktypes"),
              New("New");
          
              private String value;
          
              WorkplanGenerationMode(String value) {
                  this.value = value;
              }
          
              @JsonValue
              public String getValue() {
                  return value;
              }
          
              @JsonCreator
              public static WorkplanGenerationMode deserializeByName(@JsonProperty("name") String name) {
                  return WorkplanGenerationMode.valueOf(name);
              }
          
          
          }
          

          然后到下面的端点一个字符串值进来,并被转换为正确的枚举值,java枚举成员被初始化。

          public ResponseEntity<List<WorkplanGenerationProgressDTO>> generate(@RequestBody WorkplanFromBaseRequest request) {
          

          注意! WorkplanGenerationMode 是 WorkplanFromBaseRequest 的任何成员的类型,如下所示:

          @Data
          public class WorkplanFromBaseRequest {
          
              private WorkplanGenerationMode mode;
          

          【讨论】:

          • 如果您使用枚举类本身作为请求参数,这将不起作用。如果它嵌入在杰克逊启动的任何其他对象中,它就可以工作。我认为当遇到独立枚举时,spring不会使用杰克逊转换为枚举。有错请指正
          【解决方案9】:

          您可以使用String 代替SortEnum 参数

          @RequestParam(name = "sort", required = false) String sort
          

          并使用转换它

          SortEnum se;
          try {
             se = SortEnum.valueOf(source);
          } catch(IllegalArgumentException e) {
             se = null;
          }
          

          在 getEvents(...) 端点方法内部失去了优雅,但获得了对转换和可能的错误处理的更多控制。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-11-22
            • 2014-04-26
            • 2016-02-17
            • 2016-02-25
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多