【老王读SpringMVC】url 与 controller method 的映射关系注册

时间:2023-02-10 11:03:47

上文提到,如果我们自己要实现 spring mvc 框架的话,大致需要实现如下功能:

  • 0、将 url 与 Controller method 的对应关系进行注册
  • 1、通过请求的 url 找到 Controller method (即 url 与 Controller method 的映射)
  • 2、将请求参数进行绑定,即将入参绑定到 Controller method 的参数对象上
  • 3、执行处 Controller method (即 HandlerAdapter#handle())
  • 4、对 Controller method 的返回值进行处理
    4.1 如果正常返回的话,对返回值对象进行处理(即 ReturnValueHandler)
    包括:如果返回视图 View 的话,对视图进行渲染 (即 ViewResolver)
    4.2 如果有异常返回的话,对异常进行处理(即 @ExceptionHandler)

下面我们就来研究一下,Spring MVC是如何将 url 与 controller method 的映射关系找出来进行注册的?

分析 url 与 handler method 的映射关系的注册

经过前面对 DispatcherServlet#doDispatch() 的分析,我们知道断点应该打在获取 HandlerExecutionChain 的地方。

/**
 * 将所有的 HandlerMapping 按顺序遍历一次,获取 request 对应的 HandlerExecutionChain。
 */
@Nullable
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;
}

可以看到,在获取 HandlerExecutionChain 时,Spring 会将所有的 HandlerMapping 按顺序遍历一次。
而在 Spring 中有许多 HandlerMapping,最常用的当属 RequestMappingHandlerMapping

org.springframework.web.servlet.HandlerMapping

HandlerMapping 是用来定义 request 和处理程序(handler)之间的映射关系的。

Spring 内置了两个常用的实现: BeanNameUrlHandlerMappingRequestMappingHandlerMapping
用户可以编写自定义的 HandlerMapping,并通过实现 org.springframework.core.Ordered 来指定优先级。

下面我们来看下 HandlerMapping 的类图:

【老王读SpringMVC】url 与 controller method 的映射关系注册

SpringBoot 默认注册的 HandlerMapping 有:

  • RequestMappingHandlerMapping -- 处理 @RequestMapping 注解的 url 与处理程序的映射 (最常用)
  • BeanNameUrlHandlerMapping -- 将 / 开头的 beanName 与 url 进行映射
  • RouterFunctionMapping
  • SimpleUrlHandlerMapping -- 处理普通的 url
  • WelcomePageHandlerMapping -- 处理首页

RequestMappingHandlerMapping 注册映射关系

在 SpringMVC 中,我们最常用的定义端点接口的方式是使用 @RequestMapping
所以,我们主要来研究一下注解形式的 url 是如何进行映射关系注册的?

在 SpringMVC 中,RequestMappingHandlerMapping 是用来支持 @RequestMapping 注解形式的 url 的。
映射关系的注册是在它的父类 AbstractHandlerMethodMapping 中完成的:

【老王读SpringMVC】url 与 controller method 的映射关系注册

可以看到,当 RequestMappingHandlerMapping 这个 bean 在加载的时候,会调用父类的 AbstractHandlerMethodMapping#afterPropertiesSet() 来完成 url 与 handler method 映射关系的注册。
具体的注册是由 AbstractHandlerMethodMapping.MappingRegistry#registry() 来完成的。

判断哪些类是 handler:RequestMappingHandlerMapping#isHandler()

/**
 * bean class 上如果有 @Controller 或 @RequestMapping 的话,就认为是一个 handler 处理程序  
 */
protected boolean isHandler(Class<?> beanType) {
    return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
            AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}

具体的 register 注册逻辑

request 和 handler method 映射关系的注册是由 AbstractHandlerMethodMapping.MappingRegistry 来实现的。

MappingRegistry 的定义如下:

class MappingRegistry {
    
    // 保存所有的 <RequestMappingInfo, MappingRegistration>  
    private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
    
    // 保存 RequestMappingInfo 中拥有 directPath 的: <directPath, RequestMappingInfo>
    private final MultiValueMap<String, T> pathLookup = new LinkedMultiValueMap<>();
    
    // 保存所有的 HandlerMethod name: <name, List<HandlerMethod>>
    private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
    
    // 保存拥有 cors 配置的 HandlerMethod: <HandlerMethod, CorsConfiguration>
    private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
    .....
}

AbstractHandlerMethodMapping.MappingRegistry#register() 是负责具体的注册逻辑的:
【老王读SpringMVC】url 与 controller method 的映射关系注册

可以看到,这里注册了 4 种信息:
1、将 directPath 注册到 pathLookup 中
2、将 HandlerMethod 的 name 注册到 nameLookup 中
3、将 HandlerMethod 注册到 corsLookup 中(处理跨域请求映射)
4、将所有的 RequestMappingInfo 全部都注册到 registry 中

directPath: 指那些没有特殊字符的 path。特殊字符:?、*、{}

补充:SpringBoot 中 RequestMappingHandlerMapping bean 是在哪里定义的?
WebMvcAutoConfiguration.EnableWebMvcConfiguration#requestMappingHandlerMapping() 中定义的:

// org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#requestMappingHandlerMapping
@Bean
@Primary
public RequestMappingHandlerMapping requestMappingHandlerMapping(
        @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
        @Qualifier("mvcConversionService") FormattingConversionService conversionService,
        @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {
    // Must be @Primary for MvcUriComponentsBuilder to work
    return super.requestMappingHandlerMapping(contentNegotiationManager, conversionService,
            resourceUrlProvider);
}

找到 bean 定义处的技巧:
断点打在 bean class 的构造函数或初始化方法里面,当断点进入时,可以很方便的从调用堆栈中找到相应的 BeanDefinition 的值,BeanDefinition 中就记录了这个 bean 是从在哪里定义的。
如果断点打不到 bean class 里面的话,那么就可以在 applicationContext 中获取相应的 BeanDefinition,再查看 bean 定义的地方。
核心就是要找到 bean 对应的 BeanDefinition。

小结

在 SpringMVC 中,request 与 handler method 的请求关系注册和映射都是通过 HandlerMapping 来完成的。
HandlerMapping 的实现类中最常用的是 RequestMappingHandlerMapping,它是用来处理 @RequestMapping 注解形式的请求关系映射的。
映射关系的注册是在它的父类 AbstractHandlerMethodMapping#registerHandlerMethod() 中完成的。
这里注册了 4 种信息:
1、将 directPath 注册到 pathLookup 中
2、将 HandlerMethod 的 name 注册到 nameLookup 中
3、将 HandlerMethod 注册到 corsLookup 中(处理跨域请求映射)
4、将所有的 RequestMappingInfo 全部都注册到 registry 中