Spring Boot:请求参数到POJO的可选映射



我正在尝试将控制器方法的请求参数映射到POJO对象,但仅当它的任何字段存在时。然而,我似乎找不到一种方法来实现这一点。我有以下POJO:

public class TimeWindowModel {
@NotNull
public Date from;
@NotNull
public Date to;
}

如果没有指定任何字段,我希望得到一个空的Optional,否则我将得到一个带有POJO验证实例的Optional。Spring支持将请求参数映射到POJO对象,方法是在处理程序中不带注释:

@GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(@RequestAttribute("staff") Staff staff,
@PathVariable("shopId") Long shopId, @Valid TimeWindowModel timeWindow) {
// controller code
}

有了这个,Spring将映射请求参数"one_answers";to"到TimeWindowModel的一个实例。然而,我想让这个映射是可选的。对于POST请求,您可以使用@RequestBody @Valid Optional<T>,这将给您一个包含T实例的Optional<T>,但只有在提供了请求主体的情况下,否则它将为空。这使得@Valid按预期工作。

当没有注释时,Optional<T>似乎不做任何事情。您总是获得带有POJO实例的Optional<T>。当与@Valid结合使用时,这是有问题的,因为它会抱怨"from"one_answers";to">

目标是获得(a)一个POJO的实例,其中"from"one_answers";to"不为空或(b)什么都没有。如果只指定了其中一个,那么@Valid应该失败并报告另一个缺失。

我想出了一个解决方案,使用自定义HandlerMethodArgumentResolver, Jackson和Jackson数据绑定。

注释:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParamBind {
}

解析器:

public class RequestParamBindResolver implements HandlerMethodArgumentResolver {
private final ObjectMapper mapper;
public RequestParamBindResolver(ObjectMapper mapper) {
this.mapper = mapper.copy();
this.mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(RequestParamBind.class) != null;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mav, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// take the first instance of each request parameter
Map<String, String> requestParameters = webRequest.getParameterMap()
.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue()[0]));
// perform the actual resolution
Object resolved = doResolveArgument(parameter, requestParameters);
// *sigh*
// see: https://stackoverflow.com/questions/18091936/spring-mvc-valid-validation-with-custom-handlermethodargumentresolver
if (parameter.hasParameterAnnotation(Valid.class)) {
String parameterName = Conventions.getVariableNameForParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, resolved, parameterName);
// DataBinder constructor unwraps Optional, so the target could be null
if (binder.getTarget() != null) {
binder.validate();
BindingResult bindingResult = binder.getBindingResult();
if (bindingResult.getErrorCount() > 0)
throw new MethodArgumentNotValidException(parameter, bindingResult);
}
}
return resolved;
}
private Object doResolveArgument(MethodParameter parameter, Map<String, String> requestParameters) {
Class<?> clazz = parameter.getParameterType();
if (clazz != Optional.class)
return mapper.convertValue(requestParameters, clazz);
// special case for Optional<T>
Type type = parameter.getGenericParameterType();
Class<?> optionalType = (Class<?>)((ParameterizedType)type).getActualTypeArguments()[0];
Object obj = mapper.convertValue(requestParameters, optionalType);
// convert back to a map to find if any fields were set
// TODO: how can we tell null from not set?
if (mapper.convertValue(obj, new TypeReference<Map<String, String>>() {})
.values().stream().anyMatch(Objects::nonNull))
return Optional.of(obj);
return Optional.empty();
}
}

然后,我们注册它:

@Configuration
public class WebConfig implements WebMvcConfigurer {
//...
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestParamBindResolver(new ObjectMapper()));
}
}

最后,我们可以这样使用:

@GetMapping("/shop/{shopId}/slot")
public Slice<Slot> getSlots(@RequestAttribute("staff") Staff staff,
@PathVariable("shopId") Long shopId,
@RequestParamBind @Valid Optional<TimeWindowModel> timeWindow) {
// controller code
}

就像你期望的那样。

我确信可以通过在解析器中使用Spring自己的DataBind类来实现这一点。然而,Jackson Databind似乎是最直接的解决方案。也就是说,它无法区分设置为null的字段和未设置的字段。对于我的用例来说,这不是一个真正的问题,但它应该被注意到。

要实现逻辑(a)都不为空或(b)都为空,您需要实现自定义验证。示例如下:

https://blog.clairvoyantsoft.com/spring-boot-creating-a-custom-annotation-for-validation-edafbf9a97a4
https://www.baeldung.com/spring-mvc-custom-validator

一般来说,你创建一个新的注释,它只是一个存根,然后你创建一个验证器实现ConstraintValidator,在那里你提供你的逻辑,然后你把你的新注释到你的POJO。

最新更新