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 new file mode 100644 index 00000000000..3d2d8ca29c2 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ControllerMethodResolver.java @@ -0,0 +1,239 @@ +/* + * 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.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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.method.ControllerAdviceBean; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; +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 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. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +class ControllerMethodResolver { + + private static Log logger = LogFactory.getLog(ControllerMethodResolver.class); + + + private final List argumentResolvers; + + private final List initBinderArgumentResolvers; + + + private final Map, Set> binderMethodCache = new ConcurrentHashMap<>(64); + + private final Map, Set> attributeMethodCache = new ConcurrentHashMap<>(64); + + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = + new ConcurrentHashMap<>(64); + + + private final Map> binderAdviceCache = new LinkedHashMap<>(64); + + private final Map> attributeAdviceCache = new LinkedHashMap<>(64); + + private final Map exceptionHandlerAdviceCache = + new LinkedHashMap<>(64); + + + ControllerMethodResolver(List argumentResolvers, + List initBinderArgumentResolvers, + ApplicationContext applicationContext) { + + this.argumentResolvers = argumentResolvers; + this.initBinderArgumentResolvers = initBinderArgumentResolvers; + + initControllerAdviceCaches(applicationContext); + } + + private void initControllerAdviceCaches(ApplicationContext applicationContext) { + if (applicationContext == null) { + return; + } + if (logger.isInfoEnabled()) { + logger.info("Looking for @ControllerAdvice: " + applicationContext); + } + + List beans = ControllerAdviceBean.findAnnotatedBeans(applicationContext); + AnnotationAwareOrderComparator.sort(beans); + + for (ControllerAdviceBean bean : beans) { + Class beanType = bean.getBeanType(); + Set attrMethods = selectMethods(beanType, ATTRIBUTE_METHODS); + if (!attrMethods.isEmpty()) { + this.attributeAdviceCache.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); + if (logger.isInfoEnabled()) { + logger.info("Detected @InitBinder methods in " + bean); + } + } + ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType); + if (resolver.hasExceptionMappings()) { + this.exceptionHandlerAdviceCache.put(bean, resolver); + if (logger.isInfoEnabled()) { + logger.info("Detected @ExceptionHandler methods in " + bean); + } + } + } + } + + + /** + * Find {@code @InitBinder} methods from {@code @ControllerAdvice} + * components or from the same controller as the given request handling method. + */ + public List resolveInitBinderMethods(HandlerMethod handlerMethod) { + + List result = new ArrayList<>(); + Class handlerType = handlerMethod.getBeanType(); + + // Global methods first + this.binderAdviceCache.entrySet().forEach(entry -> { + if (entry.getKey().isApplicableToBeanType(handlerType)) { + Object bean = entry.getKey().resolveBean(); + entry.getValue().forEach(method -> result.add(createBinderMethod(bean, method))); + } + }); + + this.binderMethodCache + .computeIfAbsent(handlerType, aClass -> selectMethods(handlerType, BINDER_METHODS)) + .forEach(method -> { + Object bean = handlerMethod.getBean(); + result.add(createBinderMethod(bean, method)); + }); + + return result; + } + + private SyncInvocableHandlerMethod createBinderMethod(Object bean, Method method) { + SyncInvocableHandlerMethod invocable = new SyncInvocableHandlerMethod(bean, method); + invocable.setArgumentResolvers(this.initBinderArgumentResolvers); + return invocable; + } + + /** + * Find {@code @ModelAttribute} methods from {@code @ControllerAdvice} + * components or from the same controller as the given request handling method. + */ + public List resolveModelAttributeMethods(HandlerMethod handlerMethod) { + + List result = new ArrayList<>(); + Class handlerType = handlerMethod.getBeanType(); + + // Global methods first + this.attributeAdviceCache.entrySet().forEach(entry -> { + if (entry.getKey().isApplicableToBeanType(handlerType)) { + Object bean = entry.getKey().resolveBean(); + entry.getValue().forEach(method -> result.add(createHandlerMethod(bean, method))); + } + }); + + this.attributeMethodCache + .computeIfAbsent(handlerType, aClass -> selectMethods(handlerType, ATTRIBUTE_METHODS)) + .forEach(method -> { + Object bean = handlerMethod.getBean(); + result.add(createHandlerMethod(bean, method)); + }); + + return result; + } + + private InvocableHandlerMethod createHandlerMethod(Object bean, Method method) { + InvocableHandlerMethod invocable = new InvocableHandlerMethod(bean, method); + invocable.setArgumentResolvers(this.argumentResolvers); + return invocable; + } + + /** + * Find a matching {@code @ExceptionHandler} method from + * {@code @ControllerAdvice} components or from the same controller as the + * given request handling method. + */ + public InvocableHandlerMethod resolveExceptionHandlerMethod(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)); + } + + + /** Filter for {@link InitBinder @InitBinder} methods. */ + private static final ReflectionUtils.MethodFilter BINDER_METHODS = method -> + AnnotationUtils.findAnnotation(method, InitBinder.class) != null; + + /** Filter for {@link ModelAttribute @ModelAttribute} methods. */ + private static final ReflectionUtils.MethodFilter ATTRIBUTE_METHODS = method -> + (AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) && + (AnnotationUtils.findAnnotation(method, ModelAttribute.class) != null); + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelInitializer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelInitializer.java index caf8780cd6e..635e2111f74 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelInitializer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ModelInitializer.java @@ -38,8 +38,8 @@ import org.springframework.web.server.ServerWebExchange; /** - * Helper class to assist {@link RequestMappingHandlerAdapter} with - * initialization of the default model through {@code @ModelAttribute} methods. + * Package-private class to assist {@link RequestMappingHandlerAdapter} with + * default model initialization through {@code @ModelAttribute} methods. * * @author Rossen Stoyanchev * @since 5.0 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 bed81fff759..89031a3c044 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 @@ -16,15 +16,8 @@ package org.springframework.web.reactive.result.method.annotation; -import java.lang.reflect.Method; 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 org.apache.commons.logging.Log; @@ -37,25 +30,17 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; 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.core.codec.ByteArrayDecoder; import org.springframework.core.codec.ByteBufferDecoder; import org.springframework.core.codec.DataBufferDecoder; import org.springframework.core.codec.ResourceDecoder; import org.springframework.core.codec.StringDecoder; import org.springframework.http.codec.DecoderHttpMessageReader; -import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; -import org.springframework.web.bind.annotation.InitBinder; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.support.WebBindingInitializer; -import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.HandlerAdapter; import org.springframework.web.reactive.HandlerResult; @@ -65,8 +50,6 @@ import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentR import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod; import org.springframework.web.server.ServerWebExchange; -import static org.springframework.core.MethodIntrospector.selectMethods; - /** * Supports the invocation of {@code @RequestMapping} methods. * @@ -94,22 +77,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application private ConfigurableApplicationContext applicationContext; - - private final Map, Set> binderMethodCache = new ConcurrentHashMap<>(64); - - private final Map, Set> attributeMethodCache = new ConcurrentHashMap<>(64); - - private final Map, ExceptionHandlerMethodResolver> exceptionHandlerCache = - new ConcurrentHashMap<>(64); - - - private final Map> binderAdviceCache = new LinkedHashMap<>(64); - - private final Map> attributeAdviceCache = new LinkedHashMap<>(64); - - private final Map exceptionHandlerAdviceCache = - new LinkedHashMap<>(64); - + private ControllerMethodResolver controllerMethodResolver; private ModelInitializer modelInitializer; @@ -158,10 +126,18 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application return this.webBindingInitializer; } + /** + * Configure the registry for adapting various reactive types. + *

By default this is an instance of {@link ReactiveAdapterRegistry} with + * default settings. + */ public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { this.reactiveAdapterRegistry = registry; } + /** + * Return the configured registry for adapting reactive types. + */ public ReactiveAdapterRegistry getReactiveAdapterRegistry() { return this.reactiveAdapterRegistry; } @@ -250,51 +226,18 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application @Override public void afterPropertiesSet() throws Exception { - initControllerAdviceCache(); + if (this.argumentResolvers == null) { this.argumentResolvers = getDefaultArgumentResolvers(); } if (this.initBinderArgumentResolvers == null) { this.initBinderArgumentResolvers = getDefaultInitBinderArgumentResolvers(); } - this.modelInitializer = new ModelInitializer(getReactiveAdapterRegistry()); - } - - private void initControllerAdviceCache() { - if (getApplicationContext() == null) { - return; - } - if (logger.isInfoEnabled()) { - logger.info("Looking for @ControllerAdvice: " + getApplicationContext()); - } - List beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); - AnnotationAwareOrderComparator.sort(beans); + this.controllerMethodResolver = new ControllerMethodResolver( + getArgumentResolvers(), getInitBinderArgumentResolvers(), getApplicationContext()); - for (ControllerAdviceBean bean : beans) { - Class beanType = bean.getBeanType(); - Set attrMethods = selectMethods(beanType, ATTRIBUTE_METHODS); - if (!attrMethods.isEmpty()) { - this.attributeAdviceCache.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); - if (logger.isInfoEnabled()) { - logger.info("Detected @InitBinder methods in " + bean); - } - } - ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType); - if (resolver.hasExceptionMappings()) { - this.exceptionHandlerAdviceCache.put(bean, resolver); - if (logger.isInfoEnabled()) { - logger.info("Detected @ExceptionHandler methods in " + bean); - } - } - } + this.modelInitializer = new ModelInitializer(getReactiveAdapterRegistry()); } protected List getDefaultArgumentResolvers() { @@ -374,10 +317,10 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application HandlerMethod handlerMethod = (HandlerMethod) handler; BindingContext bindingContext = new InitBinderBindingContext( - getWebBindingInitializer(), getBinderMethods(handlerMethod)); + getWebBindingInitializer(), getInitBinderMethods(handlerMethod)); return this.modelInitializer - .initModel(bindingContext, getAttributeMethods(handlerMethod), exchange) + .initModel(bindingContext, getModelAttributeMethods(handlerMethod), exchange) .then(() -> { Function> exceptionHandler = ex -> handleException(exchange, handlerMethod, bindingContext, ex); @@ -391,68 +334,20 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application }); } - private List getBinderMethods(HandlerMethod handlerMethod) { - - List result = new ArrayList<>(); - Class handlerType = handlerMethod.getBeanType(); - - // Global methods first - this.binderAdviceCache.entrySet().forEach(entry -> { - if (entry.getKey().isApplicableToBeanType(handlerType)) { - Object bean = entry.getKey().resolveBean(); - entry.getValue().forEach(method -> result.add(createBinderMethod(bean, method))); - } - }); - - this.binderMethodCache - .computeIfAbsent(handlerType, aClass -> selectMethods(handlerType, BINDER_METHODS)) - .forEach(method -> { - Object bean = handlerMethod.getBean(); - result.add(createBinderMethod(bean, method)); - }); - - return result; + private List getInitBinderMethods(HandlerMethod handlerMethod) { + return this.controllerMethodResolver.resolveInitBinderMethods(handlerMethod); } - private SyncInvocableHandlerMethod createBinderMethod(Object bean, Method method) { - SyncInvocableHandlerMethod invocable = new SyncInvocableHandlerMethod(bean, method); - invocable.setArgumentResolvers(getInitBinderArgumentResolvers()); - return invocable; - } - - private List getAttributeMethods(HandlerMethod handlerMethod) { - - List result = new ArrayList<>(); - Class handlerType = handlerMethod.getBeanType(); - - // Global methods first - this.attributeAdviceCache.entrySet().forEach(entry -> { - if (entry.getKey().isApplicableToBeanType(handlerType)) { - Object bean = entry.getKey().resolveBean(); - entry.getValue().forEach(method -> result.add(createHandlerMethod(bean, method))); - } - }); - - this.attributeMethodCache - .computeIfAbsent(handlerType, aClass -> selectMethods(handlerType, ATTRIBUTE_METHODS)) - .forEach(method -> { - Object bean = handlerMethod.getBean(); - result.add(createHandlerMethod(bean, method)); - }); - - return result; - } - - private InvocableHandlerMethod createHandlerMethod(Object bean, Method method) { - InvocableHandlerMethod invocable = new InvocableHandlerMethod(bean, method); - invocable.setArgumentResolvers(getArgumentResolvers()); - return invocable; + private List getModelAttributeMethods(HandlerMethod handlerMethod) { + return this.controllerMethodResolver.resolveModelAttributeMethods(handlerMethod); } private Mono handleException(ServerWebExchange exchange, HandlerMethod handlerMethod, BindingContext bindingContext, Throwable ex) { - InvocableHandlerMethod invocable = getExceptionHandlerMethod(ex, handlerMethod); + InvocableHandlerMethod invocable = + this.controllerMethodResolver.resolveExceptionHandlerMethod(ex, handlerMethod); + if (invocable != null) { try { if (logger.isDebugEnabled()) { @@ -471,45 +366,4 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application return Mono.error(ex); } - private InvocableHandlerMethod 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)); - } - - - /** - * MethodFilter that matches {@link InitBinder @InitBinder} methods. - */ - public static final ReflectionUtils.MethodFilter BINDER_METHODS = method -> - AnnotationUtils.findAnnotation(method, InitBinder.class) != null; - - /** - * MethodFilter that matches {@link ModelAttribute @ModelAttribute} methods. - */ - public static final ReflectionUtils.MethodFilter ATTRIBUTE_METHODS = method -> - (AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) && - (AnnotationUtils.findAnnotation(method, ModelAttribute.class) != null); - } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelInitializerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelInitializerTests.java index 4749ce0f749..21b0027fbb6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelInitializerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ModelInitializerTests.java @@ -29,8 +29,10 @@ import rx.Single; import org.springframework.core.MethodIntrospector; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.ui.Model; +import org.springframework.util.ReflectionUtils; import org.springframework.validation.Validator; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; @@ -46,8 +48,6 @@ import org.springframework.web.server.ServerWebExchange; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; -import static org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.ATTRIBUTE_METHODS; -import static org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.BINDER_METHODS; /** * Unit tests for {@link ModelInitializer}. @@ -188,4 +188,10 @@ public class ModelInitializerTests { } } + private static final ReflectionUtils.MethodFilter BINDER_METHODS = method -> + AnnotationUtils.findAnnotation(method, InitBinder.class) != null; + + private static final ReflectionUtils.MethodFilter ATTRIBUTE_METHODS = method -> + (AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) && + (AnnotationUtils.findAnnotation(method, ModelAttribute.class) != null); }