SpringMVC之三:HandlerMapping

我们知道Spring通常以bean的形式来组织功能模块,Spring MVC也不列外。Spring MVC以一系列特定类型的bean来构建整个框架。

相关Bean类型

Bean类型 说明
HandlerMapping 实现了url到处理器的映射关系,包括与之关联的拦截器(interceptors);有两个主要实现类,RequestMappingHandlerMapping支持@RequestMapping注解标注的处理器,SimpleUrlHandlerMapping支持手动添加映射。
HandlerAdapter 封装了调用处理器的过程;这个类的存在的主要目的是将DispatcherServlet与处理器调用细节隔离开;比如RequestMappingHandlerAdapter与RequestMappingHandlerMapping对应
HandlerExceptionResolver 处理http请求过程中发生的异常
ViewResolver 将处理器返回的view名字,解析为最终的View对象来渲染对请求的响应
LocaleResolver, LocaleContextResolver 解析用户的Locale,http请求的后续处理环节可使用
ThemeResolver 用户主题解析,支持用户个性化的页面布局、css等
MultipartResolver 如果http请求时一个multi-part类型,解析各个part的内容,处理器方法中可以直接使用
FlashMapManager 管理Flash属性,可以在request之间传递,通常用于url重定向

这些bean工作在DispatcherServlet背后,构建起http请求处理的完整流程。我们可以在Spring容器中定义对应类型的bean实例,DispatcherServlet会自动查找它们;如果没有找到,就会使用DispatcherServlet.properties里配置的类型来创建。

http请求处理流程

简要介绍一下DispatcherServlet使用这些bean处理http请求的大体流程:

  1. 将WebApplicationContext绑定为request上的一个attribute,key为DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE;
  2. LocaleResolver绑定到request,使后续处理过程可以随时访问用户的Locale;
  3. ThemeResolver绑定到request;
  4. MultipartResolver检查request的请求体是不是multipart,如果是,将request包装为MultipartHttpServletRequest;
  5. 轮询所有的HandlerMapping,查找对应的handler;
  6. HandlerAdapter执行该handler,包括拦截器,handler方法等;
  7. 依据handler返回结果,生成Http响应;
  8. 如果发生异常,由HandlerExceptionResolver来处理;

HandlerMapping

这个系列的文章会陆续介绍上面大部分bean(不过我们没有必要了解上面所有bean的工作细节,只要知道他们的存在,在需要时再研究即可)。这一章单独介绍一下HandlerMapping,虽然我们几乎不太可能直接使用它,但它对我们理解Spring MVC的工作方式十分重要。

下面介绍HandlerMapping以及与之直接关联的概念和Spring MVC类型。

handler

handler这个概念在Spring MVC里面指http请求对应的处理逻辑,它负责处理请求并生成http响应。Spring MVC默认支持三种handler:

  • 一是@Controller里用@RequestMapping标注的方法
  • 二是HttpRequestHandler接口的实现
  • 三是org.springframework.web.servlet.mvc.Controller接口的实现

HandlerAdapter

上面之所以说有三种类型的handler,实际上是因为Spring MVC内置三种类型的HandlerAdapter,分别是RequestMappingHandlerAdapter,HttpRequestHandlerAdapter,和SimpleControllerHandlerAdapter。他们封装了如何执行对应类型handler的细节。

我们可以支持新类型的handler,只要同时添加配套的HandlerAdapter就行。

HandlerExecutionChain

HandlerExecutionChain包裹了http请求对应handler和相关的拦截器(HandlerInterceptor)。在一次http请求的处理流程中,DispatchServlet先从HandlerMapping中映射出一个HandlerExecutionChain实例,再通过HandlerAdapter来执行它。

HandlerMapping

现在可以看看HandlerMapping这个接口的定义了:

public interface HandlerMapping {
   HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

只要HandlerMapping能返回一个有效HandlerExecutionChain,就表明映射成功。DispatchServlet内部有一个HandlerMapping列表,对每个http请求,轮询这个列表,得到有效的HandlerExecutionChain为止。

因为拦截器是可选的,所以只要有handler,就有HandlerExecutionChain。如果查找不到任何handler,默认返回一个HTTP 404给客户端。

探查HandlerMapping实例

由于HandlerMapping是Spring bean,我们可以扫描容器来探查一下应用中HandlerMapping的状态,示例代码如下:

@RestController
public class HelloController implements Controller {

    @Autowired
    private ApplicationContext applicationContext;
    
    @RequestMapping("/test")
    public String test(String name) {
        return "test/"+name;
    }

    @RequestMapping("/handlerMapping")
    public String handlerMapping() {
        StringBuilder sb = new StringBuilder();
        Map<String, HandlerMapping> handlerMapping = applicationContext.getBeansOfType(HandlerMapping.class);
        List<HandlerMapping> mappingList = new ArrayList<>(handlerMapping.values());
        AnnotationAwareOrderComparator.sort(mappingList);
        mappingList.forEach(mapping -> {
            if (mapping instanceof SimpleUrlHandlerMapping) {
                sb.append("SimpleUrlHandlerMapping:<br/>");
                ((SimpleUrlHandlerMapping) mapping).getUrlMap().forEach((k, v) -> {
                    sb.append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;").append(k).append("====>").append(v).append("<br/>");
                });
            } else if (mapping instanceof RequestMappingHandlerMapping) {
                sb.append("RequestMappingHandlerMapping:<br/>");
                ((RequestMappingHandlerMapping) mapping).getHandlerMethods().forEach((k, v) -> {
                    sb.append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;").append(k).append("====>").append(v).append("<br/>");
                });
            } else {
                sb.append("UnknownMapping:<br/>").append("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;").append(mapping).append("<br/>");
            }
        });
        return sb.toString();
    }

这段代码将HandlerMapping这个类型的所有bean找出来,通过AnnotationAwareOrderComparator.sort按优先级进行排序,然后依次查看:对于SimpleUrlHandlerMapping,打印它的url和handler;对于RequestMappingHandlerMapping,打印url和对应的方法信息;对于其他类型,标志为UnknownMapping。

我用的Spring Boot 2.1.3来运行这个示例,打印的结果大致如下:

SimpleUrlHandlerMapping:
        **/favicon.ico====>ResourceHttpRequestHandler [class path resource [META-INF/resources/], class path resource [resources/], class path resource [static/], class path resource [public/], ServletContext resource [/], class path resource []]
RequestMappingHandlerMapping:
        { /test}====>public java.lang.String controller.HelloController.test()
        { /handlerMapping}====>public java.lang.String controller.HelloController.handlerMapping()
        { /error}====>public org.springframework.http.ResponseEntity> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
        { /error, produces [text/html]}====>public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
UnknownMapping:
        org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping@d53029f
UnknownMapping:
        org.springframework.boot.autoconfigure.web.servlet.WelcomePageHandlerMapping@11ba16b0
SimpleUrlHandlerMapping:
        /webjars/**====>ResourceHttpRequestHandler ["classpath:/META-INF/resources/webjars/"]
        /**====>ResourceHttpRequestHandler ["classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/", "/"]

打印结果里面包含的信息还是挺多的,我们来分析一番:

  1. 第一个SimpleUrlHandlerMapping,将**/favicon.ico这个url映射给了一个叫做ResourceHttpRequestHandler的处理器,从名字看,这是一个读取静态资源的handler,后面打印的是资源位置;
  2. 第二个是RequestMappingHandlerMapping,不出意外,我们在Controller里面通过注解@RequestMapping定义的两个映射/test/handlerMapping都出现在这里;Spring Boot默认还定义了/error的映射;
  3. 第三个是BeanNameUrlHandlerMapping类型的映射,它直接定义url到bean name的映射,待会我们再研究它;
  4. 接着又是一个静态资源的SimpleUrlHandlerMapping,可以让我们直接访问Spring Boot应用classpath/resource,/static, /public,/META-INF/resources/下的静态资源

我们至少可以的得出两个结论:

  1. 所有通过@RequestMapping声明的映射关系,都会被一个RequestMappingHandlerMapping类型的bean所封装;
  2. 基于Spring Boot的web应用之所以能直接通过url访问某些特定路径下的静态资源,是因为它预置了一个SimpleUrlHandlerMapping,映射了URI/**

注意:上面这些HandlerMapping是Spring Boot的自动配置机制创建的,如果你在工程中做了自定义配置,那么结果可能完全不一样。

SimpleUrlHandlerMapping

SimpleUrlHandlerMapping可以让我们手动添加url到handler的映射关系:

@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
    SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
    mapping.setUrlMap(Collections.singletonMap("/simple","helloController"));
    mapping.setOrder(Integer.MIN_VALUE);
    return mapping;
}

上面引用的helloController这个bean必须是Spring MVC支持的handler类型。

如果没有这句代码mapping.setOrder(Integer.MIN_VALUE);,访问http://localhost:8080/simple会报404错误,这是因为HandlerMapping是按优先级排列的。我们自定义的SimpleUrlHandlerMapping默认Order是0,请求被系统内置的那个处理静态资源的SimpleUrlHandlerMapping拦截了。

BeanNameUrlHandlerMapping

这个mapping动态扫描ApplicationContext下面,有没有和URL路径同名的bean:

@Component("/bean")
public class BeanHandler implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.getWriter().write("BeanHandler response");
        return null;
    }
}

本地运行这个应用,在浏览器输入http://localhost:8080/bean即可访问它。

本章完整示例代码见示例工程的handlerMapping模块。

总结

在实现层面,Spring MVC由一系列特定的bean类型来构成;DispatchServlet调用这些bean来完成http请求的处理。我们可以通过定义这些类型的bean来配置Spring MVC,不过更好的方式是使用WebMvcConfigurer接口。

在这些bean类型中,个人认为HandlerMapping是最重要的,尽管实际项目中我们几乎不直接使用它。HandlerMapping封装了URL到处理器之间的映射关系,通过对一个Sping Boot Web工程的HandlerMapping实例进行分析,我们彻底搞明白了为什么放在static、public这些路径下的静态资源能直接通过url访问到。

多个HandlerMapping之间可能会发生url映射的竞争,从而导致预料之外的404错误;有了本章的知识,对此类问题,我们应该有了明确的定位思路。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章