springmvc是怎么路由到具体的方法的?

1. 背景

在工作中大量使用到了@RestController 和 @RequestMapping等注解,通过注解定义好接口url,然后请求这个地址就能够调用我们注解下的具体方法,那么springmvc是怎么将url转换为调用具体的bean的方法的呢?

2. 基础概念

  • DispatcherServlet#doDispatch springmvc 处理请求的核心类,包括找到对应方法,调用对应的bean的方法后,进行响应处理。
  • RequestMappingInfo springmvc在启动的时候会将@RequestMapping注解的信息放到RequestMappingInfo中,例如url信息会放到patternsCondition这个属性中。
  • RequestMappingInfoHandlerMapping doDispatch方法会遍历HandlerMapping的所有子类,然后调用各自的getHandlerInternal方法进行寻找匹配的bean的方法。
  • AbstractHandlerMethodMapping RequestMappingInfoHandlerMapping的模板方法,调用lookupHandlerMethod来寻找对应bean和方法。

3. 流程

  1. DispatcherServlet#doDispatch

    调用getHandler获取对应的handler

java 复制代码
mappedHandler = getHandler(processedRequest);
  1. DispatcherServlet#getHandler

    遍历handlerMappings,调用list里的各个实现类的getHandler方法;

java 复制代码
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    //初始化的时候会将HandlerMapping 的所有bean放到handlerMappings中
    if (this.handlerMappings != null) {
        //遍历
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            //找不到继续调用下一个mapping的getHandler方法,责任链模式
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

具体的handlerMapping如下:

  1. RequestMappingInfoHandlerMapping.getHandlerInternal

    目前项目中使用的都是@RequestMapping注解,所以我们重点关注RequestMappingInfoHandlerMapping就好,可以看到最终又回去调用父类的getHandlerInternal方法了

java 复制代码
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
    try {
        //调用父类的方法
        return super.getHandlerInternal(request);
    }
    finally {
        ProducesRequestCondition.clearMediaTypesAttribute(request);
    }
}
  1. AbstractHandlerMethodMapping#getHandlerInternal

这里可以看到父类的这个方法,没啥核心代码,主要是调用lookupHandlerMethod

java 复制代码
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    //提取出url,例如我现在请求的http://127.0.0.1:8080/pre/test,最终拿到的lookupPath为/pre/test
    String lookupPath = initLookupPath(request);
    this.mappingRegistry.acquireReadLock();
    try {
        //根据url和请求获取HandlerMethod
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
    finally {
        this.mappingRegistry.releaseReadLock();
    }
}
  1. AbstractHandlerMethodMapping#lookupHandlerMethod

    这一段就是比较核心的代码了,这里会根据url先进行查找,然后再根据相关条件进行筛选

java 复制代码
@Nullable
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<>();
    //根据url获取到所有的RequestMappingInfo
    //底层维护了一个pathLookup,维护了url和List<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 {
        //返回对应的bean和方法信息
        return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
    }
}
  1. AbstractHandlerMethodMapping#addMatchingMappings
java 复制代码
private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
    for (T mapping : mappings) {
        //条件筛选
        T match = getMatchingMapping(mapping, request);
        if (match != null) {
            //mappingRegistry将RequestMappingInfo作为key,bean和方法信息作为value,维护了一个map
            matches.add(new Match(match, this.mappingRegistry.getRegistrations().get(mapping)));
        }
    }
}

4. 总结

在实际项目过程中,我们使用到了很多类似这种注解,但是都只是停留在了用的层面,作为一个搞技术的,我们其实需要有一些技术热情,可能在做项目的时候没时间,但是空闲时间来看看原理,也是很有收获的。

相关推荐
IT_陈寒1 小时前
被Java的final坑惨了,这些细节你可能也忽略了
前端·人工智能·后端
Zadig2 小时前
企业 Agent 总烂在 Demo 里?Zadig 工作流 AI 任务给了一条路
后端·devops
Zadig2 小时前
告别"人肉扛雷":Zadig 用 AI 接管发布前最脏最累的 15 分钟
后端·aiops
Nturmoils2 小时前
向量数据库不该成为新孤岛:KingbaseES 多模融合架构如何减少数据搬运
后端
一座古城2 小时前
Claude Code 架构源码解析:AI 应用开发的范式跃迁
前端·后端
Nturmoils2 小时前
用蓝耘元生代做 GitHub 热榜解读:Dify Chatflow 接入和真实项目分析
后端
K哥爬虫2 小时前
【JS 逆向百例】Vaptcha V4 手势验证码逆向分析
前端·后端
qq_185198692 小时前
SpringBoot-五-AOT
java·spring boot·后端
用户29975997127073 小时前
Service 层到底该 throw 异常还是 return 错误码?我在两个团队各推行过一种,最后被一个事务 Bug 说服了
后端
小月土星3 小时前
吞噬星空 RAG 项目 MySQL 实战:从硬编码登录到数据库鉴权(Day 1)
后端