diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java index 3d2d8ca29c2..13870f7b844 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java @@ -20,17 +20,24 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; @@ -47,9 +54,14 @@ import static org.springframework.core.MethodIntrospector.selectMethods; /** * Package-private class to assist {@link RequestMappingHandlerAdapter} with - * resolving and caching {@code @InitBinder}, {@code @ModelAttribute}, and - * {@code @ExceptionHandler} methods declared in the {@code @Controller} or in - * {@code @ControllerAdvice} components. + * resolving, initializing, and caching annotated methods declared in + * {@code @Controller} and {@code @ControllerAdvice} components: + * * * @author Rossen Stoyanchev * @since 5.0 @@ -59,37 +71,92 @@ class ControllerMethodResolver { private static Log logger = LogFactory.getLog(ControllerMethodResolver.class); - private final List argumentResolvers; + private final List initBinderResolvers; - private final List initBinderArgumentResolvers; + private final List modelAttributeResolvers; + private final List requestMappingResolvers; - private final Map, Set> binderMethodCache = new ConcurrentHashMap<>(64); + private final List exceptionHandlerResolvers; - private final Map, Set> attributeMethodCache = new ConcurrentHashMap<>(64); - private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = - new ConcurrentHashMap<>(64); + private final Map, Set> initBinderMethodCache = new ConcurrentHashMap<>(64); + private final Map, Set> modelAttributeMethodCache = new ConcurrentHashMap<>(64); - private final Map> binderAdviceCache = new LinkedHashMap<>(64); + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64); - private final Map> attributeAdviceCache = new LinkedHashMap<>(64); + + private final Map> initBinderAdviceCache = new LinkedHashMap<>(64); + + private final Map> modelAttributeAdviceCache = new LinkedHashMap<>(64); private final Map exceptionHandlerAdviceCache = new LinkedHashMap<>(64); - ControllerMethodResolver(List argumentResolvers, - List initBinderArgumentResolvers, - ApplicationContext applicationContext) { + ControllerMethodResolver(List customResolvers, + List> messageReaders, ReactiveAdapterRegistry reactiveRegistry, + ConfigurableApplicationContext applicationContext) { + + Assert.notNull(customResolvers, "'customResolvers' should not be null"); + Assert.notNull(reactiveRegistry, "ReactiveAdapterRegistry is required"); + Assert.notNull(applicationContext, "ConfigurableApplicationContext is required"); + + ResolverRegistrar registrar = ResolverRegistrar.customResolvers(customResolvers).basic(); + addResolversTo(registrar, reactiveRegistry, applicationContext); + this.initBinderResolvers = registrar.getSyncResolvers(); - this.argumentResolvers = argumentResolvers; - this.initBinderArgumentResolvers = initBinderArgumentResolvers; + registrar = ResolverRegistrar.customResolvers(customResolvers).modelAttributeSupport(); + addResolversTo(registrar, reactiveRegistry, applicationContext); + this.modelAttributeResolvers = registrar.getResolvers(); + + registrar = ResolverRegistrar.customResolvers(customResolvers).fullSupport(messageReaders); + addResolversTo(registrar, reactiveRegistry, applicationContext); + this.requestMappingResolvers = registrar.getResolvers(); + + registrar = ResolverRegistrar.customResolvers(customResolvers).basic(); + addResolversTo(registrar, reactiveRegistry, applicationContext); + this.exceptionHandlerResolvers = registrar.getResolvers(); initControllerAdviceCaches(applicationContext); } + private void addResolversTo(ResolverRegistrar registrar, + ReactiveAdapterRegistry reactiveRegistry, ConfigurableApplicationContext context) { + + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + + // Annotation-based... + registrar.add(new RequestParamMethodArgumentResolver(beanFactory, reactiveRegistry, false)); + registrar.add(new RequestParamMapMethodArgumentResolver(reactiveRegistry)); + registrar.add(new PathVariableMethodArgumentResolver(beanFactory, reactiveRegistry)); + registrar.add(new PathVariableMapMethodArgumentResolver(reactiveRegistry)); + registrar.addIfRequestBody(readers -> new RequestBodyArgumentResolver(readers, reactiveRegistry)); + registrar.addIfModelAttribute(() -> new ModelAttributeMethodArgumentResolver(reactiveRegistry, false)); + registrar.add(new RequestHeaderMethodArgumentResolver(beanFactory, reactiveRegistry)); + registrar.add(new RequestHeaderMapMethodArgumentResolver(reactiveRegistry)); + registrar.add(new CookieValueMethodArgumentResolver(beanFactory, reactiveRegistry)); + registrar.add(new ExpressionValueMethodArgumentResolver(beanFactory, reactiveRegistry)); + registrar.add(new SessionAttributeMethodArgumentResolver(beanFactory, reactiveRegistry)); + registrar.add(new RequestAttributeMethodArgumentResolver(beanFactory, reactiveRegistry)); + + // Type-based... + registrar.addIfRequestBody(readers -> new HttpEntityArgumentResolver(readers, reactiveRegistry)); + registrar.add(new ModelArgumentResolver(reactiveRegistry)); + registrar.addIfModelAttribute(() -> new ErrorsMethodArgumentResolver(reactiveRegistry)); + registrar.add(new ServerWebExchangeArgumentResolver(reactiveRegistry)); + registrar.add(new PrincipalArgumentResolver(reactiveRegistry)); + registrar.add(new WebSessionArgumentResolver(reactiveRegistry)); + + // Custom... + registrar.addCustomResolvers(); + + // Catch-all... + registrar.add(new RequestParamMethodArgumentResolver(beanFactory, reactiveRegistry, true)); + registrar.addIfModelAttribute(() -> new ModelAttributeMethodArgumentResolver(reactiveRegistry, true)); + } + private void initControllerAdviceCaches(ApplicationContext applicationContext) { if (applicationContext == null) { return; @@ -105,14 +172,14 @@ class ControllerMethodResolver { Class beanType = bean.getBeanType(); Set attrMethods = selectMethods(beanType, ATTRIBUTE_METHODS); if (!attrMethods.isEmpty()) { - this.attributeAdviceCache.put(bean, attrMethods); + this.modelAttributeAdviceCache.put(bean, attrMethods); if (logger.isInfoEnabled()) { logger.info("Detected @ModelAttribute methods in " + bean); } } Set binderMethods = selectMethods(beanType, BINDER_METHODS); if (!binderMethods.isEmpty()) { - this.binderAdviceCache.put(bean, binderMethods); + this.initBinderAdviceCache.put(bean, binderMethods); if (logger.isInfoEnabled()) { logger.info("Detected @InitBinder methods in " + bean); } @@ -129,101 +196,116 @@ class ControllerMethodResolver { /** - * Find {@code @InitBinder} methods from {@code @ControllerAdvice} - * components or from the same controller as the given request handling method. + * Return an {@link InvocableHandlerMethod} for the given + * {@code @RequestMapping} method initialized with argument resolvers. */ - public List resolveInitBinderMethods(HandlerMethod handlerMethod) { + public InvocableHandlerMethod getRequestMappingMethod(HandlerMethod handlerMethod) { + InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); + invocable.setArgumentResolvers(this.requestMappingResolvers); + return invocable; + } + + /** + * Find {@code @InitBinder} methods in {@code @ControllerAdvice} components + * or in the controller of the given {@code @RequestMapping} method. + */ + public List getInitBinderMethods(HandlerMethod handlerMethod) { List result = new ArrayList<>(); Class handlerType = handlerMethod.getBeanType(); // Global methods first - this.binderAdviceCache.entrySet().forEach(entry -> { + this.initBinderAdviceCache.entrySet().forEach(entry -> { if (entry.getKey().isApplicableToBeanType(handlerType)) { Object bean = entry.getKey().resolveBean(); - entry.getValue().forEach(method -> result.add(createBinderMethod(bean, method))); + entry.getValue().forEach(method -> result.add(getInitBinderMethod(bean, method))); } }); - this.binderMethodCache + this.initBinderMethodCache .computeIfAbsent(handlerType, aClass -> selectMethods(handlerType, BINDER_METHODS)) .forEach(method -> { Object bean = handlerMethod.getBean(); - result.add(createBinderMethod(bean, method)); + result.add(getInitBinderMethod(bean, method)); }); return result; } - private SyncInvocableHandlerMethod createBinderMethod(Object bean, Method method) { + private SyncInvocableHandlerMethod getInitBinderMethod(Object bean, Method method) { SyncInvocableHandlerMethod invocable = new SyncInvocableHandlerMethod(bean, method); - invocable.setArgumentResolvers(this.initBinderArgumentResolvers); + invocable.setArgumentResolvers(this.initBinderResolvers); return invocable; } /** - * Find {@code @ModelAttribute} methods from {@code @ControllerAdvice} - * components or from the same controller as the given request handling method. + * Find {@code @ModelAttribute} methods in {@code @ControllerAdvice} + * components or in the controller of the given {@code @RequestMapping} method. */ - public List resolveModelAttributeMethods(HandlerMethod handlerMethod) { + public List getModelAttributeMethods(HandlerMethod handlerMethod) { List result = new ArrayList<>(); Class handlerType = handlerMethod.getBeanType(); // Global methods first - this.attributeAdviceCache.entrySet().forEach(entry -> { + this.modelAttributeAdviceCache.entrySet().forEach(entry -> { if (entry.getKey().isApplicableToBeanType(handlerType)) { Object bean = entry.getKey().resolveBean(); - entry.getValue().forEach(method -> result.add(createHandlerMethod(bean, method))); + entry.getValue().forEach(method -> result.add(createAttributeMethod(bean, method))); } }); - this.attributeMethodCache + this.modelAttributeMethodCache .computeIfAbsent(handlerType, aClass -> selectMethods(handlerType, ATTRIBUTE_METHODS)) .forEach(method -> { Object bean = handlerMethod.getBean(); - result.add(createHandlerMethod(bean, method)); + result.add(createAttributeMethod(bean, method)); }); return result; } - private InvocableHandlerMethod createHandlerMethod(Object bean, Method method) { + private InvocableHandlerMethod createAttributeMethod(Object bean, Method method) { InvocableHandlerMethod invocable = new InvocableHandlerMethod(bean, method); - invocable.setArgumentResolvers(this.argumentResolvers); + invocable.setArgumentResolvers(this.modelAttributeResolvers); return invocable; } /** - * Find a matching {@code @ExceptionHandler} method from - * {@code @ControllerAdvice} components or from the same controller as the - * given request handling method. + * Find an {@code @ExceptionHandler} method in {@code @ControllerAdvice} + * components or in the controller of the given {@code @RequestMapping} method. */ - public InvocableHandlerMethod resolveExceptionHandlerMethod(Throwable ex, HandlerMethod handlerMethod) { + public Optional getExceptionHandlerMethod(Throwable ex, + HandlerMethod handlerMethod) { Class handlerType = handlerMethod.getBeanType(); - ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache - .computeIfAbsent(handlerType, ExceptionHandlerMethodResolver::new); - - return Optional - .ofNullable(resolver.resolveMethodByThrowable(ex)) - .map(method -> createHandlerMethod(handlerMethod.getBean(), method)) - .orElseGet(() -> - this.exceptionHandlerAdviceCache.entrySet().stream() - .map(entry -> { - if (entry.getKey().isApplicableToBeanType(handlerType)) { - Method method = entry.getValue().resolveMethodByThrowable(ex); - if (method != null) { - Object bean = entry.getKey().resolveBean(); - return createHandlerMethod(bean, method); - } - } - return null; - }) - .filter(Objects::nonNull) - .findFirst() - .orElse(null)); + // Controller-local first... + Object targetBean = handlerMethod.getBean(); + Method targetMethod = this.exceptionHandlerCache + .computeIfAbsent(handlerType, ExceptionHandlerMethodResolver::new) + .resolveMethodByThrowable(ex); + + if (targetMethod == null) { + // Global exception handlers... + for (ControllerAdviceBean advice : this.exceptionHandlerAdviceCache.keySet()) { + if (advice.isApplicableToBeanType(handlerType)) { + targetBean = advice.resolveBean(); + targetMethod = this.exceptionHandlerAdviceCache.get(advice).resolveMethodByThrowable(ex); + if (targetMethod != null) { + break; + } + } + } + } + + if (targetMethod == null) { + return Optional.empty(); + } + + InvocableHandlerMethod invocable = new InvocableHandlerMethod(targetBean, targetMethod); + invocable.setArgumentResolvers(this.exceptionHandlerResolvers); + return Optional.of(invocable); } @@ -236,4 +318,89 @@ class ControllerMethodResolver { (AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) && (AnnotationUtils.findAnnotation(method, ModelAttribute.class) != null); + + private static class ResolverRegistrar { + + private final List customResolvers; + + private final List> messageReaders; + + private final boolean modelAttributeSupported; + + private final List result = new ArrayList<>(); + + + private ResolverRegistrar(List customResolvers, + List> messageReaders, boolean modelAttribute) { + + this.customResolvers = new ArrayList<>(customResolvers); + this.messageReaders = messageReaders != null ? new ArrayList<>(messageReaders) : null; + this.modelAttributeSupported = modelAttribute; + } + + + public void add(HandlerMethodArgumentResolver resolver) { + this.result.add(resolver); + } + + public void addIfRequestBody(Function>, HandlerMethodArgumentResolver> function) { + if (this.messageReaders != null) { + add(function.apply(this.messageReaders)); + } + } + + public void addIfModelAttribute(Supplier supplier) { + if (this.modelAttributeSupported) { + add(supplier.get()); + } + } + + public void addCustomResolvers() { + this.customResolvers.forEach(this::add); + } + + + public List getResolvers() { + return this.result; + } + + public List getSyncResolvers() { + return this.result.stream() + .filter(resolver -> resolver instanceof SyncHandlerMethodArgumentResolver) + .map(resolver -> (SyncHandlerMethodArgumentResolver) resolver) + .collect(Collectors.toList()); + } + + + public static Builder customResolvers(List customResolvers) { + return new Builder(customResolvers); + } + + + public static class Builder { + + private final List customResolvers; + + + public Builder(List customResolvers) { + this.customResolvers = new ArrayList<>(customResolvers); + } + + + public ResolverRegistrar fullSupport(List> readers) { + Assert.notEmpty(readers, "No message readers"); + return new ResolverRegistrar(this.customResolvers, readers, true); + } + + public ResolverRegistrar modelAttributeSupport() { + return new ResolverRegistrar(this.customResolvers, null, true); + } + + public ResolverRegistrar basic() { + return new ResolverRegistrar(this.customResolvers, null, false); + } + } + + } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 89031a3c044..7b8a7efd2a2 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -46,8 +46,6 @@ import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; 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; /** @@ -61,28 +59,21 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application private static final Log logger = LogFactory.getLog(RequestMappingHandlerAdapter.class); - private final List> messageReaders = new ArrayList<>(10); + private final List> messageReaders = new ArrayList<>(32); private WebBindingInitializer webBindingInitializer; private ReactiveAdapterRegistry reactiveAdapterRegistry = new ReactiveAdapterRegistry(); - private List customArgumentResolvers; - - private List argumentResolvers; - - private List customInitBinderArgumentResolvers; - - private List initBinderArgumentResolvers; + private final List customArgumentResolvers = new ArrayList<>(8); private ConfigurableApplicationContext applicationContext; - private ControllerMethodResolver controllerMethodResolver; + private ControllerMethodResolver methodResolver; private ModelInitializer modelInitializer; - public RequestMappingHandlerAdapter() { this.messageReaders.add(new DecoderHttpMessageReader<>(new ByteArrayDecoder())); this.messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder())); @@ -143,66 +134,20 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application } /** - * Configure custom argument resolvers without overriding the built-in ones. + * Configure resolvers for custom controller method arguments. */ public void setCustomArgumentResolvers(List resolvers) { - this.customArgumentResolvers = resolvers; + this.customArgumentResolvers.clear(); + this.customArgumentResolvers.addAll(resolvers); } /** - * Return the custom argument resolvers. + * Return the configured custom argument resolvers. */ public List getCustomArgumentResolvers() { return this.customArgumentResolvers; } - /** - * Configure the complete list of supported argument types thus overriding - * the resolvers that would otherwise be configured by default. - */ - public void setArgumentResolvers(List resolvers) { - this.argumentResolvers = new ArrayList<>(resolvers); - } - - /** - * Return the configured argument resolvers. - */ - public List getArgumentResolvers() { - return this.argumentResolvers; - } - - /** - * Configure custom argument resolvers for {@code @InitBinder} methods. - */ - public void setCustomInitBinderArgumentResolvers(List resolvers) { - this.customInitBinderArgumentResolvers = resolvers; - } - - /** - * Return the custom {@code @InitBinder} argument resolvers. - */ - public List getCustomInitBinderArgumentResolvers() { - return this.customInitBinderArgumentResolvers; - } - - /** - * Configure the supported argument types in {@code @InitBinder} methods. - */ - public void setInitBinderArgumentResolvers(List 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 getInitBinderArgumentResolvers() { - return this.initBinderArgumentResolvers; - } - /** * A {@link ConfigurableApplicationContext} is expected for resolving * expressions in method argument default values as well as for @@ -227,83 +172,12 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application @Override public void afterPropertiesSet() throws Exception { - if (this.argumentResolvers == null) { - this.argumentResolvers = getDefaultArgumentResolvers(); - } - if (this.initBinderArgumentResolvers == null) { - this.initBinderArgumentResolvers = getDefaultInitBinderArgumentResolvers(); - } - - this.controllerMethodResolver = new ControllerMethodResolver( - getArgumentResolvers(), getInitBinderArgumentResolvers(), getApplicationContext()); + this.methodResolver = new ControllerMethodResolver(getCustomArgumentResolvers(), + getMessageReaders(), getReactiveAdapterRegistry(), getApplicationContext()); this.modelInitializer = new ModelInitializer(getReactiveAdapterRegistry()); } - protected List getDefaultArgumentResolvers() { - List resolvers = new ArrayList<>(); - - // Annotation-based argument resolution - resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry(), false)); - resolvers.add(new RequestParamMapMethodArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new PathVariableMapMethodArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry())); - resolvers.add(new ModelAttributeMethodArgumentResolver(getReactiveAdapterRegistry(), false)); - resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new RequestHeaderMapMethodArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new SessionAttributeMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new RequestAttributeMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - - // Type-based argument resolution - resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry())); - resolvers.add(new ModelArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new ErrorsMethodArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new ServerWebExchangeArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new PrincipalArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new WebSessionArgumentResolver(getReactiveAdapterRegistry())); - - // Custom resolvers - if (getCustomArgumentResolvers() != null) { - resolvers.addAll(getCustomArgumentResolvers()); - } - - // Catch-all - resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry(), true)); - resolvers.add(new ModelAttributeMethodArgumentResolver(getReactiveAdapterRegistry(), true)); - return resolvers; - } - - protected List getDefaultInitBinderArgumentResolvers() { - List resolvers = new ArrayList<>(); - - // Annotation-based argument resolution - resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry(), false)); - resolvers.add(new RequestParamMapMethodArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new PathVariableMapMethodArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new RequestHeaderMapMethodArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - resolvers.add(new RequestAttributeMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry())); - - // Type-based argument resolution - resolvers.add(new ModelArgumentResolver(getReactiveAdapterRegistry())); - resolvers.add(new ServerWebExchangeArgumentResolver(getReactiveAdapterRegistry())); - - // Custom resolvers - if (getCustomInitBinderArgumentResolvers() != null) { - resolvers.addAll(getCustomInitBinderArgumentResolvers()); - } - - // Catch-all - resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), getReactiveAdapterRegistry(), true)); - return resolvers; - } - @Override public boolean supports(Object handler) { @@ -317,53 +191,43 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application HandlerMethod handlerMethod = (HandlerMethod) handler; BindingContext bindingContext = new InitBinderBindingContext( - getWebBindingInitializer(), getInitBinderMethods(handlerMethod)); + getWebBindingInitializer(), this.methodResolver.getInitBinderMethods(handlerMethod)); - return this.modelInitializer - .initModel(bindingContext, getModelAttributeMethods(handlerMethod), exchange) - .then(() -> { - Function> exceptionHandler = - ex -> handleException(exchange, handlerMethod, bindingContext, ex); - - InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); - invocable.setArgumentResolvers(getArgumentResolvers()); - - return invocable.invoke(exchange, bindingContext) - .doOnNext(result -> result.setExceptionHandler(exceptionHandler)) - .otherwise(exceptionHandler); - }); - } + List modelAttributeMethods = + this.methodResolver.getModelAttributeMethods(handlerMethod); - private List getInitBinderMethods(HandlerMethod handlerMethod) { - return this.controllerMethodResolver.resolveInitBinderMethods(handlerMethod); - } - - private List getModelAttributeMethods(HandlerMethod handlerMethod) { - return this.controllerMethodResolver.resolveModelAttributeMethods(handlerMethod); - } + Function> exceptionHandler = + ex -> handleException(ex, handlerMethod, bindingContext, exchange); - private Mono handleException(ServerWebExchange exchange, HandlerMethod handlerMethod, - BindingContext bindingContext, Throwable ex) { - - InvocableHandlerMethod invocable = - this.controllerMethodResolver.resolveExceptionHandlerMethod(ex, handlerMethod); - - if (invocable != null) { - try { - if (logger.isDebugEnabled()) { - logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod()); - } - bindingContext.getModel().asMap().clear(); - Throwable cause = ex.getCause() != null ? ex.getCause() : ex; - return invocable.invoke(exchange, bindingContext, cause, handlerMethod); - } - catch (Throwable invocationEx) { - if (logger.isWarnEnabled()) { - logger.warn("Failed to invoke: " + invocable.getMethod(), invocationEx); - } - } - } - return Mono.error(ex); + return this.modelInitializer + .initModel(bindingContext, modelAttributeMethods, exchange) + .then(() -> this.methodResolver.getRequestMappingMethod(handlerMethod) + .invoke(exchange, bindingContext) + .doOnNext(result -> result.setExceptionHandler(exceptionHandler)) + .otherwise(exceptionHandler)); + } + + private Mono handleException(Throwable ex, HandlerMethod handlerMethod, + BindingContext bindingContext, ServerWebExchange exchange) { + + return this.methodResolver.getExceptionHandlerMethod(ex, handlerMethod) + .map(invocable -> { + try { + if (logger.isDebugEnabled()) { + logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod()); + } + bindingContext.getModel().asMap().clear(); + Throwable cause = ex.getCause() != null ? ex.getCause() : ex; + return invocable.invoke(exchange, bindingContext, cause, handlerMethod); + } + catch (Throwable invocationEx) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to invoke: " + invocable.getMethod(), invocationEx); + } + return null; + } + }) + .orElseGet(() -> Mono.error(ex)); } } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java new file mode 100644 index 00000000000..88805790eba --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolverTests.java @@ -0,0 +1,295 @@ +/* + * Copyright 2002-2017 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.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.codec.ByteArrayDecoder; +import org.springframework.core.codec.ByteBufferDecoder; +import org.springframework.http.HttpStatus; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.ResolvableMethod; +import org.springframework.web.reactive.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.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Unit tests for {@link ControllerMethodResolver}. + * @author Rossen Stoyanchev + */ +public class ControllerMethodResolverTests { + + private ControllerMethodResolver methodResolver; + + private HandlerMethod handlerMethod; + + + @Before + public void setUp() throws Exception { + + List customResolvers = + Arrays.asList(new CustomArgumentResolver(), new CustomSyncArgumentResolver()); + + List> messageReaders = Arrays.asList( + new DecoderHttpMessageReader<>(new ByteArrayDecoder()), + new DecoderHttpMessageReader<>(new ByteBufferDecoder())); + + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.registerBean(TestControllerAdvice.class); + applicationContext.refresh(); + + this.methodResolver = new ControllerMethodResolver( + customResolvers, messageReaders, new ReactiveAdapterRegistry(), applicationContext); + + Method method = ResolvableMethod.on(TestController.class).mockCall(TestController::handle).method(); + this.handlerMethod = new HandlerMethod(new TestController(), method); + } + + + @Test + public void requestMappingArgumentResolvers() throws Exception { + + InvocableHandlerMethod invocable = this.methodResolver.getRequestMappingMethod(this.handlerMethod); + List resolvers = invocable.getResolvers(); + + AtomicInteger index = new AtomicInteger(-1); + assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestBodyArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ModelAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestHeaderMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestHeaderMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(CookieValueMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ExpressionValueMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(SessionAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(HttpEntityArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ModelArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ErrorsMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ServerWebExchangeArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PrincipalArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(WebSessionArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(CustomArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(CustomSyncArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ModelAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + } + + @Test + public void modelAttributeArgumentResolvers() throws Exception { + + List methods = + this.methodResolver.getModelAttributeMethods(this.handlerMethod); + + assertEquals("Expected one each from Controller + ControllerAdvice", 2, methods.size()); + InvocableHandlerMethod invocable = methods.get(0); + List resolvers = invocable.getResolvers(); + + AtomicInteger index = new AtomicInteger(-1); + assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ModelAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestHeaderMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestHeaderMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(CookieValueMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ExpressionValueMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(SessionAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(ModelArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ErrorsMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ServerWebExchangeArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PrincipalArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(WebSessionArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(CustomArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(CustomSyncArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ModelAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + } + + @Test + public void initBinderArgumentResolvers() throws Exception { + + List methods = + this.methodResolver.getInitBinderMethods(this.handlerMethod); + + assertEquals("Expected one each from Controller + ControllerAdvice", 2, methods.size()); + SyncInvocableHandlerMethod invocable = methods.get(0); + List resolvers = invocable.getResolvers(); + + AtomicInteger index = new AtomicInteger(-1); + assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestHeaderMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestHeaderMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(CookieValueMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ExpressionValueMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(ModelArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ServerWebExchangeArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(CustomSyncArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass()); + } + + @Test + public void exceptionHandlerArgumentResolvers() throws Exception { + + Optional optional = + this.methodResolver.getExceptionHandlerMethod( + new ResponseStatusException(HttpStatus.BAD_REQUEST, "reason"), this.handlerMethod); + + InvocableHandlerMethod invocable = optional.orElseThrow(() -> new AssertionError("No match")); + assertEquals(TestController.class, invocable.getBeanType()); + List resolvers = invocable.getResolvers(); + + AtomicInteger index = new AtomicInteger(-1); + assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestParamMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PathVariableMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PathVariableMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestHeaderMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestHeaderMapMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(CookieValueMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ExpressionValueMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(SessionAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(RequestAttributeMethodArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(ModelArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(ServerWebExchangeArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(PrincipalArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(WebSessionArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(CustomArgumentResolver.class, next(resolvers, index).getClass()); + assertEquals(CustomSyncArgumentResolver.class, next(resolvers, index).getClass()); + + assertEquals(RequestParamMethodArgumentResolver.class, next(resolvers, index).getClass()); + } + + @Test + public void exceptionHandlerFromControllerAdvice() throws Exception { + + Optional optional = + this.methodResolver.getExceptionHandlerMethod( + new IllegalStateException("reason"), this.handlerMethod); + + InvocableHandlerMethod invocable = optional.orElseThrow(() -> new AssertionError("No match")); + assertNotNull(invocable); + assertEquals(TestControllerAdvice.class, invocable.getBeanType()); + } + + + private static HandlerMethodArgumentResolver next( + List resolvers, AtomicInteger index) { + + return resolvers.get(index.incrementAndGet()); + } + + + @Controller + private static class TestController { + + @InitBinder + void initDataBinder() {} + + @ModelAttribute + void initModel() {} + + @GetMapping + void handle() {} + + @ExceptionHandler + void handleException(ResponseStatusException ex) {} + + } + + @ControllerAdvice + private static class TestControllerAdvice { + + @InitBinder + void initDataBinder() {} + + @ModelAttribute + void initModel() {} + + @ExceptionHandler + void handleException(IllegalStateException ex) {} + + } + + private static class CustomArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter p) { + return false; + } + + @Override + public Mono resolveArgument(MethodParameter p, BindingContext c, ServerWebExchange e) { + return null; + } + } + + private static class CustomSyncArgumentResolver extends CustomArgumentResolver + implements SyncHandlerMethodArgumentResolver { + + @Override + public Optional resolveArgumentValue(MethodParameter p, BindingContext c, ServerWebExchange e) { + return null; + } + } + +}