Fix regressions from 8f558e7

The change to provide public register/unregister methods in
AbstractHandlerMethodMapping assumed that a single method cannot be
mapped more than once. This is not the case with the MvcEndpoints and
EndpointHandlerMapping from Spring Boot which wrap one or more
non-web Endpoint types with an MvcEndpointAdapter in order to expose
them for use over the web. In effect Spring MVC sees a single handler
method mapped many times.

This change removes that assumption so rather than unregistering with
a HandlerMethod, which is not necessarily unique, the unregister method
now takes the actual mapping, which is the only thing that should actually
be unique.

Issue: SPR-11541
This commit is contained in:
Rossen Stoyanchev 2015-05-04 17:24:49 -04:00
parent a274ede3ef
commit 71683c5f8d
2 changed files with 53 additions and 36 deletions

View File

@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -227,26 +228,21 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
protected abstract T getMappingForMethod(Method method, Class<?> handlerType); protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
/** /**
* Register a handler method and its unique mapping. * Register a handler method and its unique mapping. Invoked at startup for
* <p>Invoked at startup for each detected handler method. May also be * each detected handler method.
* invoked at runtime after initialization is complete.
* @param handler the bean name of the handler or the handler instance * @param handler the bean name of the handler or the handler instance
* @param method the method to register * @param method the method to register
* @param mapping the mapping conditions associated with the handler method * @param mapping the mapping conditions associated with the handler method
* @throws IllegalStateException if another method was already registered * @throws IllegalStateException if another method was already registered
* under the same mapping * under the same mapping
* @deprecated as of 4.2 you can invoke the public methods
* {@link #registerMapping(Object, Object, Method)} and
* {@link #unregisterMapping(Object)} during initialization or at runtime,
* i.e. after initialization is complete.
*/ */
public void registerHandlerMethod(Object handler, Method method, T mapping) { @Deprecated
this.mappingRegistry.register(handler, method, mapping); protected void registerHandlerMethod(Object handler, Method method, T mapping) {
} this.mappingRegistry.register(mapping, handler, method);
/**
* Un-register a handler method.
* <p>This method may be invoked at runtime after initialization has completed.
* @param handlerMethod the handler method to be unregistered
*/
public void unregisterHandlerMethod(HandlerMethod handlerMethod) {
this.mappingRegistry.unregister(handlerMethod);
} }
/** /**
@ -287,6 +283,25 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
protected void handlerMethodsInitialized(Map<T, HandlerMethod> handlerMethods) { protected void handlerMethodsInitialized(Map<T, HandlerMethod> handlerMethods) {
} }
/**
* Register the given mapping.
* <p>This method may be invoked at runtime after initialization has completed.
* @param mapping the mapping for the handler method
* @param handler the handler
* @param method the method
*/
public void registerMapping(T mapping, Object handler, Method method) {
this.mappingRegistry.register(mapping, handler, method);
}
/**
* Un-register the given mapping.
* <p>This method may be invoked at runtime after initialization has completed.
* @param mapping the mapping to unregister
*/
public void unregisterMapping(T mapping) {
this.mappingRegistry.unregister(mapping);
}
/** /**
* Look up a handler method for the given request. * Look up a handler method for the given request.
@ -437,8 +452,10 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
private final Map<String, List<HandlerMethod>> mappingNameLookup = private final Map<String, List<HandlerMethod>> mappingNameLookup =
new ConcurrentHashMap<String, List<HandlerMethod>>(); new ConcurrentHashMap<String, List<HandlerMethod>>();
private final Map<Method, MappingDefinition<T>> methodLookup = private final Map<T, MappingDefinition<T>> definitionMap = new HashMap<T, MappingDefinition<T>>();
new ConcurrentHashMap<Method, MappingDefinition<T>>();
private final Map<Method, CorsConfiguration> corsLookup =
new ConcurrentHashMap<Method, CorsConfiguration>();
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
@ -480,8 +497,7 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
*/ */
public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) { public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) {
Method method = handlerMethod.getMethod(); Method method = handlerMethod.getMethod();
MappingDefinition<T> definition = this.methodLookup.get(method); return this.corsLookup.get(method);
return (definition != null ? definition.getCorsConfiguration() : null);
} }
/** /**
@ -499,7 +515,7 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
} }
public void register(Object handler, Method method, T mapping) { public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock(); this.readWriteLock.writeLock().lock();
try { try {
@ -523,8 +539,11 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
} }
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(method, corsConfig);
}
this.methodLookup.put(method, this.definitionMap.put(mapping,
new MappingDefinition<T>(mapping, handlerMethod, directUrls, name, corsConfig)); new MappingDefinition<T>(mapping, handlerMethod, directUrls, name, corsConfig));
} }
finally { finally {
@ -540,11 +559,6 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
newHandlerMethod + "\nto " + mapping + ": There is already '" + newHandlerMethod + "\nto " + mapping + ": There is already '" +
handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped."); handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped.");
} }
MappingDefinition<T> definition = this.methodLookup.get(newHandlerMethod.getMethod());
if (definition != null) {
throw new IllegalStateException("Cannot map " + newHandlerMethod.getMethod() +
"\nto " + mapping + ".\n It is already mapped to " + definition.getMapping());
}
} }
private List<String> getDirectUrls(T mapping) { private List<String> getDirectUrls(T mapping) {
@ -585,10 +599,10 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
} }
} }
public void unregister(HandlerMethod handlerMethod) { public void unregister(T mapping) {
this.readWriteLock.writeLock().lock(); this.readWriteLock.writeLock().lock();
try { try {
MappingDefinition<T> definition = this.methodLookup.remove(handlerMethod.getMethod()); MappingDefinition<T> definition = this.definitionMap.remove(mapping);
if (definition == null) { if (definition == null) {
return; return;
} }
@ -606,6 +620,8 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
} }
removeMappingName(definition); removeMappingName(definition);
this.corsLookup.remove(definition.getHandlerMethod().getMethod());
} }
finally { finally {
this.readWriteLock.writeLock().unlock(); this.readWriteLock.writeLock().unlock();

View File

@ -62,14 +62,14 @@ public class HandlerMethodMappingTests {
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void registerDuplicates() { public void registerDuplicates() {
mapping.registerHandlerMethod(handler, method1, "foo"); mapping.registerMapping("foo", handler, method1);
mapping.registerHandlerMethod(handler, method2, "foo"); mapping.registerMapping("foo", handler, method2);
} }
@Test @Test
public void directMatch() throws Exception { public void directMatch() throws Exception {
String key = "foo"; String key = "foo";
mapping.registerHandlerMethod(handler, method1, key); mapping.registerMapping(key, handler, method1);
HandlerMethod result = mapping.getHandlerInternal(new MockHttpServletRequest("GET", key)); HandlerMethod result = mapping.getHandlerInternal(new MockHttpServletRequest("GET", key));
assertEquals(method1, result.getMethod()); assertEquals(method1, result.getMethod());
@ -77,8 +77,8 @@ public class HandlerMethodMappingTests {
@Test @Test
public void patternMatch() throws Exception { public void patternMatch() throws Exception {
mapping.registerHandlerMethod(handler, method1, "/fo*"); mapping.registerMapping("/fo*", handler, method1);
mapping.registerHandlerMethod(handler, method2, "/f*"); mapping.registerMapping("/f*", handler, method2);
HandlerMethod result = mapping.getHandlerInternal(new MockHttpServletRequest("GET", "/foo")); HandlerMethod result = mapping.getHandlerInternal(new MockHttpServletRequest("GET", "/foo"));
assertEquals(method1, result.getMethod()); assertEquals(method1, result.getMethod());
@ -86,8 +86,8 @@ public class HandlerMethodMappingTests {
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void ambiguousMatch() throws Exception { public void ambiguousMatch() throws Exception {
mapping.registerHandlerMethod(handler, method1, "/f?o"); mapping.registerMapping("/f?o", handler, method1);
mapping.registerHandlerMethod(handler, method2, "/fo?"); mapping.registerMapping("/fo?", handler, method2);
mapping.getHandlerInternal(new MockHttpServletRequest("GET", "/foo")); mapping.getHandlerInternal(new MockHttpServletRequest("GET", "/foo"));
} }
@ -114,12 +114,12 @@ public class HandlerMethodMappingTests {
@Test @Test
public void unregister() throws Exception { public void unregister() throws Exception {
String key = "foo"; String key = "foo";
mapping.registerHandlerMethod(handler, method1, key); mapping.registerMapping(key, handler, method1);
HandlerMethod handlerMethod = mapping.getHandlerInternal(new MockHttpServletRequest("GET", key)); HandlerMethod handlerMethod = mapping.getHandlerInternal(new MockHttpServletRequest("GET", key));
assertEquals(method1, handlerMethod.getMethod()); assertEquals(method1, handlerMethod.getMethod());
mapping.unregisterHandlerMethod(handlerMethod); mapping.unregisterMapping(key);
assertNull(mapping.getHandlerInternal(new MockHttpServletRequest("GET", key))); assertNull(mapping.getHandlerInternal(new MockHttpServletRequest("GET", key)));
} }
@ -159,6 +159,7 @@ public class HandlerMethodMappingTests {
} }
} }
@SuppressWarnings("unused")
@Controller @Controller
static class MyHandler { static class MyHandler {