Spring的@RequestParam与枚举类型

96

我有这个枚举:

public enum SortEnum {
    asc, desc;
}

我想将其用作rest请求的参数:

@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的含义,它的意思是:“请求实体的语法正确”,但如果字符串与枚举不匹配,我认为这并不是情况。 - Constantino Cronemberger
适当的错误代码应该是400("错误请求") - Varun
9个回答

84
如果您正在使用Spring Boot,这就是您不应该使用WebMvcConfigurationSupport的原因。最佳实践是,您应该实现接口org.springframework.core.convert.converter.Converter,并带有注释@Component,然后Spring Boot将自动加载所有Converter的bean。Spring Boot代码
@Component
public class GenderEnumConverter implements Converter<String, GenderEnum> {
    @Override
    public GenderEnum convert(String value) {
        return GenderEnum.of(Integer.valueOf(value));
    }
}

演示项目


这是最简洁的答案!在较新的Spring Boot版本(此处为2.5.2)中,这样做有所不同: https://github.com/spring-projects/spring-boot/blob/v2.5.2/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java#L327 https://github.com/spring-projects/spring-boot/blob/v2.5.2/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java#L274 - Mark
我需要这个,但也必须确保PathVariable名称与实际方法变量名称匹配。 - mr nooby noob

66

您可以创建一个自定义转换器,当提供了无效值时,它将返回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
       }
    }
}

5
如果您希望在所有端点上全局应用此行为,则这是正确的答案。如果您只想针对一个控制器设置此行为,那么 Satish Chennupati 给出了正确的解决方案。 - loesak
1
在这之后,不知何故,我的oauth2端点出了问题,用户无法进行身份验证。 - Olayinka
2
那是 com.fasterxml.jackson.databind.util.Converter 吗? - Charles Wood
8
不,那是一个org.springframework.core.convert.converter.Converter - vadipp
7
注意继承WebMvcConfigurationSupport可能会产生副作用,例如与spring-boot-starter-actuator一起使用会导致重复的bean冲突。 解决方法是使用@Autowired获取FormattingConversionService bean,并将您的转换器添加到其中。 - Jean-Marc Astesana
显示剩余2条评论

27

10

如果您有多个枚举类型,如果按照其他答案的方式操作,您最终将为每个枚举类型创建一个转换器。

这里提供了适用于所有枚举类型的解决方案。

在这种情况下,转换器或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,抛出异常更好。


2
我正好在寻找一种解决方案,以免最终需要使用大量的转换器。谢谢! - João Menighin
1
我添加的一个好东西是回退功能:如果无法通过JSON映射它,我会回退搜索枚举值。 - João Menighin
请勿返回 null。而是返回一个默认的枚举,例如 UNKNOWN 或抛出异常。 - Jacques Koorts
记住了。我已经更新了答案。无论如何,我认为如果将此评论放在问题中,它会更有价值,因为这就是问题所要求的。 - Constantino Cronemberger

6
迄今为止提供的答案不完整。以下是一个逐步实例化的答案,适用于我:
第一步,在您的终端点签名(订阅类型)中定义枚举。
示例
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,

第二步 定义一个自定义属性编辑器,用于将字符串翻译为枚举:

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);
        }
    }
}

第三步,向控制器注册属性编辑器:

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

现在,将字符串转换为枚举类型应该是完美的了。


这个可以工作,但为什么需要如此复杂的答案呢?本应该只需要一个带有 @Override 的单一方法。 - David S

1
如果您已经在实现WebMvcConfigurer而不是WebMvcConfigurationSupport,则可以通过实现addFormatters方法来添加新的转换器。
  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new MyCustomEnumConverter());
  }

0

0

使用一些最近的Spring版本,截至2021年8月,以下代码是最简单的,并且可以正常工作。唯一棘手的部分是方法deserializeByName(),您需要将其添加到枚举中,其余代码与往常一样。

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;

如果您将枚举类本身用作请求参数,则此方法无效。它仅在嵌入到其他对象中时有效,此时Jackson会介入。我认为当遇到独立的枚举类时,Spring不使用Jackson转换为枚举类。如果我错了,请纠正我。 - Deekshith Anand

-3

你可以使用String代替SortEnum参数

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

并使用它进行转换

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接