Spring 的@RequestParam 和 Enum

我有这个枚举:

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 接受的答案是有效的。但是,我决定处理 MethodArgumentTypeMismatch 异常。

@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);
}
122847 次浏览

You can create a custom converter that will return null instead of an exception when an invalid value is supplied.

Something like this:

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

And a simple converter might look like this:

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

you need to do the following

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

refer the following : https://machiel.me/post/java-enums-as-request-parameters-in-spring-4/

The answers provided so far are not complete. Here is an answer example step-by-step that worked for me:-

1st Define the enum in your endpoint signature(subscription type).
Example:

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 Define a custom property editor that will be used to translate from String to enum:

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 Register the property editor with the controller:

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

Translations from string to enum should happen perfectly now.

If you are using Spring Boot, this is the reason that you should not use WebMvcConfigurationSupport.

The best practice, you should implement interface org.springframework.core.convert.converter.Converter, and with annotation @Component. Then Spring Boot will auto load all Converter's 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

you can use String instead of SortEnum param

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

and convert it using

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

inside of getEvents(...) endpoint method losing elegance but gaining more control over conversion and possible error handling.

If you are already implementing WebMvcConfigurer, instead of WebMvcConfigurationSupport, you can add new converter by implementing the method addFormatters

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

If you have multiple enums then if you follow the other answers you'll end up creating one converter for each one.

Here is a solution that works for all enums.

Converter or PropertyEditorSupport are not appropriate in this case because they don't allow us to know the target class.

In this example I have used the Jackson ObjectMapper, but you could replace this part by a call to the static method via reflection or move the call to values() to the converter.

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

and in this case, because I'm using Jackson, the enum class has to have a static method annotated with @JsonCreator so I can map using the value rather than the constant name:

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

Instead of returning null it is better to throw an exception.

You can use @JsonValue annotation in the ENUM. Check this - https://www.baeldung.com/jackson-serialize-enums

Using some recent Spring version, as per august 2021, the following code is simplest and just works. The only tricky part is the method deserializeByName(), which you need to add to enum, the rest of code is usual.

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




}

And then to the following endpoint a string value comes in, and is transformed into correct enum value, and java enum member gets initialised with it.

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

NB! WorkplanGenerationMode is a type of any member of WorkplanFromBaseRequest, like this:

@Data
public class WorkplanFromBaseRequest {


private WorkplanGenerationMode mode;