Skip to content

Commit 6b89cf9

Browse files
committed
Add method validation to WebFlux
See gh-29825
1 parent bd054a4 commit 6b89cf9

12 files changed

+598
-18
lines changed

spring-webflux/spring-webflux.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies {
1111
optional(project(":spring-context"))
1212
optional(project(":spring-context-support")) // for FreeMarker support
1313
optional("jakarta.servlet:jakarta.servlet-api")
14+
optional("jakarta.validation:jakarta.validation-api")
1415
optional("jakarta.websocket:jakarta.websocket-api")
1516
optional("jakarta.websocket:jakarta.websocket-client-api")
1617
optional("org.webjars:webjars-locator-core")

spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java

+52-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,13 +16,17 @@
1616

1717
package org.springframework.web.reactive;
1818

19+
import java.lang.annotation.Annotation;
1920
import java.util.Collections;
2021
import java.util.Map;
2122

2223
import reactor.core.publisher.Mono;
2324

25+
import org.springframework.core.MethodParameter;
26+
import org.springframework.core.ReactiveAdapterRegistry;
2427
import org.springframework.lang.Nullable;
2528
import org.springframework.ui.Model;
29+
import org.springframework.validation.DataBinder;
2630
import org.springframework.validation.support.BindingAwareConcurrentModel;
2731
import org.springframework.web.bind.support.WebBindingInitializer;
2832
import org.springframework.web.bind.support.WebExchangeDataBinder;
@@ -50,6 +54,8 @@ public class BindingContext {
5054

5155
private final Model model = new BindingAwareConcurrentModel();
5256

57+
private boolean methodValidationApplicable;
58+
5359

5460
/**
5561
* Create a new {@code BindingContext}.
@@ -74,6 +80,16 @@ public Model getModel() {
7480
return this.model;
7581
}
7682

83+
/**
84+
* Configure flag to signal whether validation will be applied to handler
85+
* method arguments, which is the case if Bean Validation is enabled in
86+
* Spring MVC, and method parameters have {@code @Constraint} annotations.
87+
* @since 6.1
88+
*/
89+
public void setMethodValidationApplicable(boolean methodValidationApplicable) {
90+
this.methodValidationApplicable = methodValidationApplicable;
91+
}
92+
7793

7894
/**
7995
* Create a {@link WebExchangeDataBinder} to apply data binding and
@@ -112,6 +128,24 @@ public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, String
112128
return createDataBinder(exchange, null, name);
113129
}
114130

131+
/**
132+
* Variant of {@link #createDataBinder(ServerWebExchange, Object, String)}
133+
* with a {@link MethodParameter} for which the {@code DataBinder} is created.
134+
* That may provide more insight to initialize the {@link WebExchangeDataBinder}.
135+
* <p>By default, if the parameter has {@code @Valid}, Bean Validation is
136+
* excluded, deferring to method validation.
137+
* @since 6.1
138+
*/
139+
public WebExchangeDataBinder createDataBinder(
140+
ServerWebExchange exchange, @Nullable Object target, String name, MethodParameter parameter) {
141+
142+
WebExchangeDataBinder dataBinder = createDataBinder(exchange, target, name);
143+
if (this.methodValidationApplicable) {
144+
MethodValidationInitializer.updateBinder(dataBinder, parameter);
145+
}
146+
return dataBinder;
147+
}
148+
115149

116150
/**
117151
* Extended variant of {@link WebExchangeDataBinder}, adding path variables.
@@ -130,4 +164,21 @@ public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
130164
}
131165
}
132166

167+
168+
/**
169+
* Excludes Bean Validation if the method parameter has {@code @Valid}.
170+
*/
171+
private static class MethodValidationInitializer {
172+
173+
public static void updateBinder(DataBinder binder, MethodParameter parameter) {
174+
if (ReactiveAdapterRegistry.getSharedInstance().getAdapter(parameter.getParameterType()) == null) {
175+
for (Annotation annotation : parameter.getParameterAnnotations()) {
176+
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {
177+
binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator);
178+
}
179+
}
180+
}
181+
}
182+
}
183+
133184
}

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@
3737
import org.springframework.http.server.reactive.ServerHttpResponse;
3838
import org.springframework.lang.Nullable;
3939
import org.springframework.util.ObjectUtils;
40+
import org.springframework.validation.beanvalidation.MethodValidator;
4041
import org.springframework.web.method.HandlerMethod;
4142
import org.springframework.web.reactive.BindingContext;
4243
import org.springframework.web.reactive.HandlerResult;
@@ -56,6 +57,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
5657

5758
private static final Mono<Object[]> EMPTY_ARGS = Mono.just(new Object[0]);
5859

60+
private static final Class<?>[] EMPTY_GROUPS = new Class<?>[0];
61+
5962
private static final Object NO_ARG_VALUE = new Object();
6063

6164

@@ -65,6 +68,9 @@ public class InvocableHandlerMethod extends HandlerMethod {
6568

6669
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
6770

71+
@Nullable
72+
private MethodValidator methodValidator;
73+
6874

6975
/**
7076
* Create an instance from a {@code HandlerMethod}.
@@ -121,6 +127,16 @@ public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
121127
this.reactiveAdapterRegistry = registry;
122128
}
123129

130+
/**
131+
* Set the {@link MethodValidator} to perform method validation with if the
132+
* controller method {@link #shouldValidateArguments()} or
133+
* {@link #shouldValidateReturnValue()}.
134+
* @since 6.1
135+
*/
136+
public void setMethodValidator(@Nullable MethodValidator methodValidator) {
137+
this.methodValidator = methodValidator;
138+
}
139+
124140

125141
/**
126142
* Invoke the method for the given exchange.
@@ -134,6 +150,10 @@ public Mono<HandlerResult> invoke(
134150
ServerWebExchange exchange, BindingContext bindingContext, Object... providedArgs) {
135151

136152
return getMethodArgumentValues(exchange, bindingContext, providedArgs).flatMap(args -> {
153+
Class<?>[] groups = getValidationGroups();
154+
if (shouldValidateArguments() && this.methodValidator != null) {
155+
this.methodValidator.validateArguments(getBean(), getBridgedMethod(), args, groups);
156+
}
137157
Object value;
138158
Method method = getBridgedMethod();
139159
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
@@ -225,6 +245,11 @@ private void logArgumentErrorIfNecessary(ServerWebExchange exchange, MethodParam
225245
}
226246
}
227247

248+
private Class<?>[] getValidationGroups() {
249+
return ((shouldValidateArguments() || shouldValidateReturnValue()) && this.methodValidator != null ?
250+
this.methodValidator.determineValidationGroups(getBean(), getBridgedMethod()) : EMPTY_GROUPS);
251+
}
252+
228253
private static boolean isAsyncVoidReturnType(MethodParameter returnType, @Nullable ReactiveAdapter adapter) {
229254
if (adapter != null && adapter.supportsEmpty()) {
230255
if (adapter.isNoValue()) {

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ private void validate(Object target, Object[] validationHints, MethodParameter p
269269
BindingContext binding, ServerWebExchange exchange) {
270270

271271
String name = Conventions.getVariableNameForParameter(param);
272-
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name);
272+
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name, param);
273273
try {
274274
LocaleContextHolder.setLocaleContext(exchange.getLocaleContext());
275275
binder.validate(validationHints);

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@
3939
import org.springframework.lang.Nullable;
4040
import org.springframework.util.Assert;
4141
import org.springframework.util.ReflectionUtils.MethodFilter;
42+
import org.springframework.validation.beanvalidation.MethodValidator;
4243
import org.springframework.web.bind.annotation.InitBinder;
4344
import org.springframework.web.bind.annotation.ModelAttribute;
4445
import org.springframework.web.bind.annotation.RequestMapping;
@@ -94,6 +95,9 @@ class ControllerMethodResolver {
9495

9596
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
9697

98+
@Nullable
99+
private final MethodValidator methodValidator;
100+
97101
private final Map<Class<?>, Set<Method>> initBinderMethodCache = new ConcurrentHashMap<>(64);
98102

99103
private final Map<Class<?>, Set<Method>> modelAttributeMethodCache = new ConcurrentHashMap<>(64);
@@ -110,8 +114,10 @@ class ControllerMethodResolver {
110114
private final Map<Class<?>, SessionAttributesHandler> sessionAttributesHandlerCache = new ConcurrentHashMap<>(64);
111115

112116

113-
ControllerMethodResolver(ArgumentResolverConfigurer customResolvers, ReactiveAdapterRegistry adapterRegistry,
114-
ConfigurableApplicationContext context, List<HttpMessageReader<?>> readers) {
117+
ControllerMethodResolver(
118+
ArgumentResolverConfigurer customResolvers, ReactiveAdapterRegistry adapterRegistry,
119+
ConfigurableApplicationContext context, List<HttpMessageReader<?>> readers,
120+
@Nullable MethodValidator methodValidator) {
115121

116122
Assert.notNull(customResolvers, "ArgumentResolverConfigurer is required");
117123
Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required");
@@ -123,6 +129,7 @@ class ControllerMethodResolver {
123129
this.requestMappingResolvers = requestMappingResolvers(customResolvers, adapterRegistry, context, readers);
124130
this.exceptionHandlerResolvers = exceptionHandlerResolvers(customResolvers, adapterRegistry, context);
125131
this.reactiveAdapterRegistry = adapterRegistry;
132+
this.methodValidator = methodValidator;
126133

127134
initControllerAdviceCaches(context);
128135
}
@@ -260,6 +267,7 @@ public InvocableHandlerMethod getRequestMappingMethod(HandlerMethod handlerMetho
260267
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
261268
invocable.setArgumentResolvers(this.requestMappingResolvers);
262269
invocable.setReactiveAdapterRegistry(this.reactiveAdapterRegistry);
270+
invocable.setMethodValidator(this.methodValidator);
263271
return invocable;
264272
}
265273

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContext.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,12 +50,14 @@ class InitBinderBindingContext extends BindingContext {
5050
private Runnable saveModelOperation;
5151

5252

53-
InitBinderBindingContext(@Nullable WebBindingInitializer initializer,
54-
List<SyncInvocableHandlerMethod> binderMethods) {
53+
InitBinderBindingContext(
54+
@Nullable WebBindingInitializer initializer, List<SyncInvocableHandlerMethod> binderMethods,
55+
boolean methodValidationApplicable) {
5556

5657
super(initializer);
5758
this.binderMethods = binderMethods;
5859
this.binderMethodContext = new BindingContext(initializer);
60+
setMethodValidationApplicable(methodValidationApplicable);
5961
}
6062

6163

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelAttributeMethodArgumentResolver.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public Mono<Object> resolveArgument(
119119
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResultSink.asMono());
120120

121121
return valueMono.flatMap(value -> {
122-
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name);
122+
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name, parameter);
123123
return (bindingDisabled(parameter) ? Mono.empty() : bindRequestParameters(binder, exchange))
124124
.doOnError(bindingResultSink::tryEmitError)
125125
.doOnSuccess(aVoid -> {

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,9 +33,12 @@
3333
import org.springframework.http.codec.ServerCodecConfigurer;
3434
import org.springframework.lang.Nullable;
3535
import org.springframework.util.Assert;
36+
import org.springframework.util.ClassUtils;
3637
import org.springframework.util.CollectionUtils;
38+
import org.springframework.validation.beanvalidation.MethodValidator;
3739
import org.springframework.web.bind.support.WebBindingInitializer;
3840
import org.springframework.web.method.HandlerMethod;
41+
import org.springframework.web.method.support.HandlerMethodValidator;
3942
import org.springframework.web.reactive.BindingContext;
4043
import org.springframework.web.reactive.DispatchExceptionHandler;
4144
import org.springframework.web.reactive.HandlerAdapter;
@@ -57,6 +60,9 @@ public class RequestMappingHandlerAdapter
5760

5861
private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class);
5962

63+
private final static boolean BEAN_VALIDATION_PRESENT =
64+
ClassUtils.isPresent("jakarta.validation.Validator", HandlerMethod.class.getClassLoader());
65+
6066

6167
private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
6268

@@ -69,6 +75,9 @@ public class RequestMappingHandlerAdapter
6975
@Nullable
7076
private ReactiveAdapterRegistry reactiveAdapterRegistry;
7177

78+
@Nullable
79+
private MethodValidator methodValidator;
80+
7281
@Nullable
7382
private ConfigurableApplicationContext applicationContext;
7483

@@ -170,9 +179,12 @@ public void afterPropertiesSet() throws Exception {
170179
if (this.reactiveAdapterRegistry == null) {
171180
this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
172181
}
182+
if (BEAN_VALIDATION_PRESENT) {
183+
this.methodValidator = HandlerMethodValidator.from(this.webBindingInitializer, null);
184+
}
173185

174186
this.methodResolver = new ControllerMethodResolver(this.argumentResolverConfigurer,
175-
this.reactiveAdapterRegistry, this.applicationContext, this.messageReaders);
187+
this.reactiveAdapterRegistry, this.applicationContext, this.messageReaders, this.methodValidator);
176188

177189
this.modelInitializer = new ModelInitializer(this.methodResolver, this.reactiveAdapterRegistry);
178190
}
@@ -189,7 +201,8 @@ public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
189201
Assert.state(this.methodResolver != null && this.modelInitializer != null, "Not initialized");
190202

191203
InitBinderBindingContext bindingContext = new InitBinderBindingContext(
192-
getWebBindingInitializer(), this.methodResolver.getInitBinderMethods(handlerMethod));
204+
this.webBindingInitializer, this.methodResolver.getInitBinderMethods(handlerMethod),
205+
this.methodValidator != null && handlerMethod.shouldValidateArguments());
193206

194207
InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);
195208

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ public void setup() {
7676
applicationContext.refresh();
7777

7878
this.methodResolver = new ControllerMethodResolver(
79-
resolvers, ReactiveAdapterRegistry.getSharedInstance(), applicationContext, codecs.getReaders());
79+
resolvers, ReactiveAdapterRegistry.getSharedInstance(), applicationContext,
80+
codecs.getReaders(), null);
8081

8182
Method method = ResolvableMethod.on(TestController.class).mockCall(TestController::handle).method();
8283
this.handlerMethod = new HandlerMethod(new TestController(), method);

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/InitBinderBindingContextTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -133,7 +133,8 @@ private BindingContext createBindingContext(String methodName, Class<?>... param
133133
handlerMethod.setArgumentResolvers(new ArrayList<>(this.argumentResolvers));
134134
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
135135

136-
return new InitBinderBindingContext(this.bindingInitializer, Collections.singletonList(handlerMethod));
136+
return new InitBinderBindingContext(
137+
this.bindingInitializer, Collections.singletonList(handlerMethod), false);
137138
}
138139

139140

0 commit comments

Comments
 (0)