RequestMapping 注解的解析匹配注册

【RequestMapping 注解的解析匹配注册】犀渠玉剑良家子,白马金羁侠少年。这篇文章主要讲述RequestMapping 注解的解析匹配注册相关的知识,希望能为你提供帮助。
RequestMapping 注解的解析、匹配、注册

1)创建 RequestMappingHandlerMapping 实例时会触发 afterPropertiesSet 调用。 2)读取容器中所有带有 Controller 或 RequestMapping 注解的类。 3)读取此类中所有满足过滤器 ReflectionUtils.USER_DECLARED_METHODS 的方法, 读取处理方法上的RequestMapping 注解信息, 将其解析并封装为 RequestMappingInfo 注册到 RequestMappingHandlerMapping#mappingRegistry 中。RequestMappingHandlerMapping# @Nullable protected RequestMappingInfo getMappingForMethod(Method method, Class< ?> handlerType) { // 1)从处理方法中读取 RequestMapping 信息并创建 RequestMappingInfo RequestMappingInfo info = createRequestMappingInfo(method); if (info != null) { // 2)从处理器类中读取 RequestMapping 信息并创建 RequestMappingInfo RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); if (typeInfo != null) { // 如果存在,则合并 info = typeInfo.combine(info); } // 3)如果处理类上配置了前缀路径 String prefix = getPathPrefix(handlerType); if (prefix != null) { // 则完成路径拼接 info = RequestMappingInfo.paths(prefix).build().combine(info); } } return info; }@Nullable private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { // 读取注解元素上的 RequestMapping 注解信息 RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); if (requestMapping == null) { return null; }/** * 1)如果是 class,则通过 getCustomTypeCondition 读取 RequestCondition * 2)如果是 method,则通过 getCustomMethodCondition 读取 RequestCondition *特性未实现,都返回 null */ RequestCondition< ?> condition = (element instanceof Class ? getCustomTypeCondition((Class< ?> ) element) : getCustomMethodCondition((Method) element)); return createRequestMappingInfo(requestMapping, condition); }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(); }

解析 RequestMapping#path
1)解析 path/value 参数中指定的所有路径 2)如果合并的 path 参数不以 / 开头,则添加前置的 /【最佳实践:编写的每个请求路径都以 / 开头,避免不必要的调用】 3)注入 UrlPathHelper 用于读取 request 的请求路径,注入 AntPathMatcher 用于完成路径匹配【如果未指定】。PatternsRequestCondition# /** *指定的所有请求路径 */ private final Set< String> patterns; /** *用于读取请求路径的工具类 */ private final UrlPathHelper pathHelper; /** *用于执行路径匹配的 AntPathMatcher */ private final PathMatcher pathMatcher; /** *是否启用后缀模式,默认为 false */ private final boolean useSuffixPatternMatch; /** *是否自动添加尾部 /,默认为 true */ private final boolean useTrailingSlashMatch; public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper, @Nullable PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch, @Nullable List< String> fileExtensions) {this(Arrays.asList(patterns), urlPathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, fileExtensions); }private PatternsRequestCondition(Collection< String> patterns, @Nullable UrlPathHelper urlPathHelper, @Nullable PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch, @Nullable List< String> fileExtensions) {this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns)); this.pathHelper = (urlPathHelper != null ? urlPathHelper : new UrlPathHelper()); this.pathMatcher = (pathMatcher != null ? pathMatcher : new AntPathMatcher()); this.useSuffixPatternMatch = useSuffixPatternMatch; this.useTrailingSlashMatch = useTrailingSlashMatch; if (fileExtensions != null) { for (String fileExtension : fileExtensions) { if (fileExtension.charAt(0) != ‘.‘) { fileExtension = "." + fileExtension; } this.fileExtensions.add(fileExtension); } } }private static Set< String> prependLeadingSlash(Collection< String> patterns) { Set< String> result = new LinkedHashSet< > (patterns.size()); for (String pattern : patterns) { // 如果请求路径不是以 / 开头,则添加 / 前缀 if (StringUtils.hasLength(pattern) & & !pattern.startsWith("/")) { pattern = "/" + pattern; } result.add(pattern); } return result; }

  • RequestMapping#path 的匹配过程
PatternsRequestCondition# /** * Checks if any of the patterns match the given request and returns an instance * that is guaranteed to contain matching patterns, sorted via * {@link PathMatcher#getPatternComparator(String)}. * < p> A matching pattern is obtained by making checks in the following order: * < ul> * < li> Direct match * < li> Pattern match with ".*" appended if the pattern doesn‘t already contain a "." * < li> Pattern match * < li> Pattern match with "/" appended if the pattern doesn‘t already end in "/" * < /ul> */ @Override @Nullable public PatternsRequestCondition getMatchingCondition(HttpServletRequest request) { // 1)如果未指定请求路径,则默认匹配 if (patterns.isEmpty()) { return this; } // 读取请求路径 final String lookupPath = pathHelper.getLookupPathForRequest(request); // 读取匹配的所有路径 final List< String> matches = getMatchingPatterns(lookupPath); return !matches.isEmpty() ? new PatternsRequestCondition(matches, pathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, fileExtensions) : null; }/** * Find the patterns matching the given lookup path. */ public List< String> getMatchingPatterns(String lookupPath) { final List< String> matches = new ArrayList< > (); for (final String pattern : patterns) { final String match = getMatchingPattern(pattern, lookupPath); if (match != null) { matches.add(match); } } if (matches.size() > 1) { matches.sort(pathMatcher.getPatternComparator(lookupPath)); } return matches; }@Nullable private String getMatchingPattern(String pattern, String lookupPath) { // 1)pattern 和请求路径相等 if (pattern.equals(lookupPath)) { return pattern; } // 2)是否使用后缀模式,默认为 false if (useSuffixPatternMatch) { if (!fileExtensions.isEmpty() & & lookupPath.indexOf(‘.‘) != -1) { for (final String extension : fileExtensions) { if (pathMatcher.match(pattern + extension, lookupPath)) { return pattern + extension; } } } else { final boolean hasSuffix = pattern.indexOf(‘.‘) != -1; if (!hasSuffix & & pathMatcher.match(pattern + ".*", lookupPath)) { return pattern + ".*"; } } } // 3)使用 pathMatcher 指定路径匹配,默认是 AntPathMatcher if (pathMatcher.match(pattern, lookupPath)) { return pattern; } // 4)默认为 true if (useTrailingSlashMatch) { // 给 pattern 添加 / 后缀之后再次进行匹配 if (!pattern.endsWith("/") & & pathMatcher.match(pattern + "/", lookupPath)) { return pattern +"/"; } } return null; }

解析 RequestMapping#method
1)写入所有支持的 HttpMethod RequestMethodsRequestCondition# /** *支持的所有请求方法 */ private final Set< RequestMethod> methods; public RequestMethodsRequestCondition(RequestMethod... requestMethods) { this(Arrays.asList(requestMethods)); }private RequestMethodsRequestCondition(Collection< RequestMethod> requestMethods) { this.methods = Collections.unmodifiableSet(new LinkedHashSet< > (requestMethods)); }

  • RequestMapping#method 的匹配过程
RequestMethodsRequestCondition# public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest request) { if (CorsUtils.isPreFlightRequest(request)) { return matchPreFlight(request); }// 1)如果未指定 RequestMapping#method if (getMethods().isEmpty()) { // 请求方法为 OPTIONS & & 请求的分派类型不是 DispatcherType.ERROR if (RequestMethod.OPTIONS.name().equals(request.getMethod()) & & !DispatcherType.ERROR.equals(request.getDispatcherType())) { return null; // No implicit match for OPTIONS (we handle it) } return this; }return matchRequestMethod(request.getMethod()); }@Nullable private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) { HttpMethod httpMethod = HttpMethod.resolve(httpMethodValue); if (httpMethod != null) { // 1)支持的请求方法列表中存在此 HttpMethod for (RequestMethod method : getMethods()) { if (httpMethod.matches(method.name())) { return new RequestMethodsRequestCondition(method); } } /** * 2)如果是 HttpMethod.HEAD 方式 * & & 支持的请求方式列表中存在 RequestMethod.GET,则返回 GET */ if (httpMethod == HttpMethod.HEAD & & getMethods().contains(RequestMethod.GET)) { return GET_CONDITION; } } return null; }

解析 RequestMapping#params
1)将请求参数封装为 ParamExpression 后写入 ParamsRequestCondition /** *参数表达式集合 */ private final Set< ParamExpression> expressions; public ParamsRequestCondition(String... params) { this(parseExpressions(params)); }private ParamsRequestCondition(Collection< ParamExpression> conditions) { this.expressions = Collections.unmodifiableSet(new LinkedHashSet< > (conditions)); }

  • RequestMapping#params 匹配过程
ParamsRequestCondition# @Override @Nullable public ParamsRequestCondition getMatchingCondition(HttpServletRequest request) { // 只要有一个参数不匹配,则请求不匹配 for (final ParamExpression expression : expressions) { if (!expression.match(request)) { return null; } } return this; }AbstractNameValueExpression# public final boolean match(HttpServletRequest request) { boolean isMatch; // 1)如果指定了参数值,则执行值匹配 if (this.value != null) { isMatch = matchValue(request); } // 2)执行名称匹配 else { isMatch = matchName(request); } return (this.isNegated ? !isMatch : isMatch); }ParamExpression# @Override protected boolean matchName(HttpServletRequest request) { // 表单参数中存在该参数 || 参数集合中存在该参数 return (WebUtils.hasSubmitParameter(request, this.name) || request.getParameterMap().containsKey(this.name)); }@Override protected boolean matchValue(HttpServletRequest request) { // 请求参数 name 的参数值和配置值相等 return ObjectUtils.nullSafeEquals(this.value, request.getParameter(this.name)); }

解析 RequestMapping#headers
HeadersRequestCondition# /** *解析的 header 参数集合 */ private final Set< HeaderExpression> expressions; public HeadersRequestCondition(String... headers) { this(parseExpressions(headers)); }private HeadersRequestCondition(Collection< HeaderExpression> conditions) { this.expressions = Collections.unmodifiableSet(new LinkedHashSet< > (conditions)); }private static Collection< HeaderExpression> parseExpressions(String... headers) { Set< HeaderExpression> expressions = new LinkedHashSet< > (); for (String header : headers) { HeaderExpression expr = new HeaderExpression(header); // 如果是 Accept 和 Content-Type 头,则忽略 if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) { continue; } expressions.add(expr); } return expressions; }

  • RequestMapping#headers 匹配过程
HeadersRequestCondition# public HeadersRequestCondition getMatchingCondition(HttpServletRequest request) { if (CorsUtils.isPreFlightRequest(request)) { return PRE_FLIGHT_MATCH; } // 只要有一个请求头不匹配,则该请求不匹配 for (final HeaderExpression expression : expressions) { if (!expression.match(request)) { return null; } } return this; }AbstractNameValueExpression# public final boolean match(HttpServletRequest request) { boolean isMatch; // 1)如果配置了请求头的值,则执行值匹配 if (this.value != null) { isMatch = matchValue(request); } // 2)执行请求头名称匹配 else { isMatch = matchName(request); } return (this.isNegated ? !isMatch : isMatch); }HeaderExpression# // 存在目标请求头 @Override protected boolean matchName(HttpServletRequest request) { return request.getHeader(name) != null; }// 请求头的值和配置值相等 @Override protected boolean matchValue(HttpServletRequest request) { return ObjectUtils.nullSafeEquals(value, request.getHeader(name)); }

解析 RequestMapping#consumes
1)如果 headers 中指定了 Content-Type 属性,则将其解析并加入到 ConsumesRequestCondition#expressions 中。 2)解析 consumes 参数中配置的所有 MediaType,并将其加入到 ConsumesRequestCondition#expressions 中。ConsumesRequestCondition# public ConsumesRequestCondition(String[] consumes, @Nullable String[] headers) { this(parseExpressions(consumes, headers)); }private ConsumesRequestCondition(Collection< ConsumeMediaTypeExpression> expressions) { this.expressions = new ArrayList< > (expressions); Collections.sort(this.expressions); }private static Set< ConsumeMediaTypeExpression> parseExpressions(String[] consumes, @Nullable String[] headers) { final Set< ConsumeMediaTypeExpression> result = new LinkedHashSet< > (); // 1)如果 headers 参数不为 null & & headers 中存在 Content-Type 配置,则将其加入到 result 中。 if (headers != null) { for (final String header : headers) { final HeaderExpression expr = new HeaderExpression(header); if ("Content-Type".equalsIgnoreCase(expr.name) & & expr.value != null) { for (final MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated)); } } } } // 2)解析 consumes 参数中配置的所有 MediaType,将其加入到 result 中。 for (final String consume : consumes) { result.add(new ConsumeMediaTypeExpression(consume)); } return result; }AbstractMediaTypeExpression# /** *解析完成的 MediaType 类型 */ private final MediaType mediaType; /** *是否是反向匹配 */ private final boolean isNegated; AbstractMediaTypeExpression(String expression) { /** *如果表达式以 ! 开头,则表示反向匹配 */ if (expression.startsWith("!")) { this.isNegated = true; expression = expression.substring(1); } else { this.isNegated = false; } this.mediaType = MediaType.parseMediaType(expression); }MediaType# public static MediaType parseMediaType(String mediaType) { MimeType type; try { type = MimeTypeUtils.parseMimeType(mediaType); } catch (InvalidMimeTypeException ex) { throw new InvalidMediaTypeException(ex); } try { return new MediaType(type.getType(), type.getSubtype(), type.getParameters()); } catch (IllegalArgumentException ex) { throw new InvalidMediaTypeException(mediaType, ex.getMessage()); } }

  • RequestMapping#consumes 匹配过程
ConsumesRequestCondition# @Override @Nullable public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) { if (CorsUtils.isPreFlightRequest(request)) { return PRE_FLIGHT_MATCH; }// 1)如果未指定 consumes 参数则默认匹配 if (isEmpty()) { return this; }// 2)RequestMapping 指定了 consumes 参数,则执行匹配过程 MediaType contentType; try { // 读取请求的 Content-Type 属性并将其转换为 MediaType contentType = StringUtils.hasLength(request.getContentType()) ? MediaType.parseMediaType(request.getContentType()) : MediaType.APPLICATION_OCTET_STREAM; } catch (final InvalidMediaTypeException ex) { // 3)如果请求的 Content-Type 非法,则不匹配 return null; }// 3)读取所有指定的 consumes 参数表达式,进行逐个匹配 final Set< ConsumeMediaTypeExpression> result = new LinkedHashSet< > (expressions); return result.stream() .anyMatch(expression-> expression.match(contentType)) ? new ConsumesRequestCondition(result) : null; }单个 MediaType 的匹配过程 ConsumeMediaTypeExpression# public final boolean match(MediaType contentType) { // 当前 MediaType 是否匹配目标 contentType final boolean match = getMediaType().includes(contentType); // 是否是反向匹配 & & 读取匹配结果 return !isNegated() ? match : !match; }

解析 RequestMapping#produces
ProducesRequestCondition# /** *支持的结果类型 MediaType */ private final List< ProduceMediaTypeExpression> expressions; public ProducesRequestCondition(String[] produces, @Nullable String[] headers, @Nullable ContentNegotiationManager manager) {expressions = new ArrayList< > (parseExpressions(produces, headers)); Collections.sort(expressions); contentNegotiationManager = manager != null ? manager : new ContentNegotiationManager(); }private Set< ProduceMediaTypeExpression> parseExpressions(String[] produces, @Nullable String[] headers) { final Set< ProduceMediaTypeExpression> result = new LinkedHashSet< > (); // 1)如果存在 headers 配置 & & 将 Accept 头配置加入到 result 中 if (headers != null) { for (final String header : headers) { final HeaderExpression expr = new HeaderExpression(header); if ("Accept".equalsIgnoreCase(expr.name) & & expr.value != null) { for (final MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { result.add(new ProduceMediaTypeExpression(mediaType, expr.isNegated)); } } } } // 2)将所有配置的 MediaType 加入到 result 中 for (final String produce : produces) { result.add(new ProduceMediaTypeExpression(produce)); } return result; }

  • RequestMapping#produces 的匹配过程
ProducesRequestCondition# public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) { if (CorsUtils.isPreFlightRequest(request)) { return PRE_FLIGHT_MATCH; } // 1)如果未配置 produces 则匹配 if (isEmpty()) { return this; } // 2)解析客户端能接受的所有 MediaType List< MediaType> acceptedMediaTypes; try { acceptedMediaTypes = getAcceptedMediaTypes(request); } catch (final HttpMediaTypeException ex) { return null; }// 3)配置的 MediaType 列表中存在请求能接受的 MediaType final Set< ProduceMediaTypeExpression> result = new LinkedHashSet< > (expressions); result.removeIf(expression -> !expression.match(acceptedMediaTypes)); if (!result.isEmpty()) { return new ProducesRequestCondition(result, contentNegotiationManager); } // 4)如果客户端能接受所有结果类型 */* else if (acceptedMediaTypes.contains(MediaType.ALL)) { return EMPTY_CONDITION; } else { return null; } }ProduceMediaTypeExpression# public final boolean match(List< MediaType> acceptedMediaTypes) { final boolean match = matchMediaType(acceptedMediaTypes); return !isNegated() ? match : !match; }private boolean matchMediaType(List< MediaType> acceptedMediaTypes) { for (final MediaType acceptedMediaType : acceptedMediaTypes) { // 当前 MediaType 和目标 MediaType 匹配 if (getMediaType().isCompatibleWith(acceptedMediaType)) { return true; } } return false; }

RequestMappingInfo 的匹配过程
RequestMappingInfo# /** * @RequestMapping 的 name 属性值 */ @Nullable private final String name; /** *@RequestMapping path 参数匹配条件 */ private final PatternsRequestCondition patternsCondition; /** *@RequestMapping method 参数匹配条件 */ private final RequestMethodsRequestCondition methodsCondition; /** *@RequestMapping params 参数匹配条件 */ private final ParamsRequestCondition paramsCondition; /** *@RequestMapping headers 参数匹配条件 */ private final HeadersRequestCondition headersCondition; /** * @RequestMapping consumers 参数匹配条件 */ private final ConsumesRequestCondition consumesCondition; /** * @RequestMapping produces 参数匹配条件 */ private final ProducesRequestCondition producesCondition; private final RequestConditionHolder customConditionHolder; /** *使用此RequestMappingInfo 中的所有条件来匹配目标请求,如果匹配, *则返回一个新的 RequestMappingInfo,否则返回 null。 */ @Override @Nullable public RequestMappingInfo getMatchingCondition(HttpServletRequest request) { final RequestMethodsRequestCondition methods = methodsCondition.getMatchingCondition(request); if (methods == null) { return null; }final ParamsRequestCondition params = paramsCondition.getMatchingCondition(request); if (params == null) { return null; }final HeadersRequestCondition headers = headersCondition.getMatchingCondition(request); if (headers == null) { return null; }final ConsumesRequestCondition consumes = consumesCondition.getMatchingCondition(request); if (consumes == null) { return null; }final ProducesRequestCondition produces = producesCondition.getMatchingCondition(request); if (produces == null) { return null; }final PatternsRequestCondition patterns = patternsCondition.getMatchingCondition(request); if (patterns == null) { return null; }final RequestConditionHolder custom = customConditionHolder.getMatchingCondition(request); if (custom == null) { return null; }return new RequestMappingInfo(name, patterns, methods, params, headers, consumes, produces, custom.getCondition()); }

RequestMappingInfo 的注册过程
AbstractHandlerMethodMapping# private final MappingRegistry mappingRegistry = new MappingRegistry(); class MappingRegistry { /** * RequestMappingInfo 和 MappingRegistration 的注册缓存 */ private final Map< T, MappingRegistration< T> > registry = new HashMap< > (); /** * RequestMappingInfo 和 HandlerMethod 的注册缓存 */ private final Map< T, HandlerMethod> mappingLookup = new LinkedHashMap< > (); /** * url 和 RequestMappingInfo 的注册缓存 */ private final MultiValueMap< String, T> urlLookup = new LinkedMultiValueMap< > (); /** * MappingName 和 List< HandlerMethod> 的注册缓存 */ private final Map< String, List< HandlerMethod> > nameLookup = new ConcurrentHashMap< > (); /** * HandlerMethod 和 CorsConfiguration 的注册缓存 */ private final Map< HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap< > (); /** *保障线程安全的读写锁 */ private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); public void register(T mapping, Object handler, Method method) { // 获取读锁 this.readWriteLock.writeLock().lock(); try { // 创建封装了 handler 和 method 的 HandlerMethod 实例, final HandlerMethod handlerMethod = createHandlerMethod(handler, method); // 确保映射是唯一的 assertUniqueMethodMapping(handlerMethod, mapping); // 写入 RequestMappingInfo 和 handlerMethod 映射到 mappingLookup 缓存 this.mappingLookup.put(mapping, handlerMethod); final List< String> directUrls = getDirectUrls(mapping); // 将配置的 url 和 RequestMappingInfo 映射写入 urlLookup 缓存 for (final String url : directUrls) { this.urlLookup.add(url, mapping); }String name = null; if (getNamingStrategy() != null) { /** *根据命名策略读取映射的名称 * HelloCont.hello() => HC#hello */ name = getNamingStrategy().getName(handlerMethod, mapping); addMappingName(name, handlerMethod); }// 读取控制器或处理方法上 @CrossOrigin 注解配置的跨域信息 final CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { // 写入 corsLookup 缓存中 this.corsLookup.put(handlerMethod, corsConfig); }// 将 RequestMappingInfo 和 MappingRegistration 映射写入 registry 中 this.registry.put(mapping, new MappingRegistration< > (mapping, handlerMethod, directUrls, name)); } finally { this.readWriteLock.writeLock().unlock(); } }private List< String> getDirectUrls(T mapping) { final List< String> urls = new ArrayList< > (1); // 从 RequestMappingInfo 中读取配置的请求映射集合 for (final String path : getMappingPathPatterns(mapping)) { // 如果是直接的 Url【pattern 不包含 * 和 ?】 if (!getPathMatcher().isPattern(path)) { urls.add(path); } } return urls; }private void addMappingName(String name, HandlerMethod handlerMethod) { // 根据 MappingName 读取 HandlerMethod 列表 List< HandlerMethod> oldList = this.nameLookup.get(name); if (oldList == null) { oldList = Collections.emptyList(); }// 如果目标 HandlerMethod 已经存在,则直接返回 for (final HandlerMethod current : oldList) { if (handlerMethod.equals(current)) { return; } }// 将 MappingName 和 HandlerMethod 映射写入 nameLookup 中 final List< HandlerMethod> newList = new ArrayList< > (oldList.size() + 1); newList.addAll(oldList); newList.add(handlerMethod); 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; }private class Match { private final T mapping; private final HandlerMethod handlerMethod; }


    推荐阅读