parent
bd054a4918
commit
6b89cf94a3
|
@ -11,6 +11,7 @@ dependencies {
|
||||||
optional(project(":spring-context"))
|
optional(project(":spring-context"))
|
||||||
optional(project(":spring-context-support")) // for FreeMarker support
|
optional(project(":spring-context-support")) // for FreeMarker support
|
||||||
optional("jakarta.servlet:jakarta.servlet-api")
|
optional("jakarta.servlet:jakarta.servlet-api")
|
||||||
|
optional("jakarta.validation:jakarta.validation-api")
|
||||||
optional("jakarta.websocket:jakarta.websocket-api")
|
optional("jakarta.websocket:jakarta.websocket-api")
|
||||||
optional("jakarta.websocket:jakarta.websocket-client-api")
|
optional("jakarta.websocket:jakarta.websocket-client-api")
|
||||||
optional("org.webjars:webjars-locator-core")
|
optional("org.webjars:webjars-locator-core")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -16,13 +16,17 @@
|
||||||
|
|
||||||
package org.springframework.web.reactive;
|
package org.springframework.web.reactive;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.ReactiveAdapterRegistry;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.validation.DataBinder;
|
||||||
import org.springframework.validation.support.BindingAwareConcurrentModel;
|
import org.springframework.validation.support.BindingAwareConcurrentModel;
|
||||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||||
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
||||||
|
@ -50,6 +54,8 @@ public class BindingContext {
|
||||||
|
|
||||||
private final Model model = new BindingAwareConcurrentModel();
|
private final Model model = new BindingAwareConcurrentModel();
|
||||||
|
|
||||||
|
private boolean methodValidationApplicable;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@code BindingContext}.
|
* Create a new {@code BindingContext}.
|
||||||
|
@ -74,6 +80,16 @@ public class BindingContext {
|
||||||
return this.model;
|
return this.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure flag to signal whether validation will be applied to handler
|
||||||
|
* method arguments, which is the case if Bean Validation is enabled in
|
||||||
|
* Spring MVC, and method parameters have {@code @Constraint} annotations.
|
||||||
|
* @since 6.1
|
||||||
|
*/
|
||||||
|
public void setMethodValidationApplicable(boolean methodValidationApplicable) {
|
||||||
|
this.methodValidationApplicable = methodValidationApplicable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a {@link WebExchangeDataBinder} to apply data binding and
|
* Create a {@link WebExchangeDataBinder} to apply data binding and
|
||||||
|
@ -112,6 +128,24 @@ public class BindingContext {
|
||||||
return createDataBinder(exchange, null, name);
|
return createDataBinder(exchange, null, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variant of {@link #createDataBinder(ServerWebExchange, Object, String)}
|
||||||
|
* with a {@link MethodParameter} for which the {@code DataBinder} is created.
|
||||||
|
* That may provide more insight to initialize the {@link WebExchangeDataBinder}.
|
||||||
|
* <p>By default, if the parameter has {@code @Valid}, Bean Validation is
|
||||||
|
* excluded, deferring to method validation.
|
||||||
|
* @since 6.1
|
||||||
|
*/
|
||||||
|
public WebExchangeDataBinder createDataBinder(
|
||||||
|
ServerWebExchange exchange, @Nullable Object target, String name, MethodParameter parameter) {
|
||||||
|
|
||||||
|
WebExchangeDataBinder dataBinder = createDataBinder(exchange, target, name);
|
||||||
|
if (this.methodValidationApplicable) {
|
||||||
|
MethodValidationInitializer.updateBinder(dataBinder, parameter);
|
||||||
|
}
|
||||||
|
return dataBinder;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extended variant of {@link WebExchangeDataBinder}, adding path variables.
|
* Extended variant of {@link WebExchangeDataBinder}, adding path variables.
|
||||||
|
@ -130,4 +164,21 @@ public class BindingContext {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Excludes Bean Validation if the method parameter has {@code @Valid}.
|
||||||
|
*/
|
||||||
|
private static class MethodValidationInitializer {
|
||||||
|
|
||||||
|
public static void updateBinder(DataBinder binder, MethodParameter parameter) {
|
||||||
|
if (ReactiveAdapterRegistry.getSharedInstance().getAdapter(parameter.getParameterType()) == null) {
|
||||||
|
for (Annotation annotation : parameter.getParameterAnnotations()) {
|
||||||
|
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {
|
||||||
|
binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -37,6 +37,7 @@ import org.springframework.http.HttpStatusCode;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.validation.beanvalidation.MethodValidator;
|
||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
import org.springframework.web.reactive.BindingContext;
|
import org.springframework.web.reactive.BindingContext;
|
||||||
import org.springframework.web.reactive.HandlerResult;
|
import org.springframework.web.reactive.HandlerResult;
|
||||||
|
@ -56,6 +57,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
|
|
||||||
private static final Mono<Object[]> EMPTY_ARGS = Mono.just(new Object[0]);
|
private static final Mono<Object[]> EMPTY_ARGS = Mono.just(new Object[0]);
|
||||||
|
|
||||||
|
private static final Class<?>[] EMPTY_GROUPS = new Class<?>[0];
|
||||||
|
|
||||||
private static final Object NO_ARG_VALUE = new Object();
|
private static final Object NO_ARG_VALUE = new Object();
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,6 +68,9 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
|
|
||||||
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
|
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MethodValidator methodValidator;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance from a {@code HandlerMethod}.
|
* Create an instance from a {@code HandlerMethod}.
|
||||||
|
@ -121,6 +127,16 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
this.reactiveAdapterRegistry = registry;
|
this.reactiveAdapterRegistry = registry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the {@link MethodValidator} to perform method validation with if the
|
||||||
|
* controller method {@link #shouldValidateArguments()} or
|
||||||
|
* {@link #shouldValidateReturnValue()}.
|
||||||
|
* @since 6.1
|
||||||
|
*/
|
||||||
|
public void setMethodValidator(@Nullable MethodValidator methodValidator) {
|
||||||
|
this.methodValidator = methodValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoke the method for the given exchange.
|
* Invoke the method for the given exchange.
|
||||||
|
@ -134,6 +150,10 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
|
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
|
||||||
|
|
||||||
return getMethodArgumentValues(exchange, bindingContext, providedArgs).flatMap(args -> {
|
return getMethodArgumentValues(exchange, bindingContext, providedArgs).flatMap(args -> {
|
||||||
|
Class<?>[] groups = getValidationGroups();
|
||||||
|
if (shouldValidateArguments() && this.methodValidator != null) {
|
||||||
|
this.methodValidator.validateArguments(getBean(), getBridgedMethod(), args, groups);
|
||||||
|
}
|
||||||
Object value;
|
Object value;
|
||||||
Method method = getBridgedMethod();
|
Method method = getBridgedMethod();
|
||||||
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
|
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
|
||||||
|
@ -225,6 +245,11 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Class<?>[] getValidationGroups() {
|
||||||
|
return ((shouldValidateArguments() || shouldValidateReturnValue()) && this.methodValidator != null ?
|
||||||
|
this.methodValidator.determineValidationGroups(getBean(), getBridgedMethod()) : EMPTY_GROUPS);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isAsyncVoidReturnType(MethodParameter returnType, @Nullable ReactiveAdapter adapter) {
|
private static boolean isAsyncVoidReturnType(MethodParameter returnType, @Nullable ReactiveAdapter adapter) {
|
||||||
if (adapter != null && adapter.supportsEmpty()) {
|
if (adapter != null && adapter.supportsEmpty()) {
|
||||||
if (adapter.isNoValue()) {
|
if (adapter.isNoValue()) {
|
||||||
|
|
|
@ -269,7 +269,7 @@ public abstract class AbstractMessageReaderArgumentResolver extends HandlerMetho
|
||||||
BindingContext binding, ServerWebExchange exchange) {
|
BindingContext binding, ServerWebExchange exchange) {
|
||||||
|
|
||||||
String name = Conventions.getVariableNameForParameter(param);
|
String name = Conventions.getVariableNameForParameter(param);
|
||||||
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name);
|
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name, param);
|
||||||
try {
|
try {
|
||||||
LocaleContextHolder.setLocaleContext(exchange.getLocaleContext());
|
LocaleContextHolder.setLocaleContext(exchange.getLocaleContext());
|
||||||
binder.validate(validationHints);
|
binder.validate(validationHints);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -39,6 +39,7 @@ import org.springframework.http.codec.HttpMessageReader;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||||
|
import org.springframework.validation.beanvalidation.MethodValidator;
|
||||||
import org.springframework.web.bind.annotation.InitBinder;
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
@ -94,6 +95,9 @@ class ControllerMethodResolver {
|
||||||
|
|
||||||
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
|
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final MethodValidator methodValidator;
|
||||||
|
|
||||||
private final Map<Class<?>, Set<Method>> initBinderMethodCache = new ConcurrentHashMap<>(64);
|
private final Map<Class<?>, Set<Method>> initBinderMethodCache = new ConcurrentHashMap<>(64);
|
||||||
|
|
||||||
private final Map<Class<?>, Set<Method>> modelAttributeMethodCache = new ConcurrentHashMap<>(64);
|
private final Map<Class<?>, Set<Method>> modelAttributeMethodCache = new ConcurrentHashMap<>(64);
|
||||||
|
@ -110,8 +114,10 @@ class ControllerMethodResolver {
|
||||||
private final Map<Class<?>, SessionAttributesHandler> sessionAttributesHandlerCache = new ConcurrentHashMap<>(64);
|
private final Map<Class<?>, SessionAttributesHandler> sessionAttributesHandlerCache = new ConcurrentHashMap<>(64);
|
||||||
|
|
||||||
|
|
||||||
ControllerMethodResolver(ArgumentResolverConfigurer customResolvers, ReactiveAdapterRegistry adapterRegistry,
|
ControllerMethodResolver(
|
||||||
ConfigurableApplicationContext context, List<HttpMessageReader<?>> readers) {
|
ArgumentResolverConfigurer customResolvers, ReactiveAdapterRegistry adapterRegistry,
|
||||||
|
ConfigurableApplicationContext context, List<HttpMessageReader<?>> readers,
|
||||||
|
@Nullable MethodValidator methodValidator) {
|
||||||
|
|
||||||
Assert.notNull(customResolvers, "ArgumentResolverConfigurer is required");
|
Assert.notNull(customResolvers, "ArgumentResolverConfigurer is required");
|
||||||
Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required");
|
Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required");
|
||||||
|
@ -123,6 +129,7 @@ class ControllerMethodResolver {
|
||||||
this.requestMappingResolvers = requestMappingResolvers(customResolvers, adapterRegistry, context, readers);
|
this.requestMappingResolvers = requestMappingResolvers(customResolvers, adapterRegistry, context, readers);
|
||||||
this.exceptionHandlerResolvers = exceptionHandlerResolvers(customResolvers, adapterRegistry, context);
|
this.exceptionHandlerResolvers = exceptionHandlerResolvers(customResolvers, adapterRegistry, context);
|
||||||
this.reactiveAdapterRegistry = adapterRegistry;
|
this.reactiveAdapterRegistry = adapterRegistry;
|
||||||
|
this.methodValidator = methodValidator;
|
||||||
|
|
||||||
initControllerAdviceCaches(context);
|
initControllerAdviceCaches(context);
|
||||||
}
|
}
|
||||||
|
@ -260,6 +267,7 @@ class ControllerMethodResolver {
|
||||||
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
|
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
|
||||||
invocable.setArgumentResolvers(this.requestMappingResolvers);
|
invocable.setArgumentResolvers(this.requestMappingResolvers);
|
||||||
invocable.setReactiveAdapterRegistry(this.reactiveAdapterRegistry);
|
invocable.setReactiveAdapterRegistry(this.reactiveAdapterRegistry);
|
||||||
|
invocable.setMethodValidator(this.methodValidator);
|
||||||
return invocable;
|
return invocable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -50,12 +50,14 @@ class InitBinderBindingContext extends BindingContext {
|
||||||
private Runnable saveModelOperation;
|
private Runnable saveModelOperation;
|
||||||
|
|
||||||
|
|
||||||
InitBinderBindingContext(@Nullable WebBindingInitializer initializer,
|
InitBinderBindingContext(
|
||||||
List<SyncInvocableHandlerMethod> binderMethods) {
|
@Nullable WebBindingInitializer initializer, List<SyncInvocableHandlerMethod> binderMethods,
|
||||||
|
boolean methodValidationApplicable) {
|
||||||
|
|
||||||
super(initializer);
|
super(initializer);
|
||||||
this.binderMethods = binderMethods;
|
this.binderMethods = binderMethods;
|
||||||
this.binderMethodContext = new BindingContext(initializer);
|
this.binderMethodContext = new BindingContext(initializer);
|
||||||
|
setMethodValidationApplicable(methodValidationApplicable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,7 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR
|
||||||
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResultSink.asMono());
|
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResultSink.asMono());
|
||||||
|
|
||||||
return valueMono.flatMap(value -> {
|
return valueMono.flatMap(value -> {
|
||||||
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name);
|
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name, parameter);
|
||||||
return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange))
|
return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange))
|
||||||
.doOnError(bindingResultSink::tryEmitError)
|
.doOnError(bindingResultSink::tryEmitError)
|
||||||
.doOnSuccess(aVoid -> {
|
.doOnSuccess(aVoid -> {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -33,9 +33,12 @@ import org.springframework.http.codec.HttpMessageReader;
|
||||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.validation.beanvalidation.MethodValidator;
|
||||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.method.support.HandlerMethodValidator;
|
||||||
import org.springframework.web.reactive.BindingContext;
|
import org.springframework.web.reactive.BindingContext;
|
||||||
import org.springframework.web.reactive.DispatchExceptionHandler;
|
import org.springframework.web.reactive.DispatchExceptionHandler;
|
||||||
import org.springframework.web.reactive.HandlerAdapter;
|
import org.springframework.web.reactive.HandlerAdapter;
|
||||||
|
@ -57,6 +60,9 @@ public class RequestMappingHandlerAdapter
|
||||||
|
|
||||||
private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class);
|
private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class);
|
||||||
|
|
||||||
|
private final static boolean BEAN_VALIDATION_PRESENT =
|
||||||
|
ClassUtils.isPresent("jakarta.validation.Validator", HandlerMethod.class.getClassLoader());
|
||||||
|
|
||||||
|
|
||||||
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
|
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
|
||||||
|
|
||||||
|
@ -69,6 +75,9 @@ public class RequestMappingHandlerAdapter
|
||||||
@Nullable
|
@Nullable
|
||||||
private ReactiveAdapterRegistry reactiveAdapterRegistry;
|
private ReactiveAdapterRegistry reactiveAdapterRegistry;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MethodValidator methodValidator;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private ConfigurableApplicationContext applicationContext;
|
private ConfigurableApplicationContext applicationContext;
|
||||||
|
|
||||||
|
@ -170,9 +179,12 @@ public class RequestMappingHandlerAdapter
|
||||||
if (this.reactiveAdapterRegistry == null) {
|
if (this.reactiveAdapterRegistry == null) {
|
||||||
this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
|
this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
|
||||||
}
|
}
|
||||||
|
if (BEAN_VALIDATION_PRESENT) {
|
||||||
|
this.methodValidator = HandlerMethodValidator.from(this.webBindingInitializer, null);
|
||||||
|
}
|
||||||
|
|
||||||
this.methodResolver = new ControllerMethodResolver(this.argumentResolverConfigurer,
|
this.methodResolver = new ControllerMethodResolver(this.argumentResolverConfigurer,
|
||||||
this.reactiveAdapterRegistry, this.applicationContext, this.messageReaders);
|
this.reactiveAdapterRegistry, this.applicationContext, this.messageReaders, this.methodValidator);
|
||||||
|
|
||||||
this.modelInitializer = new ModelInitializer(this.methodResolver, this.reactiveAdapterRegistry);
|
this.modelInitializer = new ModelInitializer(this.methodResolver, this.reactiveAdapterRegistry);
|
||||||
}
|
}
|
||||||
|
@ -189,7 +201,8 @@ public class RequestMappingHandlerAdapter
|
||||||
Assert.state(this.methodResolver != null && this.modelInitializer != null, "Not initialized");
|
Assert.state(this.methodResolver != null && this.modelInitializer != null, "Not initialized");
|
||||||
|
|
||||||
InitBinderBindingContext bindingContext = new InitBinderBindingContext(
|
InitBinderBindingContext bindingContext = new InitBinderBindingContext(
|
||||||
getWebBindingInitializer(), this.methodResolver.getInitBinderMethods(handlerMethod));
|
this.webBindingInitializer, this.methodResolver.getInitBinderMethods(handlerMethod),
|
||||||
|
this.methodValidator != null && handlerMethod.shouldValidateArguments());
|
||||||
|
|
||||||
InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);
|
InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,8 @@ public class ControllerMethodResolverTests {
|
||||||
applicationContext.refresh();
|
applicationContext.refresh();
|
||||||
|
|
||||||
this.methodResolver = new ControllerMethodResolver(
|
this.methodResolver = new ControllerMethodResolver(
|
||||||
resolvers, ReactiveAdapterRegistry.getSharedInstance(), applicationContext, codecs.getReaders());
|
resolvers, ReactiveAdapterRegistry.getSharedInstance(), applicationContext,
|
||||||
|
codecs.getReaders(), null);
|
||||||
|
|
||||||
Method method = ResolvableMethod.on(TestController.class).mockCall(TestController::handle).method();
|
Method method = ResolvableMethod.on(TestController.class).mockCall(TestController::handle).method();
|
||||||
this.handlerMethod = new HandlerMethod(new TestController(), method);
|
this.handlerMethod = new HandlerMethod(new TestController(), method);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2023 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -133,7 +133,8 @@ public class InitBinderBindingContextTests {
|
||||||
handlerMethod.setArgumentResolvers(new ArrayList<>(this.argumentResolvers));
|
handlerMethod.setArgumentResolvers(new ArrayList<>(this.argumentResolvers));
|
||||||
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
|
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
|
||||||
|
|
||||||
return new InitBinderBindingContext(this.bindingInitializer, Collections.singletonList(handlerMethod));
|
return new InitBinderBindingContext(
|
||||||
|
this.bindingInitializer, Collections.singletonList(handlerMethod), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,477 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2023 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.web.reactive.result.method.annotation;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintViolation;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import jakarta.validation.executable.ExecutableValidator;
|
||||||
|
import jakarta.validation.metadata.BeanDescriptor;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
import org.springframework.context.MessageSourceResolvable;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.validation.Errors;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.validation.Validator;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||||
|
import org.springframework.validation.beanvalidation.MethodValidationException;
|
||||||
|
import org.springframework.validation.beanvalidation.ParameterValidationResult;
|
||||||
|
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
|
||||||
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||||
|
import org.springframework.web.bind.support.WebExchangeBindException;
|
||||||
|
import org.springframework.web.context.support.GenericWebApplicationContext;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.reactive.HandlerResult;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||||
|
import org.springframework.web.testfixture.method.ResolvableMethod;
|
||||||
|
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method validation tests for Spring MVC controller methods.
|
||||||
|
* <p>When adding tests, consider the following others:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code HandlerMethodTests} -- detection if methods need validation
|
||||||
|
* <li>{@code MethodValidationAdapterTests} -- method validation independent of Spring MVC
|
||||||
|
* <li>{@code MethodValidationProxyTests} -- method validation with proxy scenarios
|
||||||
|
* </ul>
|
||||||
|
* @author Rossen Stoyanchev
|
||||||
|
*/
|
||||||
|
public class MethodValidationTests {
|
||||||
|
|
||||||
|
private static final Person mockPerson = mock(Person.class);
|
||||||
|
|
||||||
|
private static final Errors mockErrors = mock(Errors.class);
|
||||||
|
|
||||||
|
|
||||||
|
private RequestMappingHandlerAdapter handlerAdapter;
|
||||||
|
|
||||||
|
private InvocationCountingValidator jakartaValidator;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() throws Exception {
|
||||||
|
LocalValidatorFactoryBean validatorBean = new LocalValidatorFactoryBean();
|
||||||
|
validatorBean.afterPropertiesSet();
|
||||||
|
this.jakartaValidator = new InvocationCountingValidator(validatorBean);
|
||||||
|
this.handlerAdapter = initHandlerAdapter(this.jakartaValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequestMappingHandlerAdapter initHandlerAdapter(Validator validator) throws Exception {
|
||||||
|
ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
|
||||||
|
bindingInitializer.setValidator(validator);
|
||||||
|
|
||||||
|
GenericWebApplicationContext context = new GenericWebApplicationContext();
|
||||||
|
context.refresh();
|
||||||
|
|
||||||
|
RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
|
||||||
|
handlerAdapter.setWebBindingInitializer(bindingInitializer);
|
||||||
|
handlerAdapter.setApplicationContext(context);
|
||||||
|
handlerAdapter.afterPropertiesSet();
|
||||||
|
return handlerAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void modelAttribute() {
|
||||||
|
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson));
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(request().queryParam("name", "name=Faustino1234"));
|
||||||
|
|
||||||
|
StepVerifier.create(this.handlerAdapter.handle(exchange, hm))
|
||||||
|
.consumeErrorWith(throwable -> {
|
||||||
|
WebExchangeBindException ex = (WebExchangeBindException) throwable;
|
||||||
|
|
||||||
|
assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1);
|
||||||
|
assertThat(this.jakartaValidator.getMethodValidationCount()).as("Method validation unexpected").isEqualTo(0);
|
||||||
|
|
||||||
|
assertBeanResult(ex.getBindingResult(), "student", Collections.singletonList(
|
||||||
|
"""
|
||||||
|
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||||
|
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
|
||||||
|
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||||
|
codes [student.name,name]; arguments []; default message [name],10,1]; \
|
||||||
|
default message [size must be between 1 and 10]"""));
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void modelAttributeAsync() {
|
||||||
|
|
||||||
|
// 1 for Mono argument validation + 1 for method validation of @RequestHeader
|
||||||
|
this.jakartaValidator.setMaxInvocationsExpected(2);
|
||||||
|
|
||||||
|
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handleAsync(Mono.empty(), ""));
|
||||||
|
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||||
|
request().queryParam("name", "name=Faustino1234").header("myHeader", "12345"));
|
||||||
|
|
||||||
|
HandlerResult handlerResult = this.handlerAdapter.handle(exchange, hm).block();
|
||||||
|
|
||||||
|
StepVerifier.create(((Mono<String>) handlerResult.getReturnValue()))
|
||||||
|
.consumeErrorWith(throwable -> {
|
||||||
|
WebExchangeBindException ex = (WebExchangeBindException) throwable;
|
||||||
|
|
||||||
|
assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(2);
|
||||||
|
assertThat(this.jakartaValidator.getMethodValidationCount()).isEqualTo(1);
|
||||||
|
|
||||||
|
assertBeanResult(ex.getBindingResult(), "student", Collections.singletonList(
|
||||||
|
"""
|
||||||
|
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||||
|
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
|
||||||
|
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||||
|
codes [student.name,name]; arguments []; default message [name],10,1]; \
|
||||||
|
default message [size must be between 1 and 10]"""));
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void modelAttributeWithBindingResult() {
|
||||||
|
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors));
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(request().queryParam("name", "name=Faustino1234"));
|
||||||
|
|
||||||
|
HandlerResult handlerResult = this.handlerAdapter.handle(exchange, hm).block();
|
||||||
|
|
||||||
|
assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1);
|
||||||
|
assertThat(this.jakartaValidator.getMethodValidationCount()).as("Method validation unexpected").isEqualTo(0);
|
||||||
|
|
||||||
|
assertThat(handlerResult.getReturnValue()).isEqualTo(
|
||||||
|
"""
|
||||||
|
org.springframework.validation.BeanPropertyBindingResult: 1 errors
|
||||||
|
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||||
|
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
|
||||||
|
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||||
|
codes [student.name,name]; arguments []; default message [name],10,1]; \
|
||||||
|
default message [size must be between 1 and 10]""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void modelAttributeWithBindingResultAndRequestHeader() {
|
||||||
|
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors, ""));
|
||||||
|
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||||
|
request().queryParam("name", "name=Faustino1234").header("myHeader", "123"));
|
||||||
|
|
||||||
|
StepVerifier.create(this.handlerAdapter.handle(exchange, hm))
|
||||||
|
.consumeErrorWith(throwable -> {
|
||||||
|
MethodValidationException ex = (MethodValidationException) throwable;
|
||||||
|
|
||||||
|
assertThat(this.jakartaValidator.getValidationCount()).isEqualTo(1);
|
||||||
|
assertThat(this.jakartaValidator.getMethodValidationCount()).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(ex.getConstraintViolations()).hasSize(2);
|
||||||
|
assertThat(ex.getAllValidationResults()).hasSize(2);
|
||||||
|
|
||||||
|
assertBeanResult(ex.getBeanResults().get(0), "student", Collections.singletonList(
|
||||||
|
"""
|
||||||
|
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||||
|
codes [Size.student.name,Size.name,Size.java.lang.String,Size]; \
|
||||||
|
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||||
|
codes [student.name,name]; arguments []; default message [name],10,1]; \
|
||||||
|
default message [size must be between 1 and 10]"""));
|
||||||
|
|
||||||
|
assertValueResult(ex.getValueResults().get(0), 2, "123", Collections.singletonList(
|
||||||
|
"""
|
||||||
|
org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||||
|
codes [Size.validController#handle.myHeader,Size.myHeader,Size.java.lang.String,Size]; \
|
||||||
|
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||||
|
codes [validController#handle.myHeader,myHeader]; arguments []; default message [myHeader],10,5]; \
|
||||||
|
default message [size must be between 5 and 10]"""
|
||||||
|
));
|
||||||
|
})
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatedWithMethodValidation() {
|
||||||
|
|
||||||
|
// 1 for @Validated argument validation + 1 for method validation of @RequestHeader
|
||||||
|
this.jakartaValidator.setMaxInvocationsExpected(2);
|
||||||
|
|
||||||
|
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handleValidated(mockPerson, mockErrors, ""));
|
||||||
|
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||||
|
request().queryParam("name", "name=Faustino1234").header("myHeader", "12345"));
|
||||||
|
|
||||||
|
HandlerResult handlerResult = this.handlerAdapter.handle(exchange, hm).block();
|
||||||
|
|
||||||
|
assertThat(jakartaValidator.getValidationCount()).isEqualTo(2);
|
||||||
|
assertThat(jakartaValidator.getMethodValidationCount()).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(handlerResult.getReturnValue()).isEqualTo(
|
||||||
|
"""
|
||||||
|
org.springframework.validation.BeanPropertyBindingResult: 1 errors
|
||||||
|
Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \
|
||||||
|
codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \
|
||||||
|
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||||
|
codes [person.name,name]; arguments []; default message [name],10,1]; \
|
||||||
|
default message [size must be between 1 and 10]""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void jakartaAndSpringValidator() {
|
||||||
|
HandlerMethod hm = handlerMethod(new InitBinderController(), ibc -> ibc.handle(mockPerson, mockErrors, ""));
|
||||||
|
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(
|
||||||
|
request().queryParam("name", "name=Faustino1234").header("myHeader", "12345"));
|
||||||
|
|
||||||
|
HandlerResult handlerResult = this.handlerAdapter.handle(exchange, hm).block();
|
||||||
|
|
||||||
|
assertThat(jakartaValidator.getValidationCount()).isEqualTo(1);
|
||||||
|
assertThat(jakartaValidator.getMethodValidationCount()).isEqualTo(1);
|
||||||
|
|
||||||
|
assertThat(handlerResult.getReturnValue()).isEqualTo(
|
||||||
|
"""
|
||||||
|
org.springframework.validation.BeanPropertyBindingResult: 2 errors
|
||||||
|
Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \
|
||||||
|
codes [TOO_LONG.person.name,TOO_LONG.name,TOO_LONG.java.lang.String,TOO_LONG]; \
|
||||||
|
arguments []; default message [length must be 10 or under]
|
||||||
|
Field error in object 'person' on field 'name': rejected value [name=Faustino1234]; \
|
||||||
|
codes [Size.person.name,Size.name,Size.java.lang.String,Size]; \
|
||||||
|
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: \
|
||||||
|
codes [person.name,name]; arguments []; default message [name],10,1]; \
|
||||||
|
default message [size must be between 1 and 10]""");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void springValidator() throws Exception {
|
||||||
|
HandlerMethod hm = handlerMethod(new ValidController(), c -> c.handle(mockPerson, mockErrors));
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(request().queryParam("name", "name=Faustino1234"));
|
||||||
|
|
||||||
|
RequestMappingHandlerAdapter springValidatorHandlerAdapter = initHandlerAdapter(new PersonValidator());
|
||||||
|
HandlerResult handlerResult = springValidatorHandlerAdapter.handle(exchange, hm).block();
|
||||||
|
|
||||||
|
assertThat(handlerResult.getReturnValue()).isEqualTo(
|
||||||
|
"""
|
||||||
|
org.springframework.validation.BeanPropertyBindingResult: 1 errors
|
||||||
|
Field error in object 'student' on field 'name': rejected value [name=Faustino1234]; \
|
||||||
|
codes [TOO_LONG.student.name,TOO_LONG.name,TOO_LONG.java.lang.String,TOO_LONG]; \
|
||||||
|
arguments []; default message [length must be 10 or under]""");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> HandlerMethod handlerMethod(T controller, Consumer<T> mockCallConsumer) {
|
||||||
|
Assert.isTrue(!(controller instanceof Class<?>), "Expected controller instance");
|
||||||
|
Method method = ResolvableMethod.on((Class<T>) controller.getClass()).mockCall(mockCallConsumer).method();
|
||||||
|
return new HandlerMethod(controller, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MockServerHttpRequest.BodyBuilder request() {
|
||||||
|
return MockServerHttpRequest.post("").contentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
private static void assertBeanResult(Errors errors, String objectName, List<String> fieldErrors) {
|
||||||
|
assertThat(errors.getObjectName()).isEqualTo(objectName);
|
||||||
|
assertThat(errors.getFieldErrors())
|
||||||
|
.extracting(FieldError::toString)
|
||||||
|
.containsExactlyInAnyOrderElementsOf(fieldErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
private static void assertValueResult(
|
||||||
|
ParameterValidationResult result, int parameterIndex, Object argument, List<String> errors) {
|
||||||
|
|
||||||
|
assertThat(result.getMethodParameter().getParameterIndex()).isEqualTo(parameterIndex);
|
||||||
|
assertThat(result.getArgument()).isEqualTo(argument);
|
||||||
|
assertThat(result.getResolvableErrors())
|
||||||
|
.extracting(MessageSourceResolvable::toString)
|
||||||
|
.containsExactlyInAnyOrderElementsOf(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private record Person(@Size(min = 1, max = 10) String name) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings({"unused", "SameParameterValue", "UnusedReturnValue"})
|
||||||
|
@RestController
|
||||||
|
static class ValidController {
|
||||||
|
|
||||||
|
void handle(@Valid @ModelAttribute("student") Person person) {
|
||||||
|
}
|
||||||
|
|
||||||
|
String handle(@Valid @ModelAttribute("student") Person person, Errors errors) {
|
||||||
|
return errors.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle(@Valid @ModelAttribute("student") Person person, Errors errors,
|
||||||
|
@RequestHeader @Size(min = 5, max = 10) String myHeader) {
|
||||||
|
}
|
||||||
|
|
||||||
|
String handleValidated(@Validated Person person, Errors errors,
|
||||||
|
@RequestHeader @Size(min = 5, max = 10) String myHeader) {
|
||||||
|
|
||||||
|
return errors.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<String> handleAsync(@Valid @ModelAttribute("student") Mono<Person> person,
|
||||||
|
@RequestHeader @Size(min = 5, max = 10) String myHeader) {
|
||||||
|
|
||||||
|
return person.map(Person::toString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings({"unused", "UnusedReturnValue", "SameParameterValue"})
|
||||||
|
@RestController
|
||||||
|
static class InitBinderController {
|
||||||
|
|
||||||
|
@InitBinder
|
||||||
|
void initBinder(WebDataBinder dataBinder) {
|
||||||
|
dataBinder.addValidators(new PersonValidator());
|
||||||
|
}
|
||||||
|
|
||||||
|
String handle(@Valid Person person, Errors errors, @RequestHeader @Size(min = 5, max = 10) String myHeader) {
|
||||||
|
return errors.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class PersonValidator implements Validator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> clazz) {
|
||||||
|
return (clazz == Person.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(Object target, Errors errors) {
|
||||||
|
Person person = (Person) target;
|
||||||
|
if (person.name().length() > 10) {
|
||||||
|
errors.rejectValue("name", "TOO_LONG", "length must be 10 or under");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercept and count number of method validation calls.
|
||||||
|
*/
|
||||||
|
private static class InvocationCountingValidator implements jakarta.validation.Validator, Validator {
|
||||||
|
|
||||||
|
private final SpringValidatorAdapter delegate;
|
||||||
|
|
||||||
|
private int maxInvocationsExpected = 1;
|
||||||
|
|
||||||
|
private int validationCount;
|
||||||
|
|
||||||
|
private int methodValidationCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor with maxCount=1.
|
||||||
|
*/
|
||||||
|
private InvocationCountingValidator(SpringValidatorAdapter delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxInvocationsExpected(int maxInvocationsExpected) {
|
||||||
|
this.maxInvocationsExpected = maxInvocationsExpected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total number of times Bean Validation was invoked.
|
||||||
|
*/
|
||||||
|
public int getValidationCount() {
|
||||||
|
return this.validationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of times method level Bean Validation was invoked.
|
||||||
|
*/
|
||||||
|
public int getMethodValidationCount() {
|
||||||
|
return this.methodValidationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BeanDescriptor getConstraintsForClass(Class<?> clazz) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T unwrap(Class<T> type) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExecutableValidator forExecutables() {
|
||||||
|
this.methodValidationCount++;
|
||||||
|
assertCountAndIncrement();
|
||||||
|
return this.delegate.forExecutables();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> clazz) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validate(Object target, Errors errors) {
|
||||||
|
assertCountAndIncrement();
|
||||||
|
this.delegate.validate(target, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertCountAndIncrement() {
|
||||||
|
assertThat(this.validationCount++).as("Too many calls to Bean Validation").isLessThan(this.maxInvocationsExpected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -79,7 +79,8 @@ public class ModelInitializerTests {
|
||||||
resolverConfigurer.addCustomResolver(new ModelMethodArgumentResolver(adapterRegistry));
|
resolverConfigurer.addCustomResolver(new ModelMethodArgumentResolver(adapterRegistry));
|
||||||
|
|
||||||
ControllerMethodResolver methodResolver = new ControllerMethodResolver(
|
ControllerMethodResolver methodResolver = new ControllerMethodResolver(
|
||||||
resolverConfigurer, adapterRegistry, new StaticApplicationContext(), Collections.emptyList());
|
resolverConfigurer, adapterRegistry, new StaticApplicationContext(),
|
||||||
|
Collections.emptyList(), null);
|
||||||
|
|
||||||
this.modelInitializer = new ModelInitializer(methodResolver, adapterRegistry);
|
this.modelInitializer = new ModelInitializer(methodResolver, adapterRegistry);
|
||||||
}
|
}
|
||||||
|
@ -210,7 +211,7 @@ public class ModelInitializerTests {
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
WebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
|
WebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
|
||||||
return new InitBinderBindingContext(bindingInitializer, binderMethods);
|
return new InitBinderBindingContext(bindingInitializer, binderMethods, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue