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;
import java.lang.reflect.Field;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.TypeMismatchException;
import org.springframework.core.MethodParameter;
import org.springframework.ui.ModelMap;
import org.springframework.validation.support.BindingAwareModelMap;
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.server.ServerWebExchange;
/**
* A context for binding requests to method arguments that provides access to
* the default model, data binding, validation, and type conversion.
@ -34,7 +31,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class BindingContext implements TypeConverter {
public class BindingContext {
private final ModelMap model = new BindingAwareModelMap();
@ -68,46 +65,50 @@ public class BindingContext implements TypeConverter {
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) {
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) {
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;
}
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.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
protected Method getBridgedMethod() {
return super.getBridgedMethod();

View File

@ -16,6 +16,7 @@
package org.springframework.web.reactive.result.method;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import org.springframework.util.Assert;
@ -48,11 +49,11 @@ public class SyncInvocableHandlerMethod extends InvocableHandlerMethod {
* all resolvers are {@link SyncHandlerMethodArgumentResolver}.
*/
@Override
public void setHandlerMethodArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
public void setArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.forEach(resolver ->
Assert.isInstanceOf(SyncHandlerMethodArgumentResolver.class, resolver,
"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) {
String name = Conventions.getVariableNameForParameter(param);
WebExchangeDataBinder binder = binding.createBinder(exchange, target, name);
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name);
binder.validate(validationHints);
if (binder.getBindingResult().hasErrors()) {
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.core.MethodParameter;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.reactive.result.method.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
@ -93,7 +94,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
}
arg = applyConversion(arg, parameter, bindingContext);
arg = applyConversion(arg, namedValueInfo, parameter, bindingContext, exchange);
handleResolvedValue(arg, namedValueInfo.name, parameter, model, exchange);
return arg;
})
@ -168,9 +169,12 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
protected abstract Mono<Object> resolveName(String name, MethodParameter parameter,
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 {
value = bindingContext.convertIfNecessary(value, parameter.getParameterType(), parameter);
value = binder.convertIfNecessary(value, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new ServerErrorException("Conversion not supported.", parameter, ex);
@ -193,7 +197,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
handleMissingValue(namedValueInfo.name, parameter, exchange);
}
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);
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.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
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.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.codec.ByteArrayDecoder;
import org.springframework.core.codec.ByteBufferDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.http.codec.DecoderHttpMessageReader;
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.method.HandlerMethod;
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.HandlerMethodArgumentResolver;
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;
/**
@ -57,6 +64,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class);
private final List<HttpMessageReader<?>> messageReaders = new ArrayList<>(10);
private WebBindingInitializer webBindingInitializer;
@ -67,8 +75,15 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
private List<HandlerMethodArgumentResolver> argumentResolvers;
private List<SyncHandlerMethodArgumentResolver> customInitBinderArgumentResolvers;
private List<SyncHandlerMethodArgumentResolver> initBinderArgumentResolvers;
private ConfigurableBeanFactory beanFactory;
private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
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) {
this.customArgumentResolvers = argumentResolvers;
public void setCustomArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
this.customArgumentResolvers = resolvers;
}
/**
@ -147,6 +162,39 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
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
* in method argument default values.
@ -166,21 +214,22 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
@Override
public void afterPropertiesSet() throws Exception {
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<>();
ReactiveAdapterRegistry adapterRegistry = getReactiveAdapterRegistry();
// 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 RequestBodyArgumentResolver(getMessageReaders(), adapterRegistry));
resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry()));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory()));
@ -189,7 +238,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
resolvers.add(new RequestAttributeMethodArgumentResolver(getBeanFactory()));
// Type-based argument resolution
resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), adapterRegistry));
resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry()));
resolvers.add(new ModelArgumentResolver());
resolvers.add(new ServerWebExchangeArgumentResolver());
resolvers.add(new WebSessionArgumentResolver());
@ -204,6 +253,35 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
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
public boolean supports(Object handler) {
return HandlerMethod.class.equals(handler.getClass());
@ -213,8 +291,8 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers());
BindingContext bindingContext = new BindingContext(getWebBindingInitializer());
invocable.setArgumentResolvers(getArgumentResolvers());
BindingContext bindingContext = getBindingContext(handlerMethod);
return invocable.invoke(exchange, bindingContext)
.map(result -> result.setExceptionHandler(
ex -> handleException(ex, handlerMethod, bindingContext, exchange)))
@ -222,6 +300,24 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
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,
BindingContext bindingContext, ServerWebExchange exchange) {
@ -231,7 +327,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod());
}
invocable.setHandlerMethodArgumentResolvers(getArgumentResolvers());
invocable.setArgumentResolvers(getArgumentResolvers());
bindingContext.getModel().clear();
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);
}
/**
* 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);
when(resolver.supportsParameter(any())).thenReturn(true);
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) {

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;
/**
* Create a new instance.
* @param binderMethods {@code @InitBinder} methods, or {@code null}
* @param initializer for global data binder intialization
*/
public InitBinderDataBinderFactory(List<InvocableHandlerMethod> binderMethods, WebBindingInitializer initializer) {
public InitBinderDataBinderFactory(List<InvocableHandlerMethod> binderMethods,
WebBindingInitializer initializer) {
super(initializer);
this.binderMethods = (binderMethods != null) ? binderMethods : new ArrayList<>();
}
@ -61,20 +64,20 @@ public class InitBinderDataBinderFactory extends DefaultDataBinderFactory {
if (isBinderMethodApplicable(binderMethod, binder)) {
Object returnValue = binderMethod.invokeForRequest(request, null, binder);
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
* invoked to initialize the given WebDataBinder.
* <p>The default implementation checks if target object name is included
* in the attribute names specified in the {@code @InitBinder} annotation.
* 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 initBinderMethod, WebDataBinder binder) {
InitBinder annot = initBinderMethod.getMethodAnnotation(InitBinder.class);
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()));
}

View File

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