回答
以注解为例。
Spring 容器在启动时会扫描 @Controller
或 @RestController
,然后解析类和方法上面的注解,将路径和处理器方法(handler method
)之间的映射关系存储起来。当请求到达时,我们只需要根据 request 从注册表中匹配对应的 handler method
即可。
详解
原理很简单,其实内部实现并不简单。我们直接看源码。
在面试题 Spring MVC 有哪些核心组件?中讲到,DispatcherServlet
中的 doDispatch()
是处理请求的核心逻辑,其中的 getHandler()
就是根据 request 获取对应的 Handler:
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
HandlerMapping 是来将请求 URL 映射到具体的 Handler 的核心组件,它内部包含了 url 和 Controller&Method 的映射关系。Spring MVC 内置了多种实现:
BeanNameUrlHandlerMapping
:根据 Bean 的名称来匹配 URL。ControllerClassNameHandlerMapping
:已废弃。RequestMappingHandlerMapping
:Spring MVC 默认使用的HandlerMapping
实现。通过注解@RequestMapping
或其他请求映射注解(如@GetMapping
,@PostMapping
)来映射处理器。SimpleUrlHandlerMapping
:使用配置文件中的 URL 映射,例如通过 bean 的配置来手动指定 URL 和 Handler 的对应关系。DefaultAnnotationHandlerMapping
:已废弃。RouterFunctionMapping
:使用函数式风格定义路由。Spring 5 引入,用于响应式 Web 应用(WebFlux)。SimpleServletHandlerAdapter
:辅助映射 Servlet 类型的 Bean。一般用于兼容传统的 Servlet 组件。
大明哥的应用中主要有四种,如下:
我们目前主要是使用 RequestMappingHandlerMapping
,所以我们直接看它的 getHandler()
:
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
// 省略很多代码....
}
调用 getHandlerInternal()
,该方法由子类实现,我们直接看 RequestMappingInfoHandlerMapping 就可以了:
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
try {
return super.getHandlerInternal(request);
}
finally {
ProducesRequestCondition.clearMediaTypesAttribute(request);
}
}
委托给父类的 getHandlerInternal()
:
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
首先调用 initLookupPath()
根据 request
获取 lookupPath
,其实就是提取出 url,比如我们前端请求的是 http://localhost:9901/mianshi/baodian/detail/1077120523
,则 lookupPath
就是 /mianshi/baodian/detail/1077120523
。
提取 lookupPath
后,调用 lookupHandlerMethod()
获取HandlerMethod :
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
// 根据 lookupPath 获取所有的 RequestMappingInfo
List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
if (directPathMatches != null) {
// 通过条件进行筛选,并把结果放到matches里
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// 如果还为空,则兜底遍历所有接口
addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
/*
* 如果有多个,则进行优先级排序,寻找最优的
*/
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);
if (logger.isTraceEnabled()) {
logger.trace(matches.size() + " matching mappings: " + matches);
}
if (CorsUtils.isPreFlightRequest(request)) {
for (Match match : matches) {
if (match.hasCorsConfig()) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
}
}
else {
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.getHandlerMethod().getMethod();
Method m2 = secondBestMatch.getHandlerMethod().getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}
}
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod());
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.getHandlerMethod();
}
else {
// 没有找到
return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
}
}
this.mappingRegistry
是 Spring MVC 中的注册表,用于存储和管理各种请求 URL(或其他匹配条件)到处理方法(handler method)之间的映射关系。它内部有四个映射关系:
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();
private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
- registry:用于将某种类型
T
(可能是请求路径或其他匹配条件)与MappingRegistration<T>
关联。MappingRegistration<T>
是一个包含注册信息的对象,可能包括 URL、HTTP 方法、处理方法、优先级等信息。当我们注册一个@RequestMapping
注解时,Spring MVC 会将请求路径及其相关属性存储在这个registry
中。 - pathLookup:存储了每个请求路径(作为
String
)与多个映射目标T
之间的关系。MultiValueMap
允许每个请求路径关联多个映射目标。 - nameLookup:存储了每个方法名称(
String
)与多个处理方法(HandlerMethod
)的对应关系。通常是根据方法的名称或标识来查找所有匹配的处理方法。 - corsLookup:用于存储每个处理方法(
HandlerMethod
)的 CORS 配置信息(CorsConfiguration
)。
首先调用 getMappingsByDirectPath()
从 pathLookup
注册表中获取:
public List<T> getMappingsByDirectPath(String urlPath) {
return this.pathLookup.get(urlPath);
}
大明哥个人网站 pathLookup 注册表内容如下:
我们请求的 urlPath 为 /mianshi/baodian/detail/1077120523
,所以这个注册表肯定取不到。则调用 addMatchingMappings()
从 registry
注册表中获取:
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.getRegistrations().get(mapping)));
}
}
}
这里是迭代 registry
注册表中的元素,一次调用 getMatchingMapping()
进行匹配:
public RequestMappingInfo getMatchingCondition(HttpServletRequest request) {
// 请求方法 requestMapping 是否匹配
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(request);
if (methods == null) {
return null;
}
// 请求参数是否匹配
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(request);
if (params == null) {
return null;
}
// header 是否匹配
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(request);
if (headers == null) {
return null;
}
//consums Content-Type 是否匹配
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(request);
if (consumes == null) {
return null;
}
// produces Content-Type 是否匹配
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (produces == null) {
return null;
}
// 匹配 directPath 和通配符的请求映射匹配关系
PathPatternsRequestCondition pathPatterns = null;
if (this.pathPatternsCondition != null) {
pathPatterns = this.pathPatternsCondition.getMatchingCondition(request);
if (pathPatterns == null) {
return null;
}
}
// 匹配 ant 风格的请求
PatternsRequestCondition patterns = null;
if (this.patternsCondition != null) {
patterns = this.patternsCondition.getMatchingCondition(request);
if (patterns == null) {
return null;
}
}
// 匹配自定义
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(request);
if (custom == null) {
return null;
}
return new RequestMappingInfo(this.name, pathPatterns, patterns,
methods, params, headers, consumes, produces, custom, this.options);
}
我们看 registry
注册表中的内容:
经过 getMatchingCondition()
对比我们就可以找到上图的 RequestMappingInfo。但是我们有可能会找到多个,则先进行排序,然后获取第 1 、2 个,对比两个优先级是否是一样的,如果一样则抛出异常,否则获取第一个。对比条件:
public int compareTo(RequestMappingInfo other, HttpServletRequest request) {
int result;
// Automatic vs explicit HTTP HEAD mapping
if (HttpMethod.HEAD.matches(request.getMethod())) {
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
}
result = getActivePatternsCondition().compareTo(other.getActivePatternsCondition(), request);
if (result != 0) {
return result;
}
result = this.paramsCondition.compareTo(other.getParamsCondition(), request);
if (result != 0) {
return result;
}
result = this.headersCondition.compareTo(other.getHeadersCondition(), request);
if (result != 0) {
return result;
}
result = this.consumesCondition.compareTo(other.getConsumesCondition(), request);
if (result != 0) {
return result;
}
result = this.producesCondition.compareTo(other.getProducesCondition(), request);
if (result != 0) {
return result;
}
// Implicit (no method) vs explicit HTTP method mappings
result = this.methodsCondition.compareTo(other.getMethodsCondition(), request);
if (result != 0) {
return result;
}
result = this.customConditionHolder.compareTo(other.customConditionHolder, request);
if (result != 0) {
return result;
}
return 0;
}
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。