Internal RequestMappingHandlerAdapter refactoring

Extract controller method caches including associated code and
discovery of @ControllerAdvice components into a separate, package
private helper class (ControllerMethodResolver).
This commit is contained in:
Rossen Stoyanchev 2017-03-27 18:29:03 -04:00
parent 525f30ad5e
commit e06871ef17
4 changed files with 273 additions and 174 deletions

View File

@ -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<HandlerMethodArgumentResolver> argumentResolvers;
private final List<SyncHandlerMethodArgumentResolver> initBinderArgumentResolvers;
private final Map<Class<?>, Set<Method>> binderMethodCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, Set<Method>> attributeMethodCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<>(64);
private final Map<ControllerAdviceBean, Set<Method>> binderAdviceCache = new LinkedHashMap<>(64);
private final Map<ControllerAdviceBean, Set<Method>> attributeAdviceCache = new LinkedHashMap<>(64);
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
new LinkedHashMap<>(64);
ControllerMethodResolver(List<HandlerMethodArgumentResolver> argumentResolvers,
List<SyncHandlerMethodArgumentResolver> 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<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(applicationContext);
AnnotationAwareOrderComparator.sort(beans);
for (ControllerAdviceBean bean : beans) {
Class<?> beanType = bean.getBeanType();
Set<Method> attrMethods = selectMethods(beanType, ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.attributeAdviceCache.put(bean, attrMethods);
if (logger.isInfoEnabled()) {
logger.info("Detected @ModelAttribute methods in " + bean);
}
}
Set<Method> 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<SyncInvocableHandlerMethod> resolveInitBinderMethods(HandlerMethod handlerMethod) {
List<SyncInvocableHandlerMethod> 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<InvocableHandlerMethod> resolveModelAttributeMethods(HandlerMethod handlerMethod) {
List<InvocableHandlerMethod> 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);
}

View File

@ -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

View File

@ -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<Class<?>, Set<Method>> binderMethodCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, Set<Method>> attributeMethodCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<>(64);
private final Map<ControllerAdviceBean, Set<Method>> binderAdviceCache = new LinkedHashMap<>(64);
private final Map<ControllerAdviceBean, Set<Method>> attributeAdviceCache = new LinkedHashMap<>(64);
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> 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.
* <p>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,53 +226,20 @@ 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.controllerMethodResolver = new ControllerMethodResolver(
getArgumentResolvers(), getInitBinderArgumentResolvers(), getApplicationContext());
this.modelInitializer = new ModelInitializer(getReactiveAdapterRegistry());
}
private void initControllerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
if (logger.isInfoEnabled()) {
logger.info("Looking for @ControllerAdvice: " + getApplicationContext());
}
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
AnnotationAwareOrderComparator.sort(beans);
for (ControllerAdviceBean bean : beans) {
Class<?> beanType = bean.getBeanType();
Set<Method> attrMethods = selectMethods(beanType, ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.attributeAdviceCache.put(bean, attrMethods);
if (logger.isInfoEnabled()) {
logger.info("Detected @ModelAttribute methods in " + bean);
}
}
Set<Method> 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);
}
}
}
}
protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
@ -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<Throwable, Mono<HandlerResult>> exceptionHandler =
ex -> handleException(exchange, handlerMethod, bindingContext, ex);
@ -391,68 +334,20 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, Application
});
}
private List<SyncInvocableHandlerMethod> getBinderMethods(HandlerMethod handlerMethod) {
List<SyncInvocableHandlerMethod> 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<SyncInvocableHandlerMethod> 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<InvocableHandlerMethod> getAttributeMethods(HandlerMethod handlerMethod) {
List<InvocableHandlerMethod> 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<InvocableHandlerMethod> getModelAttributeMethods(HandlerMethod handlerMethod) {
return this.controllerMethodResolver.resolveModelAttributeMethods(handlerMethod);
}
private Mono<HandlerResult> 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);
}

View File

@ -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);
}