【问题标题】:MethodValidationInterceptor and @Validated @ModelAttributeMethodValidationInterceptor 和 @Validated @ModelAttribute
【发布时间】:2023-04-01 13:13:01
【问题描述】:

我有一个 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 拦截器,它将找到 @ModelAttributeBindingResult 的“对”并在那里调用验证器,从而强制全局验证

我这样做完全错了吗?我错过了什么吗?有没有更好的办法?

【问题讨论】:

    标签: java spring spring-mvc spring-boot hibernate-validator


    【解决方案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 上正常工作

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-07-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-05-04
        • 2013-04-14
        相关资源
        最近更新 更多