/*
* 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.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.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.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, initializing, and caching annotated methods declared in
* {@code @Controller} and {@code @ControllerAdvice} components:
* <ul>
* <li>{@code @InitBinder}
* <li>{@code @ModelAttribute}
* <li>{@code @RequestMapping}
* <li>{@code @ExceptionHandler}
* </ul>
*
* @author Rossen Stoyanchev
* @since 5.0
*/
class ControllerMethodResolver {
private static Log logger = LogFactory.getLog(ControllerMethodResolver.class);
private final List<SyncHandlerMethodArgumentResolver> initBinderResolvers;
private final List<HandlerMethodArgumentResolver> modelAttributeResolvers;
private final List<HandlerMethodArgumentResolver> requestMappingResolvers;
private final List<HandlerMethodArgumentResolver> exceptionHandlerResolvers;
private final Map<Class<?>, Set<Method>> initBinderMethodCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, Set<Method>> modelAttributeMethodCache = new ConcurrentHashMap<>(64);
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64);
private final Map<ControllerAdviceBean, Set<Method>> initBinderAdviceCache = new LinkedHashMap<>(64);
private final Map<ControllerAdviceBean, Set<Method>> modelAttributeAdviceCache = new LinkedHashMap<>(64);
private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
new LinkedHashMap<>(64);
ControllerMethodResolver(ArgumentResolverConfigurer argumentResolvers,
ServerCodecConfigurer messageCodecs, ReactiveAdapterRegistry reactiveRegistry,
ConfigurableApplicationContext context) {
Assert.notNull(argumentResolvers, "ArgumentResolverConfigurer is required");
Assert.notNull(messageCodecs, "ServerCodecConfigurer is required");
Assert.notNull(reactiveRegistry, "ReactiveAdapterRegistry is required");
Assert.notNull(context, "ApplicationContext is required");
ArgumentResolverRegistrar registrar;
registrar= ArgumentResolverRegistrar.configurer(argumentResolvers).basic();
addResolversTo(registrar, reactiveRegistry, context);
this.initBinderResolvers = registrar.getSyncResolvers();
registrar = ArgumentResolverRegistrar.configurer(argumentResolvers).modelAttributeSupport();
addResolversTo(registrar, reactiveRegistry, context);
this.modelAttributeResolvers = registrar.getResolvers();
registrar = ArgumentResolverRegistrar.configurer(argumentResolvers).fullSupport(messageCodecs);
addResolversTo(registrar, reactiveRegistry, context);
this.requestMappingResolvers = registrar.getResolvers();
registrar = ArgumentResolverRegistrar.configurer(argumentResolvers).basic();
addResolversTo(registrar, reactiveRegistry, context);
this.exceptionHandlerResolvers = registrar.getResolvers();
initControllerAdviceCaches(context);
}
private void addResolversTo(ArgumentResolverRegistrar 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 RequestPartMethodArgumentResolver(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;
}
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.modelAttributeAdviceCache.put(bean, attrMethods);
if (logger.isInfoEnabled()) {
logger.info("Detected @ModelAttribute methods in " + bean);
}
}
Set<Method> binderMethods = selectMethods(beanType, BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.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);
}
}
}
}
/**
* Return an {@link InvocableHandlerMethod} for the given
* {@code @RequestMapping} method initialized with argument resolvers.
*/
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<SyncInvocableHandlerMethod> getInitBinderMethods(HandlerMethod handlerMethod) {
List<SyncInvocableHandlerMethod> result = new ArrayList<>();
Class<?> handlerType = handlerMethod.getBeanType();
// Global methods first
this.initBinderAdviceCache.entrySet().forEach(entry -> {
if (entry.getKey().isApplicableToBeanType(handlerType)) {
Object bean = entry.getKey().resolveBean();
entry.getValue().forEach(method -> result.add(getInitBinderMethod(bean, method)));
}
});
this.initBinderMethodCache
.computeIfAbsent(handlerType, aClass -> selectMethods(handlerType, BINDER_METHODS))
.forEach(method -> {
Object bean = handlerMethod.getBean();
result.add(getInitBinderMethod(bean, method));
});
return result;
}
private SyncInvocableHandlerMethod getInitBinderMethod(Object bean, Method method) {
SyncInvocableHandlerMethod invocable = new SyncInvocableHandlerMethod(bean, method);
invocable.setArgumentResolvers(this.initBinderResolvers);
return invocable;
}
/**
* Find {@code @ModelAttribute} methods in {@code @ControllerAdvice}
* components or in the controller of the given {@code @RequestMapping} method.
*/
public List<InvocableHandlerMethod> getModelAttributeMethods(HandlerMethod handlerMethod) {
List<InvocableHandlerMethod> result = new ArrayList<>();
Class<?> handlerType = handlerMethod.getBeanType();
// Global methods first
this.modelAttributeAdviceCache.entrySet().forEach(entry -> {
if (entry.getKey().isApplicableToBeanType(handlerType)) {
Object bean = entry.getKey().resolveBean();
entry.getValue().forEach(method -> result.add(createAttributeMethod(bean, method)));
}
});
this.modelAttributeMethodCache
.computeIfAbsent(handlerType, aClass -> selectMethods(handlerType, ATTRIBUTE_METHODS))
.forEach(method -> {
Object bean = handlerMethod.getBean();
result.add(createAttributeMethod(bean, method));
});
return result;
}
private InvocableHandlerMethod createAttributeMethod(Object bean, Method method) {
InvocableHandlerMethod invocable = new InvocableHandlerMethod(bean, method);
invocable.setArgumentResolvers(this.modelAttributeResolvers);
return invocable;
}
/**
* Find an {@code @ExceptionHandler} method in {@code @ControllerAdvice}
* components or in the controller of the given {@code @RequestMapping} method.
*/
public Optional<InvocableHandlerMethod> getExceptionHandlerMethod(Throwable ex,
HandlerMethod handlerMethod) {
Class<?> handlerType = handlerMethod.getBeanType();
// 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);
}
/** 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);
private static class ArgumentResolverRegistrar {
private final List<HandlerMethodArgumentResolver> customResolvers;
private final List<HttpMessageReader<?>> messageReaders;
private final boolean modelAttributeSupported;
private final List<HandlerMethodArgumentResolver> result = new ArrayList<>();
private ArgumentResolverRegistrar(ArgumentResolverConfigurer resolvers,
ServerCodecConfigurer codecs, boolean modelAttribute) {
this.customResolvers = resolvers.getCustomResolvers();
this.messageReaders = codecs != null ? codecs.getReaders() : null;
this.modelAttributeSupported = modelAttribute;
}
public void add(HandlerMethodArgumentResolver resolver) {
this.result.add(resolver);
}
public void addIfRequestBody(Function<List<HttpMessageReader<?>>, HandlerMethodArgumentResolver> function) {
if (this.messageReaders != null) {
add(function.apply(this.messageReaders));
}
}
public void addIfModelAttribute(Supplier<HandlerMethodArgumentResolver> supplier) {
if (this.modelAttributeSupported) {
add(supplier.get());
}
}
public void addCustomResolvers() {
this.customResolvers.forEach(this::add);
}
public List<HandlerMethodArgumentResolver> getResolvers() {
return this.result;
}
public List<SyncHandlerMethodArgumentResolver> getSyncResolvers() {
return this.result.stream()
.filter(resolver -> resolver instanceof SyncHandlerMethodArgumentResolver)
.map(resolver -> (SyncHandlerMethodArgumentResolver) resolver)
.collect(Collectors.toList());
}
public static Builder configurer(ArgumentResolverConfigurer configurer) {
return new Builder(configurer);
}
public static class Builder {
private final ArgumentResolverConfigurer resolvers;
public Builder(ArgumentResolverConfigurer configurer) {
this.resolvers = configurer;
}
public ArgumentResolverRegistrar fullSupport(ServerCodecConfigurer codecs) {
return new ArgumentResolverRegistrar(this.resolvers, codecs, true);
}
public ArgumentResolverRegistrar modelAttributeSupport() {
return new ArgumentResolverRegistrar(this.resolvers, null, true);
}
public ArgumentResolverRegistrar basic() {
return new ArgumentResolverRegistrar(this.resolvers, null, false);
}
}
}
}