Support for reactive controller @InitBinder methods

Issue: SPR-14543
This commit is contained in:
Rossen Stoyanchev 2016-10-31 16:08:28 +02:00
parent 0b76b6d7e9
commit 3da0295c12
12 changed files with 559 additions and 94 deletions

View File

@ -15,11 +15,7 @@
*/ */
package org.springframework.web.reactive.result.method; package org.springframework.web.reactive.result.method;
import java.lang.reflect.Field;
import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeConverter;
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.validation.support.BindingAwareModelMap; import org.springframework.validation.support.BindingAwareModelMap;
import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.WebDataBinder;
@ -27,6 +23,7 @@ import org.springframework.web.bind.WebExchangeDataBinder;
import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
/** /**
* A context for binding requests to method arguments that provides access to * A context for binding requests to method arguments that provides access to
* the default model, data binding, validation, and type conversion. * the default model, data binding, validation, and type conversion.
@ -34,7 +31,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 5.0 * @since 5.0
*/ */
public class BindingContext implements TypeConverter { public class BindingContext {
private final ModelMap model = new BindingAwareModelMap(); private final ModelMap model = new BindingAwareModelMap();
@ -68,46 +65,50 @@ public class BindingContext implements TypeConverter {
return this.model; return this.model;
} }
/**
* Create a {@link WebExchangeDataBinder} for the given object.
* @param exchange the current exchange
* @param target the object to create a data binder for, or {@code null} if
* creating a binder for a simple type
* @param objectName the name of the target object
* @return a Mono for the created {@link WebDataBinder} instance
*/
public WebExchangeDataBinder createBinder(ServerWebExchange exchange, Object target,
String objectName) {
WebExchangeDataBinder dataBinder = createBinderInstance(target, objectName); /**
* Create a {@link WebExchangeDataBinder} for applying data binding, type
* conversion, and validation on the given "target" object.
*
* @param exchange the current exchange
* @param target the object to create a data binder for
* @param name the name of the target object
* @return the {@link WebDataBinder} instance
*/
public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, Object target, String name) {
WebExchangeDataBinder dataBinder = createBinderInstance(target, name);
if (this.initializer != null) { if (this.initializer != null) {
this.initializer.initBinder(dataBinder); this.initializer.initBinder(dataBinder);
} }
return initBinder(dataBinder, exchange); return initDataBinder(dataBinder, exchange);
} }
/**
* Create a {@link WebExchangeDataBinder} without a "target" object, i.e.
* for applying type conversion to simple types.
*
* @param exchange the current exchange
* @param name the name of the target object
* @return a Mono for the created {@link WebDataBinder} instance
*/
public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, String name) {
return createDataBinder(exchange, null, name);
}
/**
* Create the data binder instance.
*/
protected WebExchangeDataBinder createBinderInstance(Object target, String objectName) { protected WebExchangeDataBinder createBinderInstance(Object target, String objectName) {
return new WebExchangeDataBinder(target, objectName); return new WebExchangeDataBinder(target, objectName);
} }
protected WebExchangeDataBinder initBinder(WebExchangeDataBinder binder, ServerWebExchange exchange) { /**
* Initialize the data binder instance for the given exchange.
*/
protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder,
ServerWebExchange exchange) {
return binder; return binder;
} }
public <T> T convertIfNecessary(Object value, Class<T> requiredType) throws TypeMismatchException {
return this.simpleValueTypeConverter.convertIfNecessary(value, requiredType);
}
public <T> T convertIfNecessary(Object value, Class<T> requiredType, MethodParameter methodParam)
throws TypeMismatchException {
return this.simpleValueTypeConverter.convertIfNecessary(value, requiredType, methodParam);
}
public <T> T convertIfNecessary(Object value, Class<T> requiredType, Field field)
throws TypeMismatchException {
return this.simpleValueTypeConverter.convertIfNecessary(value, requiredType, field);
}
} }

View File

@ -67,11 +67,24 @@ public class InvocableHandlerMethod extends HandlerMethod {
} }
public void setHandlerMethodArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { /**
* Set {@link HandlerMethodArgumentResolver}s to use to use for resolving
* method argument values.
*/
public void setArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
this.resolvers.clear(); this.resolvers.clear();
this.resolvers.addAll(resolvers); this.resolvers.addAll(resolvers);
} }
/**
* Set the ParameterNameDiscoverer for resolving parameter names when needed
* (e.g. default request attribute name).
* <p>Default is a {@link DefaultParameterNameDiscoverer}.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
@Override @Override
protected Method getBridgedMethod() { protected Method getBridgedMethod() {
return super.getBridgedMethod(); return super.getBridgedMethod();

View File

@ -16,6 +16,7 @@
package org.springframework.web.reactive.result.method; package org.springframework.web.reactive.result.method;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -48,11 +49,11 @@ public class SyncInvocableHandlerMethod extends InvocableHandlerMethod {
* all resolvers are {@link SyncHandlerMethodArgumentResolver}. * all resolvers are {@link SyncHandlerMethodArgumentResolver}.
*/ */
@Override @Override
public void setHandlerMethodArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { public void setArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.forEach(resolver -> resolvers.forEach(resolver ->
Assert.isInstanceOf(SyncHandlerMethodArgumentResolver.class, resolver, Assert.isInstanceOf(SyncHandlerMethodArgumentResolver.class, resolver,
"Expected sync argument resolver: " + resolver.getClass().getName())); "Expected sync argument resolver: " + resolver.getClass().getName()));
super.setHandlerMethodArgumentResolvers(resolvers); super.setArgumentResolvers(resolvers);
} }
/** /**

View File

@ -213,7 +213,7 @@ public abstract class AbstractMessageReaderArgumentResolver {
MethodParameter param, BindingContext binding, ServerWebExchange exchange) { MethodParameter param, BindingContext binding, ServerWebExchange exchange) {
String name = Conventions.getVariableNameForParameter(param); String name = Conventions.getVariableNameForParameter(param);
WebExchangeDataBinder binder = binding.createBinder(exchange, target, name); WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name);
binder.validate(validationHints); binder.validate(validationHints);
if (binder.getBindingResult().hasErrors()) { if (binder.getBindingResult().hasErrors()) {
throw new ServerWebInputException("Validation failed", param); throw new ServerWebInputException("Validation failed", param);

View File

@ -28,6 +28,7 @@ import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.reactive.result.method.BindingContext; import org.springframework.web.reactive.result.method.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
@ -93,7 +94,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
if ("".equals(arg) && namedValueInfo.defaultValue != null) { if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue); arg = resolveStringValue(namedValueInfo.defaultValue);
} }
arg = applyConversion(arg, parameter, bindingContext); arg = applyConversion(arg, namedValueInfo, parameter, bindingContext, exchange);
handleResolvedValue(arg, namedValueInfo.name, parameter, model, exchange); handleResolvedValue(arg, namedValueInfo.name, parameter, model, exchange);
return arg; return arg;
}) })
@ -168,9 +169,12 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
protected abstract Mono<Object> resolveName(String name, MethodParameter parameter, protected abstract Mono<Object> resolveName(String name, MethodParameter parameter,
ServerWebExchange exchange); ServerWebExchange exchange);
private Object applyConversion(Object value, MethodParameter parameter, BindingContext bindingContext) { private Object applyConversion(Object value, NamedValueInfo namedValueInfo, MethodParameter parameter,
BindingContext bindingContext, ServerWebExchange exchange) {
WebDataBinder binder = bindingContext.createDataBinder(exchange, namedValueInfo.name);
try { try {
value = bindingContext.convertIfNecessary(value, parameter.getParameterType(), parameter); value = binder.convertIfNecessary(value, parameter.getParameterType(), parameter);
} }
catch (ConversionNotSupportedException ex) { catch (ConversionNotSupportedException ex) {
throw new ServerErrorException("Conversion not supported.", parameter, ex); throw new ServerErrorException("Conversion not supported.", parameter, ex);
@ -193,7 +197,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
handleMissingValue(namedValueInfo.name, parameter, exchange); handleMissingValue(namedValueInfo.name, parameter, exchange);
} }
value = handleNullValue(namedValueInfo.name, value, parameter.getNestedParameterType()); value = handleNullValue(namedValueInfo.name, value, parameter.getNestedParameterType());
value = applyConversion(value, parameter, bindingContext); value = applyConversion(value, namedValueInfo, parameter, bindingContext, exchange);
handleResolvedValue(value, namedValueInfo.name, parameter, model, exchange); handleResolvedValue(value, namedValueInfo.name, parameter, model, exchange);
return Mono.justOrEmpty(value); return Mono.justOrEmpty(value);
} }

View File

@ -0,0 +1,87 @@
/*
* Copyright 2002-2016 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
*
* http://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.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.WebExchangeDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.result.method.BindingContext;
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
import org.springframework.web.server.ServerWebExchange;
/**
* An extension of {@link BindingContext} that uses {@code @InitBinder} methods
* to initialize a data binder instance.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class InitBinderBindingContext extends BindingContext {
private final List<SyncInvocableHandlerMethod> binderMethods;
/** BindingContext for @InitBinder method invocation */
private final BindingContext bindingContext;
public InitBinderBindingContext(WebBindingInitializer initializer,
List<SyncInvocableHandlerMethod> binderMethods) {
super(initializer);
this.binderMethods = binderMethods;
this.bindingContext = new BindingContext(initializer);
}
@Override
protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder,
ServerWebExchange exchange) {
this.binderMethods.stream()
.filter(method -> isBinderMethodApplicable(method, binder))
.forEach(method -> invokeInitBinderMethod(binder, exchange, method));
return binder;
}
/**
* Whether the given {@code @InitBinder} method should be used to initialize
* the given WebDataBinder instance. By default we check the attributes
* names of the annotation, if present.
*/
protected boolean isBinderMethodApplicable(HandlerMethod binderMethod, WebDataBinder binder) {
InitBinder annot = binderMethod.getMethodAnnotation(InitBinder.class);
Collection<String> names = Arrays.asList(annot.value());
return (names.size() == 0 || names.contains(binder.getObjectName()));
}
private void invokeInitBinderMethod(WebExchangeDataBinder binder, ServerWebExchange exchange,
SyncInvocableHandlerMethod method) {
HandlerResult result = method.invokeForHandlerResult(exchange, this.bindingContext, binder);
if (result.getReturnValue().isPresent()) {
throw new IllegalStateException("@InitBinder methods should return void: " + method);
}
}
}

View File

@ -20,6 +20,7 @@ import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -31,12 +32,16 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.codec.ByteArrayDecoder; import org.springframework.core.codec.ByteArrayDecoder;
import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.StringDecoder; import org.springframework.core.codec.StringDecoder;
import org.springframework.http.codec.DecoderHttpMessageReader; import org.springframework.http.codec.DecoderHttpMessageReader;
import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageReader;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.InitBinder;
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.annotation.ExceptionHandlerMethodResolver; import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
@ -45,6 +50,8 @@ import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.result.method.BindingContext; import org.springframework.web.reactive.result.method.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.InvocableHandlerMethod; import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
/** /**
@ -57,6 +64,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class);
private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>(10); private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>(10);
private WebBindingInitializer webBindingInitializer; private WebBindingInitializer webBindingInitializer;
@ -67,8 +75,15 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
private List<HandlerMethodArgumentResolver> argumentResolvers; private List<HandlerMethodArgumentResolver> argumentResolvers;
private List<SyncHandlerMethodArgumentResolver> customInitBinderArgumentResolvers;
private List<SyncHandlerMethodArgumentResolver> initBinderArgumentResolvers;
private ConfigurableBeanFactory beanFactory; private ConfigurableBeanFactory beanFactory;
private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache = private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<>(64); new ConcurrentHashMap<>(64);
@ -119,10 +134,10 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
} }
/** /**
* Provide custom argument resolvers without overriding the built-in ones. * Configure custom argument resolvers without overriding the built-in ones.
*/ */
public void setCustomArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { public void setCustomArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
this.customArgumentResolvers = argumentResolvers; this.customArgumentResolvers = resolvers;
} }
/** /**
@ -147,6 +162,39 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
return this.argumentResolvers; return this.argumentResolvers;
} }
/**
* Configure custom argument resolvers for {@code @InitBinder} methods.
*/
public void setCustomInitBinderArgumentResolvers(List<SyncHandlerMethodArgumentResolver> resolvers) {
this.customInitBinderArgumentResolvers = resolvers;
}
/**
* Return the custom {@code @InitBinder} argument resolvers.
*/
public List<SyncHandlerMethodArgumentResolver> getCustomInitBinderArgumentResolvers() {
return this.customInitBinderArgumentResolvers;
}
/**
* Configure the supported argument types in {@code @InitBinder} methods.
*/
public void setInitBinderArgumentResolvers(List<SyncHandlerMethodArgumentResolver> resolvers) {
this.initBinderArgumentResolvers = null;
if (resolvers != null) {
this.initBinderArgumentResolvers = new ArrayList<>();
this.initBinderArgumentResolvers.addAll(resolvers);
}
}
/**
* Return the configured argument resolvers for {@code @InitBinder} methods.
*/
public List<SyncHandlerMethodArgumentResolver> getInitBinderArgumentResolvers() {
return this.initBinderArgumentResolvers;
}
/** /**
* A {@link ConfigurableBeanFactory} is expected for resolving expressions * A {@link ConfigurableBeanFactory} is expected for resolving expressions
* in method argument default values. * in method argument default values.
@ -166,21 +214,22 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
if (this.argumentResolvers == null) { if (this.argumentResolvers == null) {
this.argumentResolvers = initArgumentResolvers(); this.argumentResolvers = getDefaultArgumentResolvers();
}
if (this.initBinderArgumentResolvers == null) {
this.initBinderArgumentResolvers = getDefaultInitBinderArgumentResolvers();
} }
} }
protected List<HandlerMethodArgumentResolver> initArgumentResolvers() { protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
ReactiveAdapterRegistry adapterRegistry = getReactiveAdapterRegistry();
// Annotation-based argument resolution // Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory())); resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory()));
resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), adapterRegistry)); resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry()));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory()));
@ -189,7 +238,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
resolvers.add(new RequestAttributeMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestAttributeMethodArgumentResolver(getBeanFactory()));
// Type-based argument resolution // Type-based argument resolution
resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), adapterRegistry)); resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry()));
resolvers.add(new ModelArgumentResolver()); resolvers.add(new ModelArgumentResolver());
resolvers.add(new ServerWebExchangeArgumentResolver()); resolvers.add(new ServerWebExchangeArgumentResolver());
resolvers.add(new WebSessionArgumentResolver()); resolvers.add(new WebSessionArgumentResolver());
@ -204,6 +253,35 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
return resolvers; return resolvers;
} }
protected List<SyncHandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
List<SyncHandlerMethodArgumentResolver> resolvers = new ArrayList<>();
// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory()));
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestAttributeMethodArgumentResolver(getBeanFactory()));
// Type-based argument resolution
resolvers.add(new ModelArgumentResolver());
resolvers.add(new ServerWebExchangeArgumentResolver());
// Custom resolvers
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomInitBinderArgumentResolvers());
}
// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
return resolvers;
}
@Override @Override
public boolean supports(Object handler) { public boolean supports(Object handler) {
return HandlerMethod.class.equals(handler.getClass()); return HandlerMethod.class.equals(handler.getClass());
@ -213,8 +291,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) { public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler; HandlerMethod handlerMethod = (HandlerMethod) handler;
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); invocable.setArgumentResolvers(getArgumentResolvers());
BindingContext bindingContext = new BindingContext(getWebBindingInitializer()); BindingContext bindingContext = getBindingContext(handlerMethod);
return invocable.invoke(exchange, bindingContext) return invocable.invoke(exchange, bindingContext)
.map(result -> result.setExceptionHandler( .map(result -> result.setExceptionHandler(
ex -> handleException(ex, handlerMethod, bindingContext, exchange))) ex -> handleException(ex, handlerMethod, bindingContext, exchange)))
@ -222,6 +300,24 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
ex, handlerMethod, bindingContext, exchange)); ex, handlerMethod, bindingContext, exchange));
} }
private BindingContext getBindingContext(HandlerMethod handlerMethod) {
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
List<SyncInvocableHandlerMethod> initBinderMethods = new ArrayList<>();
for (Method method : methods) {
Object bean = handlerMethod.getBean();
SyncInvocableHandlerMethod initBinderMethod = new SyncInvocableHandlerMethod(bean, method);
initBinderMethod.setArgumentResolvers(new ArrayList<>(this.initBinderArgumentResolvers));
initBinderMethods.add(initBinderMethod);
}
return new InitBinderBindingContext(getWebBindingInitializer(), initBinderMethods);
}
private Mono<HandlerResult> handleException(Throwable ex, HandlerMethod handlerMethod, private Mono<HandlerResult> handleException(Throwable ex, HandlerMethod handlerMethod,
BindingContext bindingContext, ServerWebExchange exchange) { BindingContext bindingContext, ServerWebExchange exchange) {
@ -231,7 +327,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod()); logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod());
} }
invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers()); invocable.setArgumentResolvers(getArgumentResolvers());
bindingContext.getModel().clear(); bindingContext.getModel().clear();
return invocable.invoke(exchange, bindingContext, ex); return invocable.invoke(exchange, bindingContext, ex);
} }
@ -259,4 +355,11 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
return (method != null ? new InvocableHandlerMethod(handlerMethod.getBean(), method) : null); return (method != null ? new InvocableHandlerMethod(handlerMethod.getBean(), method) : null);
} }
/**
* MethodFilter that matches {@link InitBinder @InitBinder} methods.
*/
public static final ReflectionUtils.MethodFilter INIT_BINDER_METHODS =
method -> AnnotationUtils.findAnnotation(method, InitBinder.class) != null;
} }

View File

@ -154,7 +154,7 @@ public class InvocableHandlerMethodTests {
HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class); HandlerMethodArgumentResolver resolver = mock(HandlerMethodArgumentResolver.class);
when(resolver.supportsParameter(any())).thenReturn(true); when(resolver.supportsParameter(any())).thenReturn(true);
when(resolver.resolveArgument(any(), any(), any())).thenReturn(resolvedValue); when(resolver.resolveArgument(any(), any(), any())).thenReturn(resolvedValue);
handlerMethod.setHandlerMethodArgumentResolvers(Collections.singletonList(resolver)); handlerMethod.setArgumentResolvers(Collections.singletonList(resolver));
} }
private void assertHandlerResultValue(Mono<HandlerResult> mono, String expected) { private void assertHandlerResultValue(Mono<HandlerResult> mono, String expected) {

View File

@ -0,0 +1,163 @@
/*
* Copyright 2002-2016 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
*
* http://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.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.reactive.result.method.BindingContext;
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
/**
* Unit tests for {@link InitBinderBindingContext}.
* @author Rossen Stoyanchev
*/
public class InitBinderBindingContextTests {
private final ConfigurableWebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
private final List<SyncHandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
private final ServerWebExchange exchange = new DefaultServerWebExchange(
new MockServerHttpRequest(), new MockServerHttpResponse(), new DefaultWebSessionManager());
@Test
public void createBinder() throws Exception {
BindingContext context = createBindingContext("initBinder", WebDataBinder.class);
WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, null);
assertNotNull(dataBinder.getDisallowedFields());
assertEquals("id", dataBinder.getDisallowedFields()[0]);
}
@Test
public void createBinderWithGlobalInitialization() throws Exception {
ConversionService conversionService = new DefaultFormattingConversionService();
bindingInitializer.setConversionService(conversionService);
BindingContext context = createBindingContext("initBinder", WebDataBinder.class);
WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, null);
assertSame(conversionService, dataBinder.getConversionService());
}
@Test
public void createBinderWithAttrName() throws Exception {
BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class);
WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, "foo");
assertNotNull(dataBinder.getDisallowedFields());
assertEquals("id", dataBinder.getDisallowedFields()[0]);
}
@Test
public void createBinderWithAttrNameNoMatch() throws Exception {
BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class);
WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, "invalidName");
assertNull(dataBinder.getDisallowedFields());
}
@Test
public void createBinderNullAttrName() throws Exception {
BindingContext context = createBindingContext("initBinderWithAttributeName", WebDataBinder.class);
WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, null);
assertNull(dataBinder.getDisallowedFields());
}
@Test(expected = IllegalStateException.class)
public void returnValueNotExpected() throws Exception {
BindingContext context = createBindingContext("initBinderReturnValue", WebDataBinder.class);
context.createDataBinder(this.exchange, null, "invalidName");
}
@Test
public void createBinderTypeConversion() throws Exception {
this.exchange.getRequest().getQueryParams().add("requestParam", "22");
this.argumentResolvers.add(new RequestParamMethodArgumentResolver(null, false));
BindingContext context = createBindingContext("initBinderTypeConversion", WebDataBinder.class, int.class);
WebDataBinder dataBinder = context.createDataBinder(this.exchange, null, "foo");
assertNotNull(dataBinder.getDisallowedFields());
assertEquals("requestParam-22", dataBinder.getDisallowedFields()[0]);
}
private BindingContext createBindingContext(String methodName, Class<?>... parameterTypes)
throws Exception {
Object handler = new InitBinderHandler();
Method method = handler.getClass().getMethod(methodName, parameterTypes);
SyncInvocableHandlerMethod handlerMethod = new SyncInvocableHandlerMethod(handler, method);
handlerMethod.setArgumentResolvers(new ArrayList<>(this.argumentResolvers));
handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer());
return new InitBinderBindingContext(
this.bindingInitializer, Collections.singletonList(handlerMethod));
}
private static class InitBinderHandler {
@InitBinder
public void initBinder(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id");
}
@InitBinder(value="foo")
public void initBinderWithAttributeName(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id");
}
@InitBinder
public String initBinderReturnValue(WebDataBinder dataBinder) {
return "invalid";
}
@InitBinder
public void initBinderTypeConversion(WebDataBinder dataBinder, @RequestParam int requestParam) {
dataBinder.setDisallowedFields("requestParam-" + requestParam);
}
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2002-2016 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
*
* http://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.text.SimpleDateFormat;
import java.util.Date;
import org.junit.Test;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.config.EnableWebReactive;
import static org.junit.Assert.assertEquals;
/**
* Data binding and type conversion related integration tests for
* {@code @Controller}-annotated classes.
*
* @author Rossen Stoyanchev
*/
public class RequestMappingDataBindingIntegrationTests extends AbstractRequestMappingIntegrationTests {
@Override
protected ApplicationContext initApplicationContext() {
AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext();
wac.register(WebConfig.class);
wac.refresh();
return wac;
}
@Test
public void handleDateParam() throws Exception {
assertEquals("Processed date!",
performPost("/date-param?date=2016-10-31&date-pattern=YYYY-mm-dd",
new HttpHeaders(), null, String.class).getBody());
}
@Configuration
@EnableWebReactive
@ComponentScan(resourcePattern = "**/RequestMappingDataBindingIntegrationTests*.class")
@SuppressWarnings({"unused", "WeakerAccess"})
static class WebConfig {
}
@Controller
@SuppressWarnings("unused")
private static class TestController {
@InitBinder
public void initBinder(WebDataBinder dataBinder, @RequestParam("date-pattern") String pattern) {
CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(pattern), false);
dataBinder.registerCustomEditor(Date.class, dateEditor);
}
@PostMapping("/date-param")
@ResponseBody
public String handleDateParam(@RequestParam Date date) {
return "Processed date!";
}
}
}

View File

@ -39,12 +39,15 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory {
private final List<InvocableHandlerMethod> binderMethods; private final List<InvocableHandlerMethod> binderMethods;
/** /**
* Create a new instance. * Create a new instance.
* @param binderMethods {@code @InitBinder} methods, or {@code null} * @param binderMethods {@code @InitBinder} methods, or {@code null}
* @param initializer for global data binder intialization * @param initializer for global data binder intialization
*/ */
public InitBinderDataBinderFactory(List<InvocableHandlerMethod> binderMethods, WebBindingInitializer initializer) { public InitBinderDataBinderFactory(List<InvocableHandlerMethod> binderMethods,
WebBindingInitializer initializer) {
super(initializer); super(initializer);
this.binderMethods = (binderMethods != null) ? binderMethods : new ArrayList<>(); this.binderMethods = (binderMethods != null) ? binderMethods : new ArrayList<>();
} }
@ -61,20 +64,20 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory {
if (isBinderMethodApplicable(binderMethod, binder)) { if (isBinderMethodApplicable(binderMethod, binder)) {
Object returnValue = binderMethod.invokeForRequest(request, null, binder); Object returnValue = binderMethod.invokeForRequest(request, null, binder);
if (returnValue != null) { if (returnValue != null) {
throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod); throw new IllegalStateException(
"@InitBinder methods should return void: " + binderMethod);
} }
} }
} }
} }
/** /**
* Return {@code true} if the given {@code @InitBinder} method should be * Whether the given {@code @InitBinder} method should be used to initialize
* invoked to initialize the given WebDataBinder. * the given WebDataBinder instance. By default we check the attributes
* <p>The default implementation checks if target object name is included * names of the annotation, if present.
* in the attribute names specified in the {@code @InitBinder} annotation.
*/ */
protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder binder) { protected boolean isBinderMethodApplicable(HandlerMethod binderMethod, WebDataBinder binder) {
InitBinder annot = initBinderMethod.getMethodAnnotation(InitBinder.class); InitBinder annot = binderMethod.getMethodAnnotation(InitBinder.class);
Collection<String> names = Arrays.asList(annot.value()); Collection<String> names = Arrays.asList(annot.value());
return (names.size() == 0 || names.contains(binder.getObjectName())); return (names.size() == 0 || names.contains(binder.getObjectName()));
} }

View File

@ -17,9 +17,8 @@
package org.springframework.web.method.annotation; package org.springframework.web.method.annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Arrays; import java.util.Collections;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
@ -37,7 +36,10 @@ import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.method.support.InvocableHandlerMethod; import org.springframework.web.method.support.InvocableHandlerMethod;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
/** /**
* Test fixture with {@link InitBinderDataBinderFactory}. * Test fixture with {@link InitBinderDataBinderFactory}.
@ -46,23 +48,19 @@ import static org.junit.Assert.*;
*/ */
public class InitBinderDataBinderFactoryTests { public class InitBinderDataBinderFactoryTests {
private ConfigurableWebBindingInitializer bindingInitializer; private final ConfigurableWebBindingInitializer bindingInitializer =
new ConfigurableWebBindingInitializer();
private HandlerMethodArgumentResolverComposite argumentResolvers; private final HandlerMethodArgumentResolverComposite argumentResolvers =
new HandlerMethodArgumentResolverComposite();
private NativeWebRequest webRequest; private final NativeWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest());
@Before
public void setUp() throws Exception {
bindingInitializer = new ConfigurableWebBindingInitializer();
argumentResolvers = new HandlerMethodArgumentResolverComposite();
webRequest = new ServletWebRequest(new MockHttpServletRequest());
}
@Test @Test
public void createBinder() throws Exception { public void createBinder() throws Exception {
WebDataBinderFactory factory = createBinderFactory("initBinder", WebDataBinder.class); WebDataBinderFactory factory = createFactory("initBinder", WebDataBinder.class);
WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, null);
assertNotNull(dataBinder.getDisallowedFields()); assertNotNull(dataBinder.getDisallowedFields());
assertEquals("id", dataBinder.getDisallowedFields()[0]); assertEquals("id", dataBinder.getDisallowedFields()[0]);
@ -73,16 +71,16 @@ public class InitBinderDataBinderFactoryTests {
ConversionService conversionService = new DefaultFormattingConversionService(); ConversionService conversionService = new DefaultFormattingConversionService();
bindingInitializer.setConversionService(conversionService); bindingInitializer.setConversionService(conversionService);
WebDataBinderFactory factory = createBinderFactory("initBinder", WebDataBinder.class); WebDataBinderFactory factory = createFactory("initBinder", WebDataBinder.class);
WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, null);
assertSame(conversionService, dataBinder.getConversionService()); assertSame(conversionService, dataBinder.getConversionService());
} }
@Test @Test
public void createBinderWithAttrName() throws Exception { public void createBinderWithAttrName() throws Exception {
WebDataBinderFactory factory = createBinderFactory("initBinderWithAttributeName", WebDataBinder.class); WebDataBinderFactory factory = createFactory("initBinderWithAttributeName", WebDataBinder.class);
WebDataBinder dataBinder = factory.createBinder(webRequest, null, "foo"); WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, "foo");
assertNotNull(dataBinder.getDisallowedFields()); assertNotNull(dataBinder.getDisallowedFields());
assertEquals("id", dataBinder.getDisallowedFields()[0]); assertEquals("id", dataBinder.getDisallowedFields()[0]);
@ -90,52 +88,54 @@ public class InitBinderDataBinderFactoryTests {
@Test @Test
public void createBinderWithAttrNameNoMatch() throws Exception { public void createBinderWithAttrNameNoMatch() throws Exception {
WebDataBinderFactory factory = createBinderFactory("initBinderWithAttributeName", WebDataBinder.class); WebDataBinderFactory factory = createFactory("initBinderWithAttributeName", WebDataBinder.class);
WebDataBinder dataBinder = factory.createBinder(webRequest, null, "invalidName"); WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, "invalidName");
assertNull(dataBinder.getDisallowedFields()); assertNull(dataBinder.getDisallowedFields());
} }
@Test @Test
public void createBinderNullAttrName() throws Exception { public void createBinderNullAttrName() throws Exception {
WebDataBinderFactory factory = createBinderFactory("initBinderWithAttributeName", WebDataBinder.class); WebDataBinderFactory factory = createFactory("initBinderWithAttributeName", WebDataBinder.class);
WebDataBinder dataBinder = factory.createBinder(webRequest, null, null); WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, null);
assertNull(dataBinder.getDisallowedFields()); assertNull(dataBinder.getDisallowedFields());
} }
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void returnValueNotExpected() throws Exception { public void returnValueNotExpected() throws Exception {
WebDataBinderFactory factory = createBinderFactory("initBinderReturnValue", WebDataBinder.class); WebDataBinderFactory factory = createFactory("initBinderReturnValue", WebDataBinder.class);
factory.createBinder(webRequest, null, "invalidName"); factory.createBinder(this.webRequest, null, "invalidName");
} }
@Test @Test
public void createBinderTypeConversion() throws Exception { public void createBinderTypeConversion() throws Exception {
webRequest.getNativeRequest(MockHttpServletRequest.class).setParameter("requestParam", "22"); this.webRequest.getNativeRequest(MockHttpServletRequest.class).setParameter("requestParam", "22");
argumentResolvers.addResolver(new RequestParamMethodArgumentResolver(null, false)); this.argumentResolvers.addResolver(new RequestParamMethodArgumentResolver(null, false));
WebDataBinderFactory factory = createBinderFactory("initBinderTypeConversion", WebDataBinder.class, int.class); WebDataBinderFactory factory = createFactory("initBinderTypeConversion", WebDataBinder.class, int.class);
WebDataBinder dataBinder = factory.createBinder(webRequest, null, "foo"); WebDataBinder dataBinder = factory.createBinder(this.webRequest, null, "foo");
assertNotNull(dataBinder.getDisallowedFields()); assertNotNull(dataBinder.getDisallowedFields());
assertEquals("requestParam-22", dataBinder.getDisallowedFields()[0]); assertEquals("requestParam-22", dataBinder.getDisallowedFields()[0]);
} }
private WebDataBinderFactory createBinderFactory(String methodName, Class<?>... parameterTypes) private WebDataBinderFactory createFactory(String methodName, Class<?>... parameterTypes)
throws Exception { throws Exception {
Object handler = new InitBinderHandler(); Object handler = new InitBinderHandler();
Method method = handler.getClass().getMethod(methodName, parameterTypes); Method method = handler.getClass().getMethod(methodName, parameterTypes);
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method); InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method);
handlerMethod.setHandlerMethodArgumentResolvers(argumentResolvers); handlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
handlerMethod.setDataBinderFactory(new DefaultDataBinderFactory(null)); handlerMethod.setDataBinderFactory(new DefaultDataBinderFactory(null));
handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer()); handlerMethod.setParameterNameDiscoverer(new LocalVariableTableParameterNameDiscoverer());
return new InitBinderDataBinderFactory(Arrays.asList(handlerMethod), bindingInitializer); return new InitBinderDataBinderFactory(
Collections.singletonList(handlerMethod), this.bindingInitializer);
} }
private static class InitBinderHandler { private static class InitBinderHandler {
@InitBinder @InitBinder