源代码版本 : spring-webmvc-5.1.4.RELEASE
RequestMappingHandlerMapping
是HandlerMapping
的一个实现,主要用于针对控制器类(带有注解@Controller
)中类级别或者方法级别的注解@RequestMapping
创建RequestMappingInfo
并管理。
RequestMappingHandlerMapping
的实现相对较为复杂,它的类层次结构如下:
RequestMappingHandlerMapping
工作原理大致是这样的 :
RequestMappingHandlerMapping
实现了接口InitializingBean
,它的初始化方法会设置各种工作参数,并检测容器中所有的控制器方法并将它们登记管理起来。针对每个使用注解@RequestMapping
的控制器方法所生成的请求匹配条件是一个RequestMappingInfo
对象,最终所被管理的是一个HandlerMethod
对象。RequestMappingHandlerMapping
会被DispatcherServlet
用于匹配一个HandlerMethod
并最终调用它处理该请求。匹配的依据是当前请求中的路径信息和RequestMappingHandlerMapping
所管理的每个控制器方法的RequestMappingInfo
请求匹配条件。package org.springframework.web.servlet.mvc.method.annotation;
// 省略 imports
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
implements MatchableHandlerMapping, EmbeddedValueResolverAware {
private boolean useSuffixPatternMatch = true;
private boolean useRegisteredSuffixPatternMatch = false;
private boolean useTrailingSlashMatch = true;
private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>();
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
@Nullable
private StringValueResolver embeddedValueResolver;
private RequestMappingInfo.BuilderConfiguration config =
new RequestMappingInfo.BuilderConfiguration();
/**
* Whether to use suffix pattern match (".*") when matching patterns to
* requests. If enabled a method mapped to "/users" also matches to "/users.*".
* The default value is true.
* Also see #setUseRegisteredSuffixPatternMatch(boolean) for
* more fine-grained control over specific suffixes to allow.
*/
public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
this.useSuffixPatternMatch = useSuffixPatternMatch;
}
/**
* Whether suffix pattern matching should work only against path extensions
* explicitly registered with the ContentNegotiationManager. This
* is generally recommended to reduce ambiguity and to avoid issues such as
* when a "." appears in the path for other reasons.
* By default this is set to "false".
*/
public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
}
/**
* Whether to match to URLs irrespective of the presence of a trailing slash.
* If enabled a method mapped to "/users" also matches to "/users/".
* The default value is true.
*/
public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
this.useTrailingSlashMatch = useTrailingSlashMatch;
}
/**
* Configure path prefixes to apply to controller methods.
* 设置应用到每个控制器方法上的路径前缀
* Prefixes are used to enrich the mappings of every @RequestMapping
* method whose controller type is matched by the corresponding
* Predicate. The prefix for the first matching predicate is used.
* Consider using org.springframework.web.method.HandlerTypePredicate
* HandlerTypePredicate to group controllers.
* @param prefixes a map with path prefixes as key
* @since 5.1
*/
public void setPathPrefixes(Map<String, Predicate<Class<?>>> prefixes) {
this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes));
}
/**
* The configured path prefixes as a read-only, possibly empty map.
* @since 5.1
*/
public Map<String, Predicate<Class<?>>> getPathPrefixes() {
return this.pathPrefixes;
}
/**
* Set the ContentNegotiationManager to use to determine requested media types.
* 设置用于决定请求媒体类型MIME的 ContentNegotiationManager
* If not set, the default constructor is used.
*/
public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null");
this.contentNegotiationManager = contentNegotiationManager;
}
/**
* Return the configured ContentNegotiationManager.
*/
public ContentNegotiationManager getContentNegotiationManager() {
return this.contentNegotiationManager;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
// RequestMappingHandlerMapping 通过基类潜在实现了接口 InitializingBean,
// 该方法是接口 InitializingBean 定义的初始化化方法,会在当前bean创建过程中
// 初始化过程中被调用.
// 该方法的主要作用是 :
// 1. 创建配置对象config,并设置运行时所需要的各种参数到该配置对象
// 2. 检测所有的控制器方法 (由基类的初始化方法提供)
@Override
public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
this.config.setContentNegotiationManager(getContentNegotiationManager());
super.afterPropertiesSet();
}
/**
* Whether to use suffix pattern matching.
*/
public boolean useSuffixPatternMatch() {
return this.useSuffixPatternMatch;
}
/**
* Whether to use registered suffixes for pattern matching.
*/
public boolean useRegisteredSuffixPatternMatch() {
return this.useRegisteredSuffixPatternMatch;
}
/**
* Whether to match to URLs irrespective of the presence of a trailing slash.
*/
public boolean useTrailingSlashMatch() {
return this.useTrailingSlashMatch;
}
/**
* Return the file extensions to use for suffix pattern matching.
*/
@Nullable
public List<String> getFileExtensions() {
return this.config.getFileExtensions();
}
/**
* Expects a handler to have either a type-level Controller
* annotation or a type-level RequestMapping annotation.
* 判断一个bean类型beanType是否是一个控制器类,判断条件 :
* beanType带有注解@Controller或者@RequestMapping
*/
@Override
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
/**
* Uses method and type-level RequestMapping annotations to create
* the RequestMappingInfo.
* 使用方法或者类层面的注解@RequestMapping信息创建相应的一个RequestMappingInfo对象,
* 如果指定的方法上没有注解@RequestMapping,则不创建RequestMappingInfo,而是返回null
* @return the created RequestMappingInfo, or null if the method
* does not have a @RequestMapping annotation.
* @see #getCustomMethodCondition(Method)
* @see #getCustomTypeCondition(Class)
*/
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
}
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).build().combine(info);
}
}
return info;
}
@Nullable
String getPathPrefix(Class<?> handlerType) {
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
if (entry.getValue().test(handlerType)) {
String prefix = entry.getKey();
if (this.embeddedValueResolver != null) {
prefix = this.embeddedValueResolver.resolveStringValue(prefix);
}
return prefix;
}
}
return null;
}
/**
* Delegates to #createRequestMappingInfo(RequestMapping, RequestCondition),
* supplying the appropriate custom RequestCondition depending on whether
* the supplied annotatedElement is a class or method.
* 获取被注解元素element上的注解@RequestMapping信息,基于此信息创建相应的RequestMappingInfo对象
* @see #getCustomTypeCondition(Class)
* @see #getCustomMethodCondition(Method)
*/
@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element,
RequestMapping.class);
RequestCondition<?> condition = (element instanceof Class ?
getCustomTypeCondition((Class<?>) element) :
getCustomMethodCondition((Method) element));
return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}
/**
* Provide a custom type-level request condition.
* The custom RequestCondition can be of any type so long as the
* same condition type is returned from all calls to this method in order
* to ensure custom request conditions can be combined and compared.
* Consider extending AbstractRequestCondition for custom
* condition types and using CompositeRequestCondition to provide
* multiple custom conditions.
* @param handlerType the handler type for which to create the condition
* @return the condition, or null
*/
@Nullable
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
return null;
}
/**
* Provide a custom method-level request condition.
* The custom RequestCondition can be of any type so long as the
* same condition type is returned from all calls to this method in order
* to ensure custom request conditions can be combined and compared.
* Consider extending AbstractRequestCondition for custom
* condition types and using CompositeRequestCondition to provide
* multiple custom conditions.
* @param method the handler method for which to create the condition
* @return the condition, or null
*/
@Nullable
protected RequestCondition<?> getCustomMethodCondition(Method method) {
return null;
}
/**
* Create a RequestMappingInfo from the supplied
* RequestMapping @RequestMapping annotation, which is either
* a directly declared annotation, a meta-annotation, or the synthesized
* result of merging annotation attributes within an annotation hierarchy.
* 从所提供的requestMapping注解信息和自定义请求匹配条件customCondition创建RequestMappingInfo对象
*/
protected RequestMappingInfo createRequestMappingInfo(
RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
RequestMappingInfo.Builder builder = RequestMappingInfo
.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name());
if (customCondition != null) {
builder.customCondition(customCondition);
}
return builder.options(this.config).build();
}
/**
* Resolve placeholder values in the given array of patterns.
* @return a new array with updated patterns
*/
protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
if (this.embeddedValueResolver == null) {
return patterns;
}
else {
String[] resolvedPatterns = new String[patterns.length];
for (int i = 0; i < patterns.length; i++) {
resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]);
}
return resolvedPatterns;
}
}
// 判断指定请求request和路径匹配模式pattern是否匹配,匹配的话构造一个RequestMatchResult并返回。
// 不匹配的话返回null
@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) {
// 没有匹配的控制器方法,则返回null
return null;
}
Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns();
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
// 根据匹配到的请求匹配条件对象中的第一个(最优的那个),和当前请求的查找路径lookupPath
// 构造一个 RequestMatchResult 对象并返回
return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher());
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mappingInfo) {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
Class<?> beanType = handlerMethod.getBeanType();
CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType,
CrossOrigin.class);
CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method,
CrossOrigin.class);
if (typeAnnotation == null && methodAnnotation == null) {
return null;
}
CorsConfiguration config = new CorsConfiguration();
updateCorsConfig(config, typeAnnotation);
updateCorsConfig(config, methodAnnotation);
if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
config.addAllowedMethod(allowedMethod.name());
}
}
return config.applyPermitDefaultValues();
}
private void updateCorsConfig(CorsConfiguration config, @Nullable CrossOrigin annotation) {
if (annotation == null) {
return;
}
for (String origin : annotation.origins()) {
config.addAllowedOrigin(resolveCorsAnnotationValue(origin));
}
for (RequestMethod method : annotation.methods()) {
config.addAllowedMethod(method.name());
}
for (String header : annotation.allowedHeaders()) {
config.addAllowedHeader(resolveCorsAnnotationValue(header));
}
for (String header : annotation.exposedHeaders()) {
config.addExposedHeader(resolveCorsAnnotationValue(header));
}
String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials());
if ("true".equalsIgnoreCase(allowCredentials)) {
config.setAllowCredentials(true);
}
else if ("false".equalsIgnoreCase(allowCredentials)) {
config.setAllowCredentials(false);
}
else if (!allowCredentials.isEmpty()) {
throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\",
\"false\", " +
"or an empty string (\"\"): current value is [" + allowCredentials + "]");
}
if (annotation.maxAge() >= 0 && config.getMaxAge() == null) {
config.setMaxAge(annotation.maxAge());
}
}
private String resolveCorsAnnotationValue(String value) {
if (this.embeddedValueResolver != null) {
String resolved = this.embeddedValueResolver.resolveStringValue(value);
return (resolved != null ? resolved : "");
}
else {
return value;
}
}
}
RequestMappingInfoHandlerMapping
RequestMappingInfoHandlerMapping
是RequestMappingHandlerMapping
直接父类,是一个抽象基类。
package org.springframework.web.servlet.mvc.method;
// 省略 imports
/**
* Abstract base class for classes for which RequestMappingInfo defines
* the mapping between a request and a handler method.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.1
*/
public abstract class RequestMappingInfoHandlerMapping extends
AbstractHandlerMethodMapping<RequestMappingInfo> {
private static final Method HTTP_OPTIONS_HANDLE_METHOD;
static {
try {
HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle");
}
catch (NoSuchMethodException ex) {
// Should never happen
throw new IllegalStateException("Failed to retrieve internal handler method for HTTP OPTIONS",
ex);
}
}
protected RequestMappingInfoHandlerMapping() {
setHandlerMethodMappingNamingStrategy(
new RequestMappingInfoHandlerMethodMappingNamingStrategy()
);
}
/**
* Get the URL path patterns associated with this RequestMappingInfo.
*/
@Override
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
return info.getPatternsCondition().getPatterns();
}
/**
* Check if the given RequestMappingInfo matches the current request and
* return a (potentially new) instance with conditions that match the
* current request -- for example with a subset of URL patterns.
* @return an info in case of a match; or null otherwise.
*/
@Override
protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info,
HttpServletRequest request) {
return info.getMatchingCondition(request);
}
/**
* Provide a Comparator to sort RequestMappingInfos matched to a request.
*/
@Override
protected Comparator<RequestMappingInfo> getMappingComparator(
final HttpServletRequest request) {
return (info1, info2) -> info1.compareTo(info2, request);
}
/**
* Expose URI template variables, matrix variables, and producible media types in the request.
* @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
* @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE
* @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
*/
@Override
protected void handleMatch(RequestMappingInfo info, String lookupPath,
HttpServletRequest request) {
super.handleMatch(info, lookupPath, request);
String bestPattern;
Map<String, String> uriVariables;
Set<String> patterns = info.getPatternsCondition().getPatterns();
if (patterns.isEmpty()) {
bestPattern = lookupPath;
uriVariables = Collections.emptyMap();
}
else {
bestPattern = patterns.iterator().next();
uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
}
request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
if (isMatrixVariableContentAvailable()) {
Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request,
uriVariables);
request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
}
Map<String, String> decodedUriVariables = getUrlPathHelper().decodePathVariables(request,
uriVariables);
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
}
}
private boolean isMatrixVariableContentAvailable() {
return !getUrlPathHelper().shouldRemoveSemicolonContent();
}
private Map<String, MultiValueMap<String, String>> extractMatrixVariables(
HttpServletRequest request, Map<String, String> uriVariables) {
Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<>();
uriVariables.forEach((uriVarKey, uriVarValue) -> {
int equalsIndex = uriVarValue.indexOf('=');
if (equalsIndex == -1) {
return;
}
int semicolonIndex = uriVarValue.indexOf(';');
if (semicolonIndex != -1 && semicolonIndex != 0) {
uriVariables.put(uriVarKey, uriVarValue.substring(0, semicolonIndex));
}
String matrixVariables;
if (semicolonIndex == -1 || semicolonIndex == 0 || equalsIndex < semicolonIndex) {
matrixVariables = uriVarValue;
}
else {
matrixVariables = uriVarValue.substring(semicolonIndex + 1);
}
MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables);
result.put(uriVarKey, getUrlPathHelper().decodeMatrixVariables(request, vars));
});
return result;
}
/**
* Iterate all RequestMappingInfo's once again, look if any match by URL at
* least and raise exceptions according to what doesn't match.
* @throws HttpRequestMethodNotSupportedException if there are matches by URL
* but not by HTTP method
* @throws HttpMediaTypeNotAcceptableException if there are matches by URL
* but not by consumable/producible media types
*/
@Override
protected HandlerMethod handleNoMatch(
Set<RequestMappingInfo> infos, String lookupPath, HttpServletRequest request)
throws ServletException {
PartialMatchHelper helper = new PartialMatchHelper(infos, request);
if (helper.isEmpty()) {
return null;
}
if (helper.hasMethodsMismatch()) {
Set<String> methods = helper.getAllowedMethods();
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
HttpOptionsHandler handler = new HttpOptionsHandler(methods);
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
}
throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
}
if (helper.hasConsumesMismatch()) {
Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
MediaType contentType = null;
if (StringUtils.hasLength(request.getContentType())) {
try {
contentType = MediaType.parseMediaType(request.getContentType());
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
}
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<>(mediaTypes));
}
if (helper.hasProducesMismatch()) {
Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
throw new HttpMediaTypeNotAcceptableException(new ArrayList<>(mediaTypes));
}
if (helper.hasParamsMismatch()) {
List<String[]> conditions = helper.getParamConditions();
throw new UnsatisfiedServletRequestParameterException(conditions,
request.getParameterMap());
}
return null;
}
/**
* Aggregate all partial matches and expose methods checking across them.
*/
private static class PartialMatchHelper {
private final List<PartialMatch> partialMatches = new ArrayList<>();
public PartialMatchHelper(Set<RequestMappingInfo> infos, HttpServletRequest request) {
for (RequestMappingInfo info : infos) {
if (info.getPatternsCondition().getMatchingCondition(request) != null) {
this.partialMatches.add(new PartialMatch(info, request));
}
}
}
/**
* Whether there any partial matches.
*/
public boolean isEmpty() {
return this.partialMatches.isEmpty();
}
/**
* Any partial matches for "methods"?
*/
public boolean hasMethodsMismatch() {
for (PartialMatch match : this.partialMatches) {
if (match.hasMethodsMatch()) {
return false;
}
}
return true;
}
/**
* Any partial matches for "methods" and "consumes"?
*/
public boolean hasConsumesMismatch() {
for (PartialMatch match : this.partialMatches) {
if (match.hasConsumesMatch()) {
return false;
}
}
return true;
}
/**
* Any partial matches for "methods", "consumes", and "produces"?
*/
public boolean hasProducesMismatch() {
for (PartialMatch match : this.partialMatches) {
if (match.hasProducesMatch()) {
return false;
}
}
return true;
}
/**
* Any partial matches for "methods", "consumes", "produces", and "params"?
*/
public boolean hasParamsMismatch() {
for (PartialMatch match : this.partialMatches) {
if (match.hasParamsMatch()) {
return false;
}
}
return true;
}
/**
* Return declared HTTP methods.
*/
public Set<String> getAllowedMethods() {
Set<String> result = new LinkedHashSet<>();
for (PartialMatch match : this.partialMatches) {
for (RequestMethod method : match.getInfo().getMethodsCondition().getMethods()) {
result.add(method.name());
}
}
return result;
}
/**
* Return declared "consumable" types but only among those that also
* match the "methods" condition.
*/
public Set<MediaType> getConsumableMediaTypes() {
Set<MediaType> result = new LinkedHashSet<>();
for (PartialMatch match : this.partialMatches) {
if (match.hasMethodsMatch()) {
result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes());
}
}
return result;
}
/**
* Return declared "producible" types but only among those that also
* match the "methods" and "consumes" conditions.
*/
public Set<MediaType> getProducibleMediaTypes() {
Set<MediaType> result = new LinkedHashSet<>();
for (PartialMatch match : this.partialMatches) {
if (match.hasConsumesMatch()) {
result.addAll(match.getInfo().getProducesCondition().getProducibleMediaTypes());
}
}
return result;
}
/**
* Return declared "params" conditions but only among those that also
* match the "methods", "consumes", and "params" conditions.
*/
public List<String[]> getParamConditions() {
List<String[]> result = new ArrayList<>();
for (PartialMatch match : this.partialMatches) {
if (match.hasProducesMatch()) {
Set<NameValueExpression<String>> set =
match.getInfo().getParamsCondition().getExpressions();
if (!CollectionUtils.isEmpty(set)) {
int i = 0;
String[] array = new String[set.size()];
for (NameValueExpression<String> expression : set) {
array[i++] = expression.toString();
}
result.add(array);
}
}
}
return result;
}
/**
* Container for a RequestMappingInfo that matches the URL path at least.
*/
private static class PartialMatch {
private final RequestMappingInfo info;
private final boolean methodsMatch;
private final boolean consumesMatch;
private final boolean producesMatch;
private final boolean paramsMatch;
/**
* Create a new {@link PartialMatch} instance.
* @param info the RequestMappingInfo that matches the URL path.
* @param request the current request
*/
public PartialMatch(RequestMappingInfo info, HttpServletRequest request) {
this.info = info;
this.methodsMatch = (info.getMethodsCondition().getMatchingCondition(request) != null);
this.consumesMatch = (info.getConsumesCondition().getMatchingCondition(request) != null);
this.producesMatch = (info.getProducesCondition().getMatchingCondition(request) != null);
this.paramsMatch = (info.getParamsCondition().getMatchingCondition(request) != null);
}
public RequestMappingInfo getInfo() {
return this.info;
}
public boolean hasMethodsMatch() {
return this.methodsMatch;
}
public boolean hasConsumesMatch() {
return (hasMethodsMatch() && this.consumesMatch);
}
public boolean hasProducesMatch() {
return (hasConsumesMatch() && this.producesMatch);
}
public boolean hasParamsMatch() {
return (hasProducesMatch() && this.paramsMatch);
}
@Override
public String toString() {
return this.info.toString();
}
}
}
/**
* Default handler for HTTP OPTIONS.
*/
private static class HttpOptionsHandler {
private final HttpHeaders headers = new HttpHeaders();
public HttpOptionsHandler(Set<String> declaredMethods) {
this.headers.setAllow(initAllowedHttpMethods(declaredMethods));
}
private static Set<HttpMethod> initAllowedHttpMethods(Set<String> declaredMethods) {
Set<HttpMethod> result = new LinkedHashSet<>(declaredMethods.size());
if (declaredMethods.isEmpty()) {
for (HttpMethod method : HttpMethod.values()) {
if (method != HttpMethod.TRACE) {
result.add(method);
}
}
}
else {
for (String method : declaredMethods) {
HttpMethod httpMethod = HttpMethod.valueOf(method);
result.add(httpMethod);
if (httpMethod == HttpMethod.GET) {
result.add(HttpMethod.HEAD);
}
}
result.add(HttpMethod.OPTIONS);
}
return result;
}
@SuppressWarnings("unused")
public HttpHeaders handle() {
return this.headers;
}
}
}
AbstractHandlerMethodMapping
AbstractHandlerMethodMapping
是RequestMappingInfoHandlerMapping
的父类。
package org.springframework.web.servlet.handler;
// 省略 imports
/**
* Abstract base class for HandlerMapping implementations that define
* a mapping between a request and a HandlerMethod.
*
* For each registered handler method, a unique mapping is maintained with
* subclasses defining the details of the mapping type .
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 3.1
* @param the mapping for a HandlerMethod containing the conditions
* needed to match the handler method to incoming request.
*/
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping
implements InitializingBean {
/**
* Bean name prefix for target beans behind scoped proxies. Used to exclude those
* targets from handler method detection, in favor of the corresponding proxies.
*
* We're not checking the autowire-candidate status here, which is how the
* proxy target filtering problem is being handled at the autowiring level,
* since autowire-candidate may have been turned to false for other
* reasons, while still expecting the bean to be eligible for handler methods.
* Originally defined in org.springframework.aop.scope.ScopedProxyUtils
* but duplicated here to avoid a hard dependency on the spring-aop module.
*/
private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH =
new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class,
"handle"));
private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
static {
ALLOW_CORS_CONFIG.addAllowedOrigin("*");
ALLOW_CORS_CONFIG.addAllowedMethod("*");
ALLOW_CORS_CONFIG.addAllowedHeader("*");
ALLOW_CORS_CONFIG.setAllowCredentials(true);
}
private boolean detectHandlerMethodsInAncestorContexts = false;
@Nullable
private HandlerMethodMappingNamingStrategy<T> namingStrategy;
private final MappingRegistry mappingRegistry = new MappingRegistry();
/**
* Whether to detect handler methods in beans in ancestor ApplicationContexts.
* Default is "false": Only beans in the current ApplicationContext are
* considered, i.e. only in the context that this HandlerMapping itself
* is defined in (typically the current DispatcherServlet's context).
* Switch this flag on to detect handler beans in ancestor contexts
* (typically the Spring root WebApplicationContext) as well.
* @see #getCandidateBeanNames()
*/
public void setDetectHandlerMethodsInAncestorContexts(
boolean detectHandlerMethodsInAncestorContexts) {
this.detectHandlerMethodsInAncestorContexts = detectHandlerMethodsInAncestorContexts;
}
/**
* Configure the naming strategy to use for assigning a default name to every
* mapped handler method.
* The default naming strategy is based on the capital letters of the
* class name followed by "#" and then the method name, e.g. "TC#getFoo"
* for a class named TestController with method getFoo.
*/
public void setHandlerMethodMappingNamingStrategy(
HandlerMethodMappingNamingStrategy<T> namingStrategy) {
this.namingStrategy = namingStrategy;
}
/**
* Return the configured naming strategy or null.
*/
@Nullable
public HandlerMethodMappingNamingStrategy<T> getNamingStrategy() {
return this.namingStrategy;
}
/**
* Return a (read-only) map with all mappings and HandlerMethod's.
*/
public Map<T, HandlerMethod> getHandlerMethods() {
this.mappingRegistry.acquireReadLock();
try {
return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
/**
* Return the handler methods for the given mapping name.
* @param mappingName the mapping name
* @return a list of matching HandlerMethod's or null; the returned
* list will never be modified and is safe to iterate.
* @see #setHandlerMethodMappingNamingStrategy
*/
@Nullable
public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) {
return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName);
}
/**
* Return the internal mapping registry. Provided for testing purposes.
*/
MappingRegistry getMappingRegistry() {
return this.mappingRegistry;
}
/**
* Register the given mapping.
* 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) {
if (logger.isTraceEnabled()) {
logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
}
this.mappingRegistry.register(mapping, handler, method);
}
/**
* Un-register the given mapping.
* This method may be invoked at runtime after initialization has completed.
* @param mapping the mapping to unregister
*/
public void unregisterMapping(T mapping) {
if (logger.isTraceEnabled()) {
logger.trace("Unregister mapping \"" + mapping + "\"");
}
this.mappingRegistry.unregister(mapping);
}
// Handler method detection
/**
* Detects handler methods at initialization.
* 该类潜在实现了接口 InitializingBean,
* 该方法是接口 InitializingBean 定义的初始化化方法,会在当前bean创建过程中
* 初始化过程中被调用。该方法的主要作用是检测所有的控制器方法。
* @see #initHandlerMethods
*/
@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
/**
* Scan beans in the ApplicationContext, detect and register handler methods.
* @see #getCandidateBeanNames()
* @see #processCandidateBean
* @see #handlerMethodsInitialized
*/
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
/**
* Determine the names of candidate beans in the application context.
* @since 5.1
* @see #setDetectHandlerMethodsInAncestorContexts
* @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors
*/
protected String[] getCandidateBeanNames() {
return (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
obtainApplicationContext(), Object.class) :
obtainApplicationContext().getBeanNamesForType(Object.class));
}
/**
* Determine the type of the specified candidate bean and call
* #detectHandlerMethods if identified as a handler type.
* This implementation avoids bean creation through checking
* org.springframework.beans.factory.BeanFactory#getType
* and calling #detectHandlerMethods with the bean name.
* @param beanName the name of the candidate bean
* @since 5.1
* @see #isHandler
* @see #detectHandlerMethods
*/
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
if (beanType != null && isHandler(beanType)) {
// 检测到这是一个控制器类,进一步检测其中的控制器方法
detectHandlerMethods(beanName);
}
}
/**
* Look for handler methods in the specified handler bean.
* 检测一个控制器类中的控制器方法,也就是那些使用了注解@RequestMapping的方法
* @param handler either a bean name or an actual handler instance
* @see #getMappingForMethod
*/
protected void detectHandlerMethods(Object handler) {
// 获取参数handler的类型
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
// 针对每个控制器方法,创建相应的RequestMappingInfo对象
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
// 针对每个method,定位其最终要被调用的控制器方法,记为invocableMethod,使用
// 方法 registerHandlerMethod 将其登记到 mappingRegistry
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
private String formatMappings(Class<?> userType, Map<Method, T> methods) {
String formattedType = Arrays.stream(userType.getPackage().getName().split("\\."))
.map(p -> p.substring(0, 1))
.collect(Collectors.joining(".", "", ".")) + userType.getSimpleName();
Function<Method, String> methodFormatter = method -> Arrays.stream(method.getParameterTypes())
.map(Class::getSimpleName)
.collect(Collectors.joining(",", "(", ")"));
return methods.entrySet().stream()
.map(e -> {
Method method = e.getKey();
return e.getValue() + ": " + method.getName() + methodFormatter.apply(method);
})
.collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
}
/**
* Register a handler method and its unique mapping. Invoked at startup for
* each detected handler method.
* @param handler the bean name of the handler or the handler instance
* @param method the method to register
* @param mapping the mapping conditions associated with the handler method
* @throws IllegalStateException if another method was already registered
* under the same mapping
*/
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
/**
* Create the HandlerMethod instance.
* @param handler either a bean name or an actual handler instance
* @param method the target method
* @return the created HandlerMethod
*/
protected HandlerMethod createHandlerMethod(Object handler, Method method) {
HandlerMethod handlerMethod;
if (handler instanceof String) {
String beanName = (String) handler;
handlerMethod = new HandlerMethod(beanName,
obtainApplicationContext().getAutowireCapableBeanFactory(), method);
}
else {
handlerMethod = new HandlerMethod(handler, method);
}
return handlerMethod;
}
/**
* Extract and return the CORS configuration for the mapping.
*/
@Nullable
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) {
return null;
}
/**
* Invoked after all handler methods have been detected.
* @param handlerMethods a read-only map with handler methods and mappings.
*/
protected void handlerMethodsInitialized(Map<T, HandlerMethod> handlerMethods) {
// Total includes detected mappings + explicit registrations via registerMapping
int total = handlerMethods.size();
if ((logger.isTraceEnabled() && total == 0) || (logger.isDebugEnabled() && total > 0) ) {
logger.debug(total + " mappings in " + formatMappingName());
}
}
// Handler method lookup
/**
* Look up a handler method for the given request.
*/
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
/**
* Look up the best-matching handler method for the current request.
* If multiple matches are found, the best match is selected.
* @param lookupPath mapping lookup path within the current servlet mapping
* @param request the current request
* @return the best-matching handler method, or null if no match
* @see #handleMatch(Object, String, HttpServletRequest)
* @see #handleNoMatch(Set, String, HttpServletRequest)
*/
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request)
throws Exception {
List<Match> matches = new ArrayList<>();
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
private void addMatchingMappings(Collection<T> mappings, List<Match> matches,
HttpServletRequest request) {
for (T mapping : mappings) {
T match = getMatchingMapping(mapping, request);
if (match != null) {
matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
}
}
}
/**
* Invoked when a matching mapping is found.
* @param mapping the matching mapping
* @param lookupPath mapping lookup path within the current servlet mapping
* @param request the current request
*/
protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) {
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
}
/**
* Invoked when no matching mapping is not found.
* @param mappings all registered mappings
* @param lookupPath mapping lookup path within the current servlet mapping
* @param request the current request
* @throws ServletException in case of errors
*/
@Nullable
protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath,
HttpServletRequest request)
throws Exception {
return null;
}
@Override
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request);
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) {
return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG;
}
else {
CorsConfiguration corsConfigFromMethod =
this.mappingRegistry.getCorsConfiguration(handlerMethod);
corsConfig = (corsConfig != null ?
corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod);
}
}
return corsConfig;
}
// Abstract template methods
/**
* Whether the given type is a handler with handler methods.
* @param beanType the type of the bean being checked
* @return "true" if this a handler type, "false" otherwise.
*/
protected abstract boolean isHandler(Class<?> beanType);
/**
* Provide the mapping for a handler method. A method for which no
* mapping can be provided is not a handler method.
* @param method the method to provide a mapping for
* @param handlerType the handler type, possibly a sub-type of the method's
* declaring class
* @return the mapping, or null if the method is not mapped
*/
@Nullable
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
/**
* Extract and return the URL paths contained in a mapping.
*/
protected abstract Set<String> getMappingPathPatterns(T mapping);
/**
* Check if a mapping matches the current request and return a (potentially
* new) mapping with conditions relevant to the current request.
* @param mapping the mapping to get a match for
* @param request the current HTTP servlet request
* @return the match, or null if the mapping doesn't match
*/
@Nullable
protected abstract T getMatchingMapping(T mapping, HttpServletRequest request);
/**
* Return a comparator for sorting matching mappings.
* The returned comparator should sort 'better' matches higher.
* @param request the current request
* @return the comparator (never null)
*/
protected abstract Comparator<T> getMappingComparator(HttpServletRequest request);
/**
* A registry that maintains all mappings to handler methods, exposing methods
* to perform lookups and providing concurrent access.
* Package-private for testing purposes.
*/
class MappingRegistry {
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* Return all mappings and handler methods. Not thread-safe.
* @see #acquireReadLock()
*/
public Map<T, HandlerMethod> getMappings() {
return this.mappingLookup;
}
/**
* Return matches for the given URL path. Not thread-safe.
* @see #acquireReadLock()
*/
@Nullable
public List<T> getMappingsByUrl(String urlPath) {
return this.urlLookup.get(urlPath);
}
/**
* Return handler methods by mapping name. Thread-safe for concurrent use.
*/
public List<HandlerMethod> getHandlerMethodsByMappingName(String mappingName) {
return this.nameLookup.get(mappingName);
}
/**
* Return CORS configuration. Thread-safe for concurrent use.
*/
public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) {
HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod();
return this.corsLookup.get(original != null ? original : handlerMethod);
}
/**
* Acquire the read lock when using getMappings and getMappingsByUrl.
*/
public void acquireReadLock() {
this.readWriteLock.readLock().lock();
}
/**
* Release the read lock after using getMappings and getMappingsByUrl.
*/
public void releaseReadLock() {
this.readWriteLock.readLock().unlock();
}
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
assertUniqueMethodMapping(handlerMethod, mapping);
this.mappingLookup.put(mapping, handlerMethod);
List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
this.registry.put(mapping, new MappingRegistration<>(mapping,
handlerMethod, directUrls, name));
}
finally {
this.readWriteLock.writeLock().unlock();
}
}
private void assertUniqueMethodMapping(HandlerMethod newHandlerMethod, T mapping) {
HandlerMethod handlerMethod = this.mappingLookup.get(mapping);
if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) {
throw new IllegalStateException(
"Ambiguous mapping. Cannot map '" + newHandlerMethod.getBean() +
"' method \n" +
newHandlerMethod + "\nto " + mapping + ": There is already '" +
handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped.");
}
}
private List<String> getDirectUrls(T mapping) {
List<String> urls = new ArrayList<>(1);
for (String path : getMappingPathPatterns(mapping)) {
if (!getPathMatcher().isPattern(path)) {
urls.add(path);
}
}
return urls;
}
private void addMappingName(String name, HandlerMethod handlerMethod) {
List<HandlerMethod> oldList = this.nameLookup.get(name);
if (oldList == null) {
oldList = Collections.emptyList();
}
for (HandlerMethod current : oldList) {
if (handlerMethod.equals(current)) {
return;
}
}
List<HandlerMethod> newList = new ArrayList<>(oldList.size() + 1);
newList.addAll(oldList);
newList.add(handlerMethod);
this.nameLookup.put(name, newList);
}
public void unregister(T mapping) {
this.readWriteLock.writeLock().lock();
try {
MappingRegistration<T> definition = this.registry.remove(mapping);
if (definition == null) {
return;
}
this.mappingLookup.remove(definition.getMapping());
for (String url : definition.getDirectUrls()) {
List<T> list = this.urlLookup.get(url);
if (list != null) {
list.remove(definition.getMapping());
if (list.isEmpty()) {
this.urlLookup.remove(url);
}
}
}
removeMappingName(definition);
this.corsLookup.remove(definition.getHandlerMethod());
}
finally {
this.readWriteLock.writeLock().unlock();
}
}
private void removeMappingName(MappingRegistration<T> definition) {
String name = definition.getMappingName();
if (name == null) {
return;
}
HandlerMethod handlerMethod = definition.getHandlerMethod();
List<HandlerMethod> oldList = this.nameLookup.get(name);
if (oldList == null) {
return;
}
if (oldList.size() <= 1) {
this.nameLookup.remove(name);
return;
}
List<HandlerMethod> newList = new ArrayList<>(oldList.size() - 1);
for (HandlerMethod current : oldList) {
if (!current.equals(handlerMethod)) {
newList.add(current);
}
}
this.nameLookup.put(name, newList);
}
}
private static class MappingRegistration<T> {
private final T mapping;
private final HandlerMethod handlerMethod;
private final List<String> directUrls;
@Nullable
private final String mappingName;
public MappingRegistration(T mapping, HandlerMethod handlerMethod,
@Nullable List<String> directUrls, @Nullable String mappingName) {
Assert.notNull(mapping, "Mapping must not be null");
Assert.notNull(handlerMethod, "HandlerMethod must not be null");
this.mapping = mapping;
this.handlerMethod = handlerMethod;
this.directUrls = (directUrls != null ? directUrls : Collections.emptyList());
this.mappingName = mappingName;
}
public T getMapping() {
return this.mapping;
}
public HandlerMethod getHandlerMethod() {
return this.handlerMethod;
}
public List<String> getDirectUrls() {
return this.directUrls;
}
@Nullable
public String getMappingName() {
return this.mappingName;
}
}
/**
* A thin wrapper around a matched HandlerMethod and its mapping, for the purpose of
* comparing the best match with a comparator in the context of the current request.
*/
private class Match {
private final T mapping;
private final HandlerMethod handlerMethod;
public Match(T mapping, HandlerMethod handlerMethod) {
this.mapping = mapping;
this.handlerMethod = handlerMethod;
}
@Override
public String toString() {
return this.mapping.toString();
}
}
private class MatchComparator implements Comparator<Match> {
private final Comparator<T> comparator;
public MatchComparator(Comparator<T> comparator) {
this.comparator = comparator;
}
@Override
public int compare(Match match1, Match match2) {
return this.comparator.compare(match1.mapping, match2.mapping);
}
}
private static class EmptyHandler {
@SuppressWarnings("unused")
public void handle() {
throw new UnsupportedOperationException("Not implemented");
}
}
}