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:
Rossen Stoyanchev
2014-06-07 16:07:56 -04:00
parent e374769ecc
commit 56a82c1cbe
2 changed files with 417 additions and 5 deletions

View File

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