【小家Spring】Spring MVC容器的web九大组件之---HandlerMapping源码详解(一)---BeanNameUrlHandlerMapping系列(下)

简介: 【小家Spring】Spring MVC容器的web九大组件之---HandlerMapping源码详解(一)---BeanNameUrlHandlerMapping系列(下)

MatchableHandlerMapping


这是HandlerMapping的另外一个分支,这是它的一个子接口。


// @since 4.3.1 出现得挺晚的接口
public interface MatchableHandlerMapping extends HandlerMapping {
  // 确定给定的请求是否符合请求条件  pattern:模版
  @Nullable
  RequestMatchResult match(HttpServletRequest request, String pattern);
}


目前有两个类实现了此方法RequestMappingHandlerMapping和AbstractUrlHandlerMapping,但是Spring内部并还没有调用过此接口方法,因此此处暂时略过此部分~


接下来最重要的就是以AbstractHandlerMapping为主线,看看他的真正实现类们了。它主要分为两大主线:

AbstractUrlHandlerMapping方向和AbstractHandlerMethodMapping


AbstractHandlerMethodMapping系列是当前使用得最多的,基于方法的Handler模式,文章二会着重继续分析


AbstractUrlHandlerMapping系列


从命名中也能看出来,它和URL有关。它的大致思路为:将url对应的Handler保存在一个Map中,在getHandlerInternal方法中使用url从Map中获取Handler


// 虽然附加实现了MatchableHandlerMapping ,但本文并不准备详细分析
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping implements MatchableHandlerMapping {
  // 根路径 / 的处理器~
  @Nullable
  private Object rootHandler;
  // 是否使用斜线/匹配   如果为true  那么`/users`它也会匹配上`/users/`  默认是false的
  private boolean useTrailingSlashMatch = false;
  // 设置是否延迟初始化handler。仅适用于单实例handler   默认是false表示立即实例化
  private boolean lazyInitHandlers = false;
  // 这个Map就是缓存下,URL对应的Handler(注意这里只是handler,而不是chain)
  private final Map<String, Object> handlerMap = new LinkedHashMap<>();
  ...
  // 这个就是父类留给子类实现的抽象方法,此抽象类相当于进行了进一步的模版实现~
  @Override
  @Nullable
  protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
    // 找到URL的后半段:如`/api/v1/hello`  由此可见Spring MVC处理URL路径匹配都是从工程名后面开始匹配的~~~~
    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
    // 根据url查找handler 
    // 1、先去handlerMap里找,若找到了那就实例化它,并且并且给chain里加入一个拦截器:`PathExposingHandlerInterceptor`  它是个private私有类的HandlerInterceptor
    // 该拦截器的作用:request.setAttribute()请求域里面放置四个属性 key见HandlerMapping的常量们~~~
    // 2、否则就使用PathMatcher去匹配URL,这里面光匹配其实是比较简单的。但是这里面还解决了一个问题:那就是匹配上多个路径的问题
    // 因此:若匹配上多个路径了,就按照PathMatcher的排序规则排序,取值get(0)~~~最后就是同上,加上那个HandlerInterceptor即可
    // 需要注意的是:若存在uriTemplateVariables,也就是路径里都存在多个最佳的匹配的情况  比如/book/{id}和/book/{name}这两种。
    // 还有就是URI完全一样,但是一个是get方法,一个是post方法之类的  那就再加一个拦截器`UriTemplateVariablesHandlerInterceptor`  它request.setAttribute()了一个属性:key为 xxx.uriTemplateVariables
    // 这些Attribute后续都是有用滴~~~~~~ 请注意:这里默认的两个拦截器每次都是new出来的和Handler可议说是绑定的,所以不会存在线程安全问题~~~~
    Object handler = lookupHandler(lookupPath, request);
    // 若没找到:
    if (handler == null) {
      // 处理跟路径 / 和默认的Handler~~~~
      Object rawHandler = null;
      if ("/".equals(lookupPath)) {
        rawHandler = getRootHandler();
      }
      if (rawHandler == null) {
        rawHandler = getDefaultHandler();
      }
      if (rawHandler != null) {
        if (rawHandler instanceof String) {
          String handlerName = (String) rawHandler;
          rawHandler = obtainApplicationContext().getBean(handlerName);
        }
        validateHandler(rawHandler, request);
        // 就是注册上面说的默认的两个拦截器~~~~~~~  第四个参数为null,就只会注册一个拦截器~~~
        // 然后把rawHandler转换成chain(这个时候chain里面可能已经有两个拦截器了,然后父类还会继续把用户自定义的拦截器放上去~~~~)
        handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
      }
    }
    return handler;
  }
  // =========该抽象类提供的这个方法就特别重要了:向handlerMap里面put值的唯一入口~~~  可以批量urls
  protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
    Assert.notNull(urlPaths, "URL path array must not be null");
    for (String urlPath : urlPaths) {
      registerHandler(urlPath, beanName);
    }
  }
  protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
    Assert.notNull(urlPath, "URL path must not be null");
    Assert.notNull(handler, "Handler object must not be null");
    Object resolvedHandler = handler;
    // 如果是beanName,并且它是立马加载的~~~~
    if (!this.lazyInitHandlers && handler instanceof String) {
      String handlerName = (String) handler;
      ApplicationContext applicationContext = obtainApplicationContext();
      // 并且还需要是单例的,那就立马实例化吧~~~~
      if (applicationContext.isSingleton(handlerName)) {
        resolvedHandler = applicationContext.getBean(handlerName);
      }
    }
    // 先尝试从Map中去获取
    Object mappedHandler = this.handlerMap.get(urlPath);
    if (mappedHandler != null) {
      // 这个异常错误信息,相信我们在开发中经常碰到吧:简单就是说就是一个URL只能映射到一个Handler上(但是一个Handler是可以处理多个URL的,这个需要注意)
      // 这个校验必不可少啊~~~~
      if (mappedHandler != resolvedHandler) {
        throw new IllegalStateException("Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
            "]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
      }
    } else {
      // 如果你的handler处理的路径是根路径,那太好了  你的这个处理器就很特殊啊~~~~
      if (urlPath.equals("/")) {
        setRootHandler(resolvedHandler);
      }
      // 这个路径相当于处理所有  优先级是最低的  所以当作默认的处理器来使用~~~~
      else if (urlPath.equals("/*")) {
        setDefaultHandler(resolvedHandler);
      }
      // 正常的路径了~~~
      // 注意此处:好像是Spring5之后 把这句Mapped的日志级别   直接降低到trace级别了,简直太低了有木有~~~
      // 在Spring 5之前,这里的日志级别包括上面的setRoot等是info(所以我们在控制台经常能看见大片的'Mapped URL path'日志~~~~)
      // 所以:自Spring5之后不再会看controller这样的映射的日志了(除非你日志界别调低~~~)可能Spring认为这种日志多,且不认为是重要的信息吧~~~
      else {
        this.handlerMap.put(urlPath, resolvedHandler);
        if (logger.isTraceEnabled()) {
          logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
        }
      }
    }
  }
  // 该缓存也提供了一个只读视图给调用者访问~~~
  public final Map<String, Object> getHandlerMap() {
    return Collections.unmodifiableMap(this.handlerMap);
  }
}


该抽象类提供了一个Map,缓存着了URL和它对应的Handler,这是个非常重要的缓存。它提供了registerHandler()允许子类调用,向缓存里注册url和handler的对应关系~


注意:此处肯定不能把Map放出去,让子类直接put的。因为程序必须要高内聚,才能保证更好的隔离性以及稳定性


AbstractDetectingUrlHandlerMapping



这又是个抽象类,继承自AbstractUrlHandlerMapping。它就越来越具有功能化了:Detecting表明它是有检测URL的功能的~

// @since 2.5  它Spring2.5后才出来
public abstract class AbstractDetectingUrlHandlerMapping extends AbstractUrlHandlerMapping {
  // 是否要去祖先容器里面检测所有的Handlers    默认是false表示只在自己的容器里面找
  // 若设置为true,相当于在父容器里的Controller也会被挖出来~~~~ 一般我并不建议这么去做
  private boolean detectHandlersInAncestorContexts = false;
  public void setDetectHandlersInAncestorContexts(boolean detectHandlersInAncestorContexts) {
    this.detectHandlersInAncestorContexts = detectHandlersInAncestorContexts;
  }
  // 说白了,这里是检测的入口 detectHandlers();
  @Override
  public void initApplicationContext() throws ApplicationContextException {
    super.initApplicationContext();
    detectHandlers();
  }
  protected void detectHandlers() throws BeansException {
    // 这个就不解释了:默认只会在当前容器里面去查找检测~~~
    // 注意:这里使用的Object.class  说明是把本容器内所有类型的Bean定义都拿出来了~~~~
    String[] beanNames = (this.detectHandlersInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
        getApplicationContext().getBeanNamesForType(Object.class));
    // Take any bean name that we can determine URLs for.
    for (String beanName : beanNames) {
      // 这是个抽象方法由子类去实现。  它的作用就是看看url和bean怎么才算是匹配呢?也就是说这个handler到底能够处理哪些URL呢?
      // 注意:此处还是类级别(Bean),相当于一个类就是一个Handler哦~~~~
      String[] urls = determineUrlsForHandler(beanName);
      if (!ObjectUtils.isEmpty(urls)) {
        // 注册进去  缓存起来~
        registerHandler(urls, beanName);
      }
    }
  }
}

AbstractDetectingUrlHandlerMapping是通过扫描方式注册Handler,收到请求时由AbstractUrlHandlerMapping的getHandlerInternal进行分发看看到底是交给哪个Handler进行处理~


最后真的是得看它实现类的时候了


BeanNameUrlHandlerMapping


它是AbstractDetectingUrlHandlerMapping的唯一实现类


说明:DefaultAnnotationHandlerMapping、BeanNameUrlHandlerMapping、AbstractControllerUrlHandlerMapping在Spring4.3的时候都被标记为过期,在Spring5以后直接就把这些类干掉了,因此本处说的唯一、源码都是基于Spring5.以上的版本的~~~


public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
  @Override
  protected String[] determineUrlsForHandler(String beanName) {
    List<String> urls = new ArrayList<>();
    // 意思就是必须以/开头才行~~~~~~这算是一种约定吧~~~
    // 这种方式和@WebServlet方式一毛一样~~~~~
    if (beanName.startsWith("/")) {
      urls.add(beanName);
    }
    // 当然别名也是可以的
    String[] aliases = obtainApplicationContext().getAliases(beanName);
    for (String alias : aliases) {
      if (alias.startsWith("/")) {
        urls.add(alias);
      }
    }
    return StringUtils.toStringArray(urls);
  }
}


该实现有点那啥,就是根据bean的名称来匹配URL。方式同@WebServlet一毛一样


SimpleUrlHandlerMapping


它是AbstractUrlHandlerMapping的直接实现类,也是一个基于Map的简单实现。


public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
  private final Map<String, Object> urlMap = new LinkedHashMap<>();
  public void setMappings(Properties mappings) {
    CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
  }
  public void setUrlMap(Map<String, ?> urlMap) {
    this.urlMap.putAll(urlMap);
  }
  @Override
  public void initApplicationContext() throws BeansException {
    super.initApplicationContext();
    registerHandlers(this.urlMap);
  }
  // 这个实现简单到令人发指
  protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
    if (urlMap.isEmpty()) {
      logger.trace("No patterns in " + formatMappingName());
    } else {
      urlMap.forEach((url, handler) -> {
        // 如果还没有斜线,在前面加上斜线
        if (!url.startsWith("/")) {
          url = "/" + url;
        }
        if (handler instanceof String) {
          handler = ((String) handler).trim();
        }
        registerHandler(url, handler);
      });
    }
  }
}


它的实现就是吧开发者指定的一个Map,然后容器启动的时候把它注册进去即可,非常的简单的一个实现。当然我们自己已经知道了URL和Handler的映射关系了,然后需要进一步构造出一个HandlerMapping的时候,或许它是一个较快解决问题的选择~~~~ 它最重要的是urlMap这个参数~


它一般用于基于XML的配置文件的 形式,形如:


<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
  <property>
    <map>
      <entry key="/hello.do" value="myController"/>
      <entry key="/my.do" value="myController"/>
    </map>
  </property>
</bean>


Demo


写一个控制器:


@Controller("/hello") // 注意此处BeanName必须是/开头,否则是不会作为handler的
public class HelloController { }


这样项目启动的时候,就可以看到这么一句日志,证明url是映射成功了的~:


org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping.registerHandler Mapped URL path [/hello] onto handler '/hello'


但是请求后报出如下错误(http://localhost:8080/demo_war_war/hello):


No adapter for handler [com.fsx.controller.HelloController@5e31e1a4]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler


找不到适配器,这其实是下一节讲解HandlerAdapter的内容,此处介绍怎么做:


// 实现`org.springframework.web.servlet.mvc.Controller`这个接口,才被认为是一个控制器~
@Controller("/hello")
public class HelloController extends AbstractController {
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("this is my demo");
        return null;
    }
}



这样子我们再请求,就能正常的进入handleRequestInternal方法来处理请求了。


我们的Controller类必须(直接|间接)实现org.springframework.web.servlet.mvc.Controller接口,否则无法判断具体处理方法是谁!!!


总结


本篇介绍的HandlerMapping,除了介绍它的抽象实现外。就是介绍了AbstractUrlHandlerMapping系列。

它主要实现是BeanNameUrlHandlerMapping和SimpleUrlHandlerMapping,属于Spring最早期的控制器实现。完全是基于类级别的:一个类就是一个Handler,模式和源生的Servlet没太大差异(还是和Servlet源生API有耦合的~)。


显然这种模式在现在全注解时代已经完全过时了,开发、运行效率都太低下了。但是了解了这些过去的技术,才会发现这些都是一脉相承的,了解背后作者的设计意图、走向才是最为重要的目的


从分析DispatcherServlet的时候发现,SpringMVC默认是给容器内注入了两个HandlerMapping组件的:RequestMappingHandlerMapping和BeanNameUrlHandlerMapping由此可见Spring还是保持了充分的向下兼容的~

相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
6月前
|
前端开发 Java 微服务
《深入理解Spring》:Spring、Spring MVC与Spring Boot的深度解析
Spring Framework是Java生态的基石,提供IoC、AOP等核心功能;Spring MVC基于其构建,实现Web层MVC架构;Spring Boot则通过自动配置和内嵌服务器,极大简化了开发与部署。三者层层演进,Spring Boot并非替代,而是对前者的高效封装与增强,适用于微服务与快速开发,而深入理解Spring Framework有助于更好驾驭整体技术栈。
|
8月前
|
运维 数据可视化 C++
2025 热门的 Web 化容器部署工具对比:Portainer VS Websoft9
2025年热门Web化容器部署工具对比:Portainer与Websoft9。Portainer以轻量可视化管理见长,适合技术团队运维;Websoft9则提供一站式应用部署与容器管理,内置丰富开源模板,降低中小企业部署门槛。两者各有优势,助力企业提升容器化效率。
530 1
2025 热门的 Web 化容器部署工具对比:Portainer VS Websoft9
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
1416 0
|
前端开发 Java 物联网
智慧班牌源码,采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署
智慧班牌系统是一款基于信息化与物联网技术的校园管理工具,集成电子屏显示、人脸识别及数据交互功能,实现班级信息展示、智能考勤与家校互通。系统采用Java + Spring Boot后端框架,搭配Vue2前端技术,支持SaaS云部署与私有化定制。核心功能涵盖信息发布、考勤管理、教务处理及数据分析,助力校园文化建设与教学优化。其综合性和可扩展性有效打破数据孤岛,提升交互体验并降低管理成本,适用于日常教学、考试管理和应急场景,为智慧校园建设提供全面解决方案。
662 70
|
8月前
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
1032 0
|
9月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
660 0
|
9月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
281 0
|
9月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
374 0
|
6月前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
556 4
下一篇
开通oss服务