MethodValidationInterceptor и @Validated @ModelAttribute - PullRequest
0 голосов
/ 25 мая 2018

У меня есть приложение 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, потому что он уже проверен связывателем, и я хочу обработать его по-другому?

Я копался ввесенний код, и, похоже, не предназначен для совместной работы.У меня есть несколько идей, как это исправить:

  • уберите @Validated из аргумента и
    • вызов validator.validate() явно в методе контроллера - уродливо и опасно (вы можетезабудьте об этом)
    • создайте еще один перехватчик AOP, который найдет «пары» из @ModelAttribute и BindingResult и вызовет там валидатор, вызывая глобальную проверку

Я совершенно не прав?Я что-то пропустил?Есть ли лучший способ?

Ответы [ 2 ]

0 голосов
/ 12 апреля 2019

Спасибо, что поделились этим решением.

Я использовал его как источник вдохновения и как основу для создания более общего валидатора аргументов метода, который я намерен использовать в выбранных методах.

Валидациязапускается для методов, аннотированных @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

0 голосов
/ 25 мая 2018

Я придумала решение, которое позволит мне продолжить работу, но я не считаю эту проблему решенной.

Как я уже говорил в первоначальном вопросе, этот аспект заставляет проверять@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
)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...