MethodValidationInterceptor 和 @Validated @ModelAttribute
Posted
技术标签:
【中文标题】MethodValidationInterceptor 和 @Validated @ModelAttribute【英文标题】:MethodValidationInterceptor and @Validated @ModelAttribute 【发布时间】:2022-01-27 05:22:56 【问题描述】:我有一个 Spring Boot 2 应用程序,我希望能够使用 Hibernate 验证器验证控制器参数 - 我正在成功使用它。我将所有控制器都注释为@Validated
,并且我正在使用@PathVariable @AssertUuid final String customerId
等请求参数的验证 - 到目前为止一切顺利,一切正常。
但是,我还希望能够从表单中验证 @ModelAttribute
。
@Controller
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(path = "/customers")
@Validated
public class CustomerController
private final CustomerFacade customerFacade;
public CustomerController(
final CustomerFacade customerFacade
)
this.customerFacade = customerFacade;
@GetMapping("/create")
public ModelAndView create(
final AccessToken accessToken
)
return new ModelAndView("customer/create")
.addObject("customer", new CreateCustomerRequest());
@PostMapping("/create")
public ModelAndView handleCreate(
final AccessToken accessToken,
@Validated @ModelAttribute("customer") final CreateCustomerRequest customerValues,
final BindingResult validation
) throws
UserDoesNotHaveAdminAccessException
if (validation.hasErrors())
return new ModelAndView("customer/create")
.addObject("customer", customerValues);
CustomerResult newCustomer = customerFacade.createCustomer(
accessToken,
customerValues.getName()
);
return new ModelAndView(new RedirectView("..."));
public static final class CreateCustomerRequest
@NotNull
@NotBlank
private String name;
public CreateCustomerRequest(final String name)
this.name = name;
public CreateCustomerRequest()
public String getName()
return name;
但这会导致MethodValidationInterceptor
在我发送无效数据时抛出ConstraintViolationException
。这通常是有道理的,我希望在所有其他情况下都有这种行为,但在这种情况下,如您所见,我想使用 BindingResult
来处理验证错误 - 这在处理表单时是必需的。
有没有办法告诉 Spring 不使用 MethodValidationInterceptor
验证这个特定参数,因为它已经被 binder 验证了,我想以不同的方式处理它?
我一直在研究 spring 代码,它看起来并不是为了协同工作而设计的。我有一些想法可以解决这个问题:
从参数中删除@Validated
并
在控制器方法中显式调用validator.validate()
- 丑陋而危险(你可能忘记调用它)
创建另一个 AOP 拦截器,它将找到 @ModelAttribute
和 BindingResult
的“对”并在那里调用验证器,从而强制全局验证
我这样做完全错了吗?我错过了什么吗?有没有更好的办法?
【问题讨论】:
【参考方案1】:我想出了一个可以让我继续工作的解决方案,但我认为这个问题还没有解决。
正如我在原始问题中所暗示的那样,当 @ModelAttribute
未使用 @Validated
或 @Valid
注释时,此方面会强制验证它。
这意味着ConstraintViolationException
不会因为无效@ModelAttribute
而被抛出,您可以处理方法体中的错误。
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.MethodParameter;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import javax.validation.Valid;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@SuppressWarnings("checkstyle:IllegalThrows")
@Aspect
public class ControllerModelAttributeAutoValidatingAspect
private final Validator validator;
public ControllerModelAttributeAutoValidatingAspect(
final Validator validator
)
this.validator = validator;
@Around("execution(public * ((@org.springframework.web.bind.annotation.RequestMapping *)+).*(..)))")
public Object proceed(final ProceedingJoinPoint pjp) throws Throwable
MethodSignature methodSignature = MethodSignature.class.cast(pjp.getSignature());
List<MethodParameter> methodParameters = getMethodParameters(methodSignature);
PeekingIterator<MethodParameter> parametersIterator = Iterators.peekingIterator(methodParameters.iterator());
while (parametersIterator.hasNext())
MethodParameter parameter = parametersIterator.next();
if (!parameter.hasParameterAnnotation(ModelAttribute.class))
// process only ModelAttribute arguments
continue;
if (parameter.hasParameterAnnotation(Validated.class) || parameter.hasParameterAnnotation(Valid.class))
// if the argument is annotated as validated, the binder already validated it
continue;
MethodParameter nextParameter = parametersIterator.peek();
if (!Errors.class.isAssignableFrom(nextParameter.getParameterType()))
// the Errors argument has to be right after the ModelAttribute argument to form a pair
continue;
Object target = pjp.getArgs()[methodParameters.indexOf(parameter)];
Errors errors = Errors.class.cast(pjp.getArgs()[methodParameters.indexOf(nextParameter)]);
validator.validate(target, errors);
return pjp.proceed();
private List<MethodParameter> getMethodParameters(final MethodSignature methodSignature)
return IntStream.range(0, methodSignature.getParameterNames().length)
.mapToObj(i -> new MethodParameter(methodSignature.getMethod(), i))
.collect(Collectors.toList());
现在,您可以继续在控制器方法中使用验证注释,同时,final BindingResult validation
可以按预期工作。
@PostMapping("/create")
public ModelAndView handleCreate(
final AccessToken accessToken,
@ModelAttribute("customer") final CreateCustomerRequest customerValues,
final BindingResult validation
)
【讨论】:
【参考方案2】:感谢您分享此解决方案。
我用它作为灵感和基础,创建了一个更通用的方法参数验证器,我打算在选定的方法上使用它。
@Validate 注解的方法触发验证:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Validate
例子:
@Validate
public void testMe(BindingModel bindingModel, Errors errors)
if (!errors.hasErrors())
// bindingModel is valid
这是修改后的方面类:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Aspect
@Component
public class ValidateAspect
private final Validator validator;
public ValidateAspect(Validator validator)
this.validator = validator;
@Around("@annotation(Validate)")
public Object proceed(ProceedingJoinPoint pjp) throws Throwable
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
List<MethodParameter> methodParameters = getMethodParameters(methodSignature);
for (int i = 0; i < methodParameters.size() - 1; i++)
MethodParameter parameter = methodParameters.get(i);
MethodParameter nextParameter = methodParameters.get(i + 1);
if (!Errors.class.isAssignableFrom(nextParameter.getParameterType()))
// the Errors argument has to be right after the validated argument to form a pair
continue;
Object target = pjp.getArgs()[methodParameters.indexOf(parameter)];
Errors errors = (Errors) pjp.getArgs()[methodParameters.indexOf(nextParameter)];
validator.validate(target, errors);
return pjp.proceed();
private static List<MethodParameter> getMethodParameters(MethodSignature methodSignature)
return IntStream
.range(0, methodSignature.getParameterNames().length)
.mapToObj(i -> new MethodParameter(methodSignature.getMethod(), i))
.collect(Collectors.toList());
上面的代码已经过测试并且(到目前为止)似乎可以在 Spring Boot 2.1.4.RELEASE 上正常工作
【讨论】:
以上是关于MethodValidationInterceptor 和 @Validated @ModelAttribute的主要内容,如果未能解决你的问题,请参考以下文章