Переглянути джерело

MvcRequestMatcher servletPath Polish / XML Config

Fixes gh-4014
Joe Grandja 9 роки тому
батько
коміт
dabcc5416a

+ 6 - 59
config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java

@@ -15,15 +15,6 @@
  */
 package org.springframework.security.config.annotation.web;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.List;
-
-import javax.servlet.ServletContext;
-import javax.servlet.ServletRegistration;
-
-import org.springframework.beans.factory.SmartInitializingSingleton;
 import org.springframework.context.ApplicationContext;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
@@ -34,9 +25,12 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher;
 import org.springframework.security.web.util.matcher.RegexRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.ClassUtils;
-import org.springframework.web.context.ServletContextAware;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 /**
  * A base class for registering {@link RequestMatcher}'s. For example, it might allow for
  * specifying which {@link RequestMatcher} require a certain level of authorization.
@@ -171,12 +165,9 @@ public abstract class AbstractRequestMatcherRegistry<C> {
 		List<MvcRequestMatcher> matchers = new ArrayList<MvcRequestMatcher>(
 				mvcPatterns.length);
 		for (String mvcPattern : mvcPatterns) {
-			MvcRequestMatcher matcher;
-			if(isServlet30) {
-				matcher = new ServletPathValidatingtMvcRequestMatcher(introspector, mvcPattern);
+			MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern);
+			if (isServlet30) {
 				opp.postProcess(matcher);
-			} else {
-				matcher = new MvcRequestMatcher(introspector, mvcPattern);
 			}
 			if (method != null) {
 				matcher.setMethod(method);
@@ -316,48 +307,4 @@ public abstract class AbstractRequestMatcherRegistry<C> {
 		}
 	}
 
-	static class ServletPathValidatingtMvcRequestMatcher extends MvcRequestMatcher implements SmartInitializingSingleton, ServletContextAware {
-		private ServletContext servletContext;
-
-		/**
-		 * @param introspector
-		 * @param pattern
-		 */
-		public ServletPathValidatingtMvcRequestMatcher(HandlerMappingIntrospector introspector,
-				String pattern) {
-			super(introspector, pattern);
-		}
-
-		/* (non-Javadoc)
-		 * @see org.springframework.beans.factory.SmartInitializingSingleton#afterSingletonsInstantiated()
-		 */
-		@Override
-		public void afterSingletonsInstantiated() {
-			if(getServletPath() != null) {
-				return;
-			}
-			Collection<? extends ServletRegistration> registrations = servletContext.getServletRegistrations().values();
-			for(ServletRegistration registration : registrations) {
-				Collection<String> mappings = registration.getMappings();
-				for(String mapping : mappings) {
-					if(mapping.startsWith("/") && mapping.length() > 1) {
-						throw new IllegalStateException(
-								"servletPath must not be null for mvcPattern \"" + getMvcPattern()
-										+ "\" when providing a servlet mapping of "
-										+ mapping + " for servlet "
-										+ registration.getClassName());
-					}
-				}
-			}
-		}
-
-		/* (non-Javadoc)
-		 * @see org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet.ServletContext)
-		 */
-		@Override
-		public void setServletContext(ServletContext servletContext) {
-			this.servletContext = servletContext;
-		}
-
-	}
 }

+ 1 - 1
config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd

@@ -1351,7 +1351,7 @@
                 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are
                 2 or more HttpServlet's registered in the ServletContext that have mappings starting with
                 '/' and are different; 2) The pattern starts with the same value of a registered
-                HttpServlet path, excluding the default (root) HttpServlet '/'
+                HttpServlet path, excluding the default (root) HttpServlet '/'.
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>

+ 33 - 11
config/src/test/groovy/org/springframework/security/config/AbstractXmlConfigTests.groovy

@@ -1,26 +1,25 @@
 package org.springframework.security.config
 
 import groovy.xml.MarkupBuilder
-
-import org.mockito.Mockito;
-import org.springframework.context.support.AbstractXmlApplicationContext
+import org.mockito.Mockito
+import org.springframework.context.ApplicationListener
+import org.springframework.context.support.AbstractRefreshableApplicationContext
+import org.springframework.mock.web.MockServletContext
+import org.springframework.security.CollectingAppListener
 import org.springframework.security.config.util.InMemoryXmlApplicationContext
+import org.springframework.security.config.util.InMemoryXmlWebApplicationContext
 import org.springframework.security.core.context.SecurityContextHolder
 import spock.lang.Specification
-import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML
-import org.springframework.context.ApplicationListener
-import org.springframework.context.ApplicationEvent
-import org.springframework.security.authentication.event.AbstractAuthenticationEvent
-import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent
-import org.springframework.security.access.event.AbstractAuthorizationEvent
-import org.springframework.security.CollectingAppListener
 
+import javax.servlet.ServletContext
+
+import static org.springframework.security.config.ConfigTestUtils.AUTH_PROVIDER_XML
 /**
  *
  * @author Luke Taylor
  */
 abstract class AbstractXmlConfigTests extends Specification {
-	AbstractXmlApplicationContext appContext;
+	AbstractRefreshableApplicationContext appContext;
 	Writer writer;
 	MarkupBuilder xml;
 	ApplicationListener appListener;
@@ -81,4 +80,27 @@ abstract class AbstractXmlConfigTests extends Specification {
 		appContext = new InMemoryXmlApplicationContext(writer.toString() + extraXml);
 		appContext.addApplicationListener(appListener);
 	}
+
+	def createWebAppContext() {
+		createWebAppContext(AUTH_PROVIDER_XML);
+	}
+
+	def createWebAppContext(ServletContext servletContext) {
+		createWebAppContext(AUTH_PROVIDER_XML, servletContext);
+	}
+
+	def createWebAppContext(String extraXml) {
+		createWebAppContext(extraXml, null);
+	}
+
+	def createWebAppContext(String extraXml, ServletContext servletContext) {
+		appContext = new InMemoryXmlWebApplicationContext(writer.toString() + extraXml);
+		appContext.addApplicationListener(appListener);
+		if (servletContext != null) {
+			appContext.setServletContext(servletContext);
+		} else {
+			appContext.setServletContext(new MockServletContext());
+		}
+		appContext.refresh();
+	}
 }

+ 1 - 2
config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedTests.groovy

@@ -16,7 +16,6 @@
 package org.springframework.security.config.doc
 
 import groovy.util.slurpersupport.GPathResult;
-import groovy.util.slurpersupport.NodeChild
 
 import org.springframework.security.config.http.SecurityFilters
 
@@ -89,7 +88,7 @@ class XsdDocumentedTests extends Specification {
 	def 'the latest schema is being validated'() {
 		when: 'all the schemas are found'
 		def schemas = schemaDocument.getParentFile().list().findAll { it.endsWith('.xsd') }
-		then: 'the count is equal to 8, if not then schemaDocument needs updated'
+		then: 'the count is equal to 10, if not then schemaDocument needs updated'
 		schemas.size() == 10
 	}
 

+ 56 - 3
config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy

@@ -15,17 +15,25 @@
  */
 package org.springframework.security.config.http
 
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+import org.springframework.beans.factory.BeanCreationException
 import org.springframework.beans.factory.parsing.BeanDefinitionParsingException
 import org.springframework.mock.web.MockFilterChain
 import org.springframework.mock.web.MockHttpServletRequest
 import org.springframework.mock.web.MockHttpServletResponse
+import org.springframework.mock.web.MockServletContext
 import org.springframework.security.access.SecurityConfig
 import org.springframework.security.crypto.codec.Base64
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
 import org.springframework.web.bind.annotation.RequestMapping
 import org.springframework.web.bind.annotation.RestController
 
+import javax.servlet.ServletContext
+import javax.servlet.ServletRegistration
 import javax.servlet.http.HttpServletResponse
+
+import static org.mockito.Mockito.*
 /**
  *
  * @author Rob Winch
@@ -199,6 +207,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 
 	def "intercept-url supports mvc matchers"() {
 		setup:
+		MockServletContext servletContext = mockServletContext();
 		MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
 		MockHttpServletResponse response = new MockHttpServletResponse()
 		MockFilterChain chain = new MockFilterChain()
@@ -209,7 +218,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 		bean('pathController',PathController)
 		xml.'mvc:annotation-driven'()
 
-		createAppContext()
+		createWebAppContext(servletContext)
 		when:
 		request.servletPath = "/path"
 		springSecurityFilterChain.doFilter(request, response, chain)
@@ -235,6 +244,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 
 	def "intercept-url mvc supports path variables"() {
 		setup:
+		MockServletContext servletContext = mockServletContext();
 		MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
 		MockHttpServletResponse response = new MockHttpServletResponse()
 		MockFilterChain chain = new MockFilterChain()
@@ -242,7 +252,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 			'http-basic'()
 			'intercept-url'(pattern: '/user/{un}/**', access: "#un == 'user'")
 		}
-		createAppContext()
+		createWebAppContext(servletContext)
 		when: 'user can access'
 		request.servletPath = '/user/user/abc'
 		springSecurityFilterChain.doFilter(request,response,chain)
@@ -266,6 +276,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 
 	def "intercept-url mvc matchers with servlet path"() {
 		setup:
+		MockServletContext servletContext = mockServletContext("/spring");
 		MockHttpServletRequest request = new MockHttpServletRequest(method:'GET')
 		MockHttpServletResponse response = new MockHttpServletResponse()
 		MockFilterChain chain = new MockFilterChain()
@@ -275,7 +286,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 		}
 		bean('pathController',PathController)
 		xml.'mvc:annotation-driven'()
-		createAppContext()
+		createWebAppContext(servletContext)
 		when:
 		request.servletPath = "/spring"
 		request.requestURI = "/spring/path"
@@ -302,6 +313,30 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 		response.status == HttpServletResponse.SC_UNAUTHORIZED
 	}
 
+	def "intercept-url mvc matchers servlet path required"() {
+		when:
+		MockServletContext servletContext = mockServletContext("/spring");
+		xml.http('request-matcher':'mvc') {
+			'http-basic'()
+			'intercept-url'(pattern: '/path', access: "denyAll")
+		}
+		createWebAppContext(servletContext)
+		then:
+		thrown(BeanCreationException)
+	}
+
+	def "intercept-url mvc matchers servlet path NOT required"() {
+		when:
+		MockServletContext servletContext = mockServletContext();
+		xml.http('request-matcher':'mvc') {
+			'http-basic'()
+			'intercept-url'(pattern: '/path', access: "denyAll")
+		}
+		createWebAppContext(servletContext)
+		then:
+		noExceptionThrown()
+	}
+
 	def "intercept-url ant matcher with servlet path fails"() {
 		when:
 		xml.http('request-matcher':'ant') {
@@ -352,6 +387,24 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
 		}
 	}
 
+	private ServletContext mockServletContext() {
+		return mockServletContext("/");
+	}
+
+	private ServletContext mockServletContext(String servletPath) {
+		MockServletContext servletContext = spy(new MockServletContext());
+		final ServletRegistration registration = mock(ServletRegistration.class);
+		when(registration.getMappings()).thenReturn(Collections.singleton(servletPath));
+		Answer<Map<String, ? extends ServletRegistration>> answer = new Answer<Map<String, ? extends ServletRegistration>>() {
+			@Override
+			public Map<String, ? extends ServletRegistration> answer(InvocationOnMock invocation) throws Throwable {
+				return Collections.<String, ServletRegistration>singletonMap("spring", registration);
+			}
+		};
+		when(servletContext.getServletRegistrations()).thenAnswer(answer);
+		return servletContext;
+	}
+
 	def login(MockHttpServletRequest request, String username, String password) {
 		String toEncode = username + ':' + password
 		request.addHeader('Authorization','Basic ' + new String(Base64.encode(toEncode.getBytes('UTF-8'))))

+ 16 - 21
config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java → config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java

@@ -16,14 +16,6 @@
 
 package org.springframework.security.config.annotation.web;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Map;
-
-import javax.servlet.Registration;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletRegistration;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -31,14 +23,17 @@ import org.mockito.Mock;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.runners.MockitoJUnitRunner;
 import org.mockito.stubbing.Answer;
-
 import org.springframework.mock.web.MockServletContext;
-import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.ServletPathValidatingtMvcRequestMatcher;
+import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRegistration;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.mockito.Mockito.*;
 
 /**
  * @author Rob Winch
@@ -49,34 +44,34 @@ public class AbstractRequestMatcherRegistryTests {
 	@Mock
 	HandlerMappingIntrospector introspector;
 
-	ServletPathValidatingtMvcRequestMatcher matcher;
+	MvcRequestMatcher matcher;
 
 	ServletContext servletContext;
 
 	@Before
 	public void setup() {
 		servletContext = spy(new MockServletContext());
-		matcher = new ServletPathValidatingtMvcRequestMatcher(introspector, "/foo");
+		matcher = new MvcRequestMatcher(introspector, "/foo");
 		matcher.setServletContext(servletContext);
 	}
 
 	@Test(expected = IllegalStateException.class)
-	public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedFailsWithSpringServlet() {
+	public void servletPathValidatingMvcRequestMatcherAfterPropertiesSetFailsWithSpringServlet() throws Exception {
 		setMappings("/spring");
-		matcher.afterSingletonsInstantiated();
+		matcher.afterPropertiesSet();
 	}
 
 	@Test
-	public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedWithSpringServlet() {
+	public void servletPathValidatingMvcRequestMatcherAfterPropertiesSetWithSpringServlet() throws Exception {
 		matcher.setServletPath("/spring");
 		setMappings("/spring");
-		matcher.afterSingletonsInstantiated();
+		matcher.afterPropertiesSet();
 	}
 
 	@Test
-	public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedDefaultServlet() {
+	public void servletPathValidatingMvcRequestMatcherAfterPropertiesSetDefaultServlet() throws Exception {
 		setMappings("/");
-		matcher.afterSingletonsInstantiated();
+		matcher.afterPropertiesSet();
 	}
 
 	private void setMappings(String... mappings) {

+ 3 - 2
config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java

@@ -27,6 +27,7 @@ import org.junit.Test;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import org.springframework.beans.factory.BeanCreationException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -449,7 +450,7 @@ public class AuthorizeRequestsTests {
 		}
 	}
 
-	@Test(expected = IllegalStateException.class)
+	@Test(expected = BeanCreationException.class)
 	public void mvcMatcherServletPathRequired() throws Exception {
 		final ServletRegistration registration = mock(ServletRegistration.class);
 		when(registration.getMappings()).thenReturn(Collections.singleton("/spring"));
@@ -502,4 +503,4 @@ public class AuthorizeRequestsTests {
 
 		this.context.getAutowireCapableBeanFactory().autowireBean(this);
 	}
-}
+}

+ 6 - 4
config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java

@@ -25,7 +25,7 @@ import org.springframework.security.util.InMemoryResource;
  * @author Luke Taylor
  */
 public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext {
-	private static final String BEANS_OPENING = "<b:beans xmlns='http://www.springframework.org/schema/security'\n"
+	static final String BEANS_OPENING = "<b:beans xmlns='http://www.springframework.org/schema/security'\n"
 			+ "    xmlns:context='http://www.springframework.org/schema/context'\n"
 			+ "    xmlns:b='http://www.springframework.org/schema/beans'\n"
 			+ "    xmlns:aop='http://www.springframework.org/schema/aop'\n"
@@ -38,16 +38,18 @@ public class InMemoryXmlApplicationContext extends AbstractXmlApplicationContext
 			+ "http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd\n"
 			+ "http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd\n"
 			+ "http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-";
-	private static final String BEANS_CLOSE = "</b:beans>\n";
+	static final String BEANS_CLOSE = "</b:beans>\n";
+
+	static final String SPRING_SECURITY_VERSION = "4.1";
 
 	Resource inMemoryXml;
 
 	public InMemoryXmlApplicationContext(String xml) {
-		this(xml, "4.1", null);
+		this(xml, SPRING_SECURITY_VERSION, null);
 	}
 
 	public InMemoryXmlApplicationContext(String xml, ApplicationContext parent) {
-		this(xml, "4.1", parent);
+		this(xml, SPRING_SECURITY_VERSION, parent);
 	}
 
 	public InMemoryXmlApplicationContext(String xml, String secVersion,

+ 60 - 0
config/src/test/java/org/springframework/security/config/util/InMemoryXmlWebApplicationContext.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012-2016 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.security.config.util;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.security.util.InMemoryResource;
+import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext;
+
+import java.io.IOException;
+
+import static org.springframework.security.config.util.InMemoryXmlApplicationContext.BEANS_CLOSE;
+import static org.springframework.security.config.util.InMemoryXmlApplicationContext.BEANS_OPENING;
+import static org.springframework.security.config.util.InMemoryXmlApplicationContext.SPRING_SECURITY_VERSION;
+
+/**
+ * @author Joe Grandja
+ */
+public class InMemoryXmlWebApplicationContext extends AbstractRefreshableWebApplicationContext {
+	private Resource inMemoryXml;
+
+	public InMemoryXmlWebApplicationContext(String xml) {
+		this(xml, SPRING_SECURITY_VERSION, null);
+	}
+
+	public InMemoryXmlWebApplicationContext(String xml, ApplicationContext parent) {
+		this(xml, SPRING_SECURITY_VERSION, parent);
+	}
+
+	public InMemoryXmlWebApplicationContext(String xml, String secVersion,
+											ApplicationContext parent) {
+		String fullXml = BEANS_OPENING + secVersion + ".xsd'>\n" + xml + BEANS_CLOSE;
+		inMemoryXml = new InMemoryResource(fullXml);
+		setAllowBeanDefinitionOverriding(true);
+		setParent(parent);
+	}
+
+	@Override
+	protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
+		XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
+		reader.loadBeanDefinitions(new Resource[] { inMemoryXml });
+	}
+
+}

+ 47 - 1
web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java

@@ -16,17 +16,24 @@
 
 package org.springframework.security.web.servlet.util.matcher;
 
+import org.springframework.beans.factory.InitializingBean;
 import org.springframework.http.HttpMethod;
+import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.security.web.util.matcher.RequestVariablesExtractor;
 import org.springframework.util.AntPathMatcher;
+import org.springframework.util.ClassUtils;
 import org.springframework.util.PathMatcher;
+import org.springframework.web.context.ServletContextAware;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 import org.springframework.web.servlet.handler.MatchableHandlerMapping;
 import org.springframework.web.servlet.handler.RequestMatchResult;
 import org.springframework.web.util.UrlPathHelper;
 
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRegistration;
 import javax.servlet.http.HttpServletRequest;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 
@@ -45,13 +52,18 @@ import java.util.Map;
  * @since 4.1.1
  */
 public class MvcRequestMatcher
-		implements RequestMatcher, RequestVariablesExtractor {
+		implements RequestMatcher, RequestVariablesExtractor, InitializingBean, ServletContextAware {
+
+	private static final boolean isServlet30 = ClassUtils.isPresent(
+		"javax.servlet.ServletRegistration", MvcRequestMatcher.class.getClassLoader());
+
 	private final DefaultMatcher defaultMatcher = new DefaultMatcher();
 
 	private final HandlerMappingIntrospector introspector;
 	private final String pattern;
 	private HttpMethod method;
 	private String servletPath;
+	private ServletContext servletContext;
 
 	public MvcRequestMatcher(HandlerMappingIntrospector introspector, String pattern) {
 		this.introspector = introspector;
@@ -100,6 +112,40 @@ public class MvcRequestMatcher
 				: result.extractUriTemplateVariables();
 	}
 
+	/* (non-Javadoc)
+	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
+	 */
+	@Override
+	public void afterPropertiesSet() throws Exception {
+		// servletPath is required when at least one registered Servlet
+		// is mapped to a path other than the default (root) path '/'
+		if (this.servletContext == null || !isServlet30) {
+			return;
+		}
+		if (this.getServletPath() != null) {
+			return;
+		}
+		for (ServletRegistration registration : this.servletContext.getServletRegistrations().values()) {
+			for (String mapping : registration.getMappings()) {
+				if (mapping.startsWith("/") && mapping.length() > 1) {
+					throw new IllegalStateException(
+						"servletPath must not be null for mvcPattern \"" + this.getMvcPattern()
+							+ "\" when providing a servlet mapping of "
+							+ mapping + " for servlet "
+							+ registration.getClassName());
+				}
+			}
+		}
+	}
+
+	/* (non-Javadoc)
+	 * @see org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet.ServletContext)
+	 */
+	@Override
+	public void setServletContext(ServletContext servletContext) {
+		this.servletContext = servletContext;
+	}
+
 	/**
 	 * @param method the method to set
 	 */