提交 a30ab30e 编写于 作者: R Rossen Stoyanchev

Introduce HandlerMapping introspection API

Issue: SPR-14321
上级 a25c43f6
......@@ -27,21 +27,21 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.core.Ordered;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.cors.CorsProcessor;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.context.request.WebRequestInterceptor;
import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.CorsProcessor;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.DefaultCorsProcessor;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.cors.DefaultCorsProcessor;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.util.UrlPathHelper;
/**
......@@ -471,7 +471,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
}
private class PreFlightHandler implements HttpRequestHandler {
private class PreFlightHandler implements HttpRequestHandler, CorsConfigurationSource {
private final CorsConfiguration config;
......@@ -485,10 +485,15 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
corsProcessor.processRequest(this.config, request, response);
}
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
return this.config;
}
}
private class CorsInterceptor extends HandlerInterceptorAdapter {
private class CorsInterceptor extends HandlerInterceptorAdapter implements CorsConfigurationSource {
private final CorsConfiguration config;
......@@ -502,6 +507,11 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
return corsProcessor.processRequest(this.config, request, response);
}
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
return this.config;
}
}
}
......@@ -30,6 +30,8 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.support.MatchableHandlerMapping;
import org.springframework.web.servlet.support.RequestMatchResult;
/**
* Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping}
......@@ -50,7 +52,8 @@ import org.springframework.web.servlet.HandlerMapping;
* @author Arjen Poutsma
* @since 16.04.2003
*/
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping
implements MatchableHandlerMapping {
private Object rootHandler;
......@@ -279,6 +282,20 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
}
@Override
public RequestMatchResult match(HttpServletRequest request, String pattern) {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
if (getPathMatcher().match(pattern, lookupPath)) {
return new RequestMatchResult(pattern, lookupPath, getPathMatcher());
}
else if (useTrailingSlashMatch()) {
if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", lookupPath)) {
return new RequestMatchResult(pattern + "/", lookupPath, getPathMatcher());
}
}
return null;
}
/**
* Register the specified handler for the given URL paths.
* @param urlPaths the URLs that the bean should be mapped to
......
......@@ -20,6 +20,8 @@ import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
......@@ -38,6 +40,8 @@ import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.servlet.support.MatchableHandlerMapping;
import org.springframework.web.servlet.support.RequestMatchResult;
/**
* Creates {@link RequestMappingInfo} instances from type and method-level
......@@ -50,7 +54,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
* @since 3.1
*/
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
implements EmbeddedValueResolverAware {
implements EmbeddedValueResolverAware, MatchableHandlerMapping {
private boolean useSuffixPatternMatch = true;
......@@ -274,6 +278,18 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
}
@Override
public RequestMatchResult match(HttpServletRequest request, String pattern) {
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
if (matchingInfo == null) {
return null;
}
Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns();
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher());
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
......
/*
* Copyright 2002-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.web.servlet.support;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
/**
* Helper class to get information from the {@code HandlerMapping} that would
* serve a specific request.
*
* <p>Provides the following methods:
* <ul>
* <li>{@link #getMatchableHandlerMapping} -- obtain a {@code HandlerMapping}
* to check request-matching criteria against.
* <li>{@link #getCorsConfiguration} -- obtain the CORS configuration for the
* request.
* </ul>
*
* @author Rossen Stoyanchev
* @since 4.3
*/
public class HandlerMappingIntrospector implements CorsConfigurationSource {
private final List<HandlerMapping> handlerMappings;
/**
* Constructor that detects the configured {@code HandlerMapping}s in the
* given {@code ApplicationContext} or falling back on
* "DispatcherServlet.properties" like the {@code DispatcherServlet}.
*/
public HandlerMappingIntrospector(ApplicationContext context) {
this.handlerMappings = initHandlerMappings(context);
}
private static List<HandlerMapping> initHandlerMappings(ApplicationContext context) {
Map<String, HandlerMapping> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
context, HandlerMapping.class, true, false);
if (!beans.isEmpty()) {
List<HandlerMapping> mappings = new ArrayList<HandlerMapping>(beans.values());
AnnotationAwareOrderComparator.sort(mappings);
return mappings;
}
return initDefaultHandlerMappings(context);
}
private static List<HandlerMapping> initDefaultHandlerMappings(ApplicationContext context) {
Properties props;
String path = "DispatcherServlet.properties";
try {
Resource resource = new ClassPathResource(path, DispatcherServlet.class);
props = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage());
}
String value = props.getProperty(HandlerMapping.class.getName());
String[] names = StringUtils.commaDelimitedListToStringArray(value);
List<HandlerMapping> result = new ArrayList<HandlerMapping>(names.length);
for (String name : names) {
try {
Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader());
Object mapping = context.getAutowireCapableBeanFactory().createBean(clazz);
result.add((HandlerMapping) mapping);
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]");
}
}
return result;
}
/**
* Return the configured HandlerMapping's.
*/
public List<HandlerMapping> getHandlerMappings() {
return this.handlerMappings;
}
/**
* Find the {@link HandlerMapping} that would handle the given request and
* return it as a {@link MatchableHandlerMapping} that can be used to
* test request-matching criteria. If the matching HandlerMapping is not an
* instance of {@link MatchableHandlerMapping}, an IllegalStateException is
* raised.
*
* @param request the current request
* @return the resolved matcher, or {@code null}
* @throws Exception if any of the HandlerMapping's raise an exception
*/
public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request);
for (HandlerMapping handlerMapping : this.handlerMappings) {
Object handler = handlerMapping.getHandler(wrapper);
if (handler == null) {
continue;
}
if (handlerMapping instanceof MatchableHandlerMapping) {
return ((MatchableHandlerMapping) handlerMapping);
}
throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping");
}
return null;
}
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request);
for (HandlerMapping handlerMapping : this.handlerMappings) {
HandlerExecutionChain handler = null;
try {
handler = handlerMapping.getHandler(wrapper);
}
catch (Exception ex) {
// Ignore
}
if (handler == null) {
continue;
}
if (handler.getInterceptors() != null) {
for (HandlerInterceptor interceptor : handler.getInterceptors()) {
if (interceptor instanceof CorsConfigurationSource) {
return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper);
}
}
}
if (handler.getHandler() instanceof CorsConfigurationSource) {
return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper);
}
}
return null;
}
/**
* Request wrapper that ignores request attribute changes.
*/
private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper {
private RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) {
super(request);
}
@Override
public void setAttribute(String name, Object value) {
// Ignore attribute change
}
}
}
\ No newline at end of file
/*
* Copyright 2002-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.web.servlet.support;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.servlet.HandlerMapping;
/**
* Additional interface that a {@link HandlerMapping} can implement to expose
* a request matching API aligned with its internal request matching
* configuration and implementation.
*
* @author Rossen Stoyanchev
* @since 4.3
* @see HandlerMappingIntrospector
*/
public interface MatchableHandlerMapping {
/**
* Whether the given request matches the request criteria.
* @param request the current request
* @param pattern the pattern to match
* @return the result from request matching or {@code null}
*/
RequestMatchResult match(HttpServletRequest request, String pattern);
}
/*
* Copyright 2002-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.web.servlet.support;
import java.util.Collections;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
/**
* Container for the result from request pattern matching via
* {@link MatchableHandlerMapping} with a method to further extract URI template
* variables from the pattern.
*
* @author Rossen Stoyanchev
* @since 4.3
*/
public class RequestMatchResult {
private final String matchingPattern;
private final String lookupPath;
private final PathMatcher pathMatcher;
/**
* Create an instance with a matching pattern.
* @param matchingPattern the matching pattern, possibly not the same as the
* input pattern, e.g. inputPattern="/foo" and matchingPattern="/foo/".
* @param lookupPath the lookup path extracted from the request
* @param pathMatcher the PathMatcher used
*/
public RequestMatchResult(String matchingPattern, String lookupPath, PathMatcher pathMatcher) {
Assert.hasText(matchingPattern, "'matchingPattern' is required");
Assert.hasText(lookupPath, "'lookupPath' is required");
Assert.notNull(pathMatcher, "'pathMatcher' is required");
this.matchingPattern = matchingPattern;
this.lookupPath = lookupPath;
this.pathMatcher = pathMatcher;
}
/**
* Whether the pattern was matched to the request.
*/
public boolean isMatch() {
return (this.matchingPattern != null);
}
/**
* Extract URI template variables from the matching pattern as defined in
* {@link PathMatcher#extractUriTemplateVariables}.
* @return a map with URI template variables
*/
public Map<String, String> extractUriTemplateVariables() {
if (!isMatch()) {
return Collections.<String, String>emptyMap();
}
return this.pathMatcher.extractUriTemplateVariables(this.matchingPattern, this.lookupPath);
}
}
/*
* Copyright 2002-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.web.servlet.support;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE;
/**
* Unit tests for {@link HandlerMappingIntrospector}.
* @author Rossen Stoyanchev
*/
public class HandlerMappingIntrospectorTests {
@Test
public void detectHandlerMappings() throws Exception {
StaticWebApplicationContext cxt = new StaticWebApplicationContext();
cxt.registerSingleton("hmA", SimpleUrlHandlerMapping.class);
cxt.registerSingleton("hmB", SimpleUrlHandlerMapping.class);
cxt.registerSingleton("hmC", SimpleUrlHandlerMapping.class);
cxt.refresh();
List<?> expected = Arrays.asList(cxt.getBean("hmA"), cxt.getBean("hmB"), cxt.getBean("hmC"));
List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings();
assertEquals(expected, actual);
}
@Test
public void detectHandlerMappingsOrdered() throws Exception {
StaticWebApplicationContext cxt = new StaticWebApplicationContext();
MutablePropertyValues pvs = new MutablePropertyValues(Collections.singletonMap("order", "3"));
cxt.registerSingleton("hmA", SimpleUrlHandlerMapping.class, pvs);
pvs = new MutablePropertyValues(Collections.singletonMap("order", "2"));
cxt.registerSingleton("hmB", SimpleUrlHandlerMapping.class, pvs);
pvs = new MutablePropertyValues(Collections.singletonMap("order", "1"));
cxt.registerSingleton("hmC", SimpleUrlHandlerMapping.class, pvs);
cxt.refresh();
List<?> expected = Arrays.asList(cxt.getBean("hmC"), cxt.getBean("hmB"), cxt.getBean("hmA"));
List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings();
assertEquals(expected, actual);
}
@Test @SuppressWarnings("deprecation")
public void defaultHandlerMappings() throws Exception {
StaticWebApplicationContext cxt = new StaticWebApplicationContext();
cxt.refresh();
List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings();
assertEquals(2, actual.size());
assertEquals(BeanNameUrlHandlerMapping.class, actual.get(0).getClass());
assertEquals(DefaultAnnotationHandlerMapping.class, actual.get(1).getClass());
}
@Test
public void getMatchable() throws Exception {
MutablePropertyValues pvs = new MutablePropertyValues(
Collections.singletonMap("urlMap",
Collections.singletonMap("/path", new Object())));
StaticWebApplicationContext cxt = new StaticWebApplicationContext();
cxt.registerSingleton("hm", SimpleUrlHandlerMapping.class, pvs);
cxt.refresh();
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
MatchableHandlerMapping hm = new HandlerMappingIntrospector(cxt).getMatchableHandlerMapping(request);
assertEquals(cxt.getBean("hm"), hm);
assertNull("Attributes changes not ignored", request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE));
}
@Test(expected = IllegalStateException.class)
public void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() throws Exception {
StaticWebApplicationContext cxt = new StaticWebApplicationContext();
cxt.registerSingleton("hm1", TestHandlerMapping.class);
cxt.refresh();
MockHttpServletRequest request = new MockHttpServletRequest();
new HandlerMappingIntrospector(cxt).getMatchableHandlerMapping(request);
}
@Test
public void getCorsConfigurationPreFlight() throws Exception {
AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext();
cxt.register(TestConfig.class);
cxt.refresh();
// PRE-FLIGHT
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path");
request.addHeader("Origin", "http://localhost:9000");
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");
CorsConfiguration corsConfig = new HandlerMappingIntrospector(cxt).getCorsConfiguration(request);
assertNotNull(corsConfig);
assertEquals(Collections.singletonList("http://localhost:9000"), corsConfig.getAllowedOrigins());
assertEquals(Collections.singletonList("POST"), corsConfig.getAllowedMethods());
}
@Test
public void getCorsConfigurationActual() throws Exception {
AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext();
cxt.register(TestConfig.class);
cxt.refresh();
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path");
request.addHeader("Origin", "http://localhost:9000");
CorsConfiguration corsConfig = new HandlerMappingIntrospector(cxt).getCorsConfiguration(request);
assertNotNull(corsConfig);
assertEquals(Collections.singletonList("http://localhost:9000"), corsConfig.getAllowedOrigins());
assertEquals(Collections.singletonList("POST"), corsConfig.getAllowedMethods());
}
private static class TestHandlerMapping implements HandlerMapping {
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
return new HandlerExecutionChain(new Object());
}
}
@Configuration @SuppressWarnings({"WeakerAccess", "unused"})
static class TestConfig {
@Bean
public RequestMappingHandlerMapping handlerMapping() {
return new RequestMappingHandlerMapping();
}
@Bean
public TestController testController() {
return new TestController();
}
}
@CrossOrigin("http://localhost:9000")
@Controller
private static class TestController {
@PostMapping("/path")
public void handle() {
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册