Add dependency ordering for @ModelAttribute methods
Before this change @ModelAttribute methods were not invoked in any particular order other than ensuring global @ControllerAdvice methods are called first and local @Controller methods second. This change introduces a simple algorithm that selects the next @ModelAttribute method to invoke by making a pass over all methods and looking for one that has no dependencies (i.e. @ModelAttribute input arguments) or has all dependencies resolved (i.e. available in the model). The process is repeated until no more @ModelAttribute methods remain. If the next @ModelAttribute method cannot be determined because all remaining methods have unresolved dependencies, the first available method is picked anyway just as before, i.e. with required dependencies created through the default constructor. Examples in ModelFactoryOrderingTests. Issue: SPR-6299
This commit is contained in:
@@ -19,9 +19,13 @@ package org.springframework.web.method.annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
@@ -54,7 +58,9 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
*/
|
||||
public final class ModelFactory {
|
||||
|
||||
private final List<InvocableHandlerMethod> handlerMethods;
|
||||
private static final Log logger = LogFactory.getLog(ModelFactory.class);
|
||||
|
||||
private final List<ModelMethod> modelMethods = new ArrayList<ModelMethod>();
|
||||
|
||||
private final WebDataBinderFactory dataBinderFactory;
|
||||
|
||||
@@ -63,14 +69,18 @@ public final class ModelFactory {
|
||||
|
||||
/**
|
||||
* Create a new instance with the given {@code @ModelAttribute} methods.
|
||||
* @param handlerMethods the {@code @ModelAttribute} methods to invoke
|
||||
* @param invocableMethods the {@code @ModelAttribute} methods to invoke
|
||||
* @param dataBinderFactory for preparation of {@link BindingResult} attributes
|
||||
* @param sessionAttributesHandler for access to session attributes
|
||||
*/
|
||||
public ModelFactory(List<InvocableHandlerMethod> handlerMethods, WebDataBinderFactory dataBinderFactory,
|
||||
public ModelFactory(List<InvocableHandlerMethod> invocableMethods, WebDataBinderFactory dataBinderFactory,
|
||||
SessionAttributesHandler sessionAttributesHandler) {
|
||||
|
||||
this.handlerMethods = (handlerMethods != null) ? handlerMethods : new ArrayList<InvocableHandlerMethod>();
|
||||
if (invocableMethods != null) {
|
||||
for (InvocableHandlerMethod method : invocableMethods) {
|
||||
this.modelMethods.add(new ModelMethod(method));
|
||||
}
|
||||
}
|
||||
this.dataBinderFactory = dataBinderFactory;
|
||||
this.sessionAttributesHandler = sessionAttributesHandler;
|
||||
}
|
||||
@@ -115,7 +125,8 @@ public final class ModelFactory {
|
||||
private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer mavContainer)
|
||||
throws Exception {
|
||||
|
||||
for (InvocableHandlerMethod attrMethod : this.handlerMethods) {
|
||||
while (!this.modelMethods.isEmpty()) {
|
||||
InvocableHandlerMethod attrMethod = getNextModelMethod(mavContainer).getHandlerMethod();
|
||||
String modelName = attrMethod.getMethodAnnotation(ModelAttribute.class).value();
|
||||
if (mavContainer.containsAttribute(modelName)) {
|
||||
continue;
|
||||
@@ -132,6 +143,25 @@ public final class ModelFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private ModelMethod getNextModelMethod(ModelAndViewContainer mavContainer) {
|
||||
for (ModelMethod modelMethod : this.modelMethods) {
|
||||
if (modelMethod.checkDependencies(mavContainer)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Selected @ModelAttribute method " + modelMethod);
|
||||
}
|
||||
this.modelMethods.remove(modelMethod);
|
||||
return modelMethod;
|
||||
}
|
||||
}
|
||||
ModelMethod modelMethod = this.modelMethods.get(0);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Selected @ModelAttribute method (not present: " +
|
||||
modelMethod.getUnresolvedDependencies(mavContainer)+ ") " + modelMethod);
|
||||
}
|
||||
this.modelMethods.remove(modelMethod);
|
||||
return modelMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find {@code @ModelAttribute} arguments also listed as {@code @SessionAttributes}.
|
||||
*/
|
||||
@@ -240,4 +270,50 @@ public final class ModelFactory {
|
||||
!(value instanceof Map) && !BeanUtils.isSimpleValueType(value.getClass()));
|
||||
}
|
||||
|
||||
|
||||
private static class ModelMethod {
|
||||
|
||||
private final InvocableHandlerMethod handlerMethod;
|
||||
|
||||
private final Set<String> dependencies = new HashSet<String>();
|
||||
|
||||
|
||||
private ModelMethod(InvocableHandlerMethod handlerMethod) {
|
||||
this.handlerMethod = handlerMethod;
|
||||
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
|
||||
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
|
||||
this.dependencies.add(getNameForParameter(parameter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public InvocableHandlerMethod getHandlerMethod() {
|
||||
return this.handlerMethod;
|
||||
}
|
||||
|
||||
public boolean checkDependencies(ModelAndViewContainer mavContainer) {
|
||||
for (String name : this.dependencies) {
|
||||
if (!mavContainer.containsAttribute(name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<String> getUnresolvedDependencies(ModelAndViewContainer mavContainer) {
|
||||
List<String> result = new ArrayList<String>(this.dependencies.size());
|
||||
for (String name : this.dependencies) {
|
||||
if (!mavContainer.containsAttribute(name)) {
|
||||
result.add(name);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.handlerMethod.getMethod().toGenericString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user