红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

Spring 源码学习 14:路径映射

2025年7月5日 8点热度 0人点赞 0条评论

默认的 Spring 框架使用RequestMappingHandlerMapping进行请求路径映射,它会根据@RequestMapping注解进行路径映射,此外,Spring 框架还提供一些其它的路径映射方式。

BeanNameUrlHandlerMapping

使用 BeanNameUrlHandlerMapping 可以用 Bean 名称处理路径映射,在配置类中添加:

/**
 * 处理器映射器,根据 bean 名称进行路径映射
 * @return
 */
@Bean
public BeanNameUrlHandlerMapping beanNameUrlHandlerMapping(){
    return new BeanNameUrlHandlerMapping();
}

要搭配一个SimpleControllerHandlerAdapter使用:

/**
 * 处理器适配器,可以调用实现了 Controller 接口的控制器
 * @return
 */
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter(){
    return new SimpleControllerHandlerAdapter();
}

这个处理器适配器可以调用实现了Controller接口的控制器。

因为在用 Bean 名称映射 url 请求的情况下,一个控制器 Bean 只能处理一个路径,换言之只应当有一个处理器方法,Controller 接口就是定义此种情况的控制器类。

添加控制器类定义:

@Component("/hello")
static class HelloController implements Controller{
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        response.getWriter().print("Hello World");
        return null;
    }
}

控制器类实现了Controller接口,并且用 Bean 名称(/hello)设定了可以处理的请求路径。

注意,此类 Bean 名称以/开头。

同样,也可以用 Bean 方法添加此类控制器 Bean:

@Bean("/bye")
public Controller byeController(){
    return new Controller() {
        @Override
        public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
            response.getWriter().print("bye");
            return null;
        }
    };
}

自定义 HandlerMapping

可以自己实现BeanNameUrlHandlerMapping:

@Component
@Slf4j
static class MyHandlerMapping implements HandlerMapping {
    @Autowired
    private ApplicationContext applicationContext;
    private Map<String, Controller> urlControllerMap = new HashMap<>();
​
    @PostConstruct
    public void postConstruct() {
        // 取容器中所有的路径名称控制器 bean,生成路径-控制器映射
        // 获取所有实现了 Controller 接口的 bean
        Map<String, Controller> controllerMap = applicationContext.getBeansOfType(Controller.class);
        if (controllerMap.isEmpty()) {
            return;
        }
        // 过滤掉名称不以 / 开头的 bean
        urlControllerMap = controllerMap.entrySet().stream()
                .filter(e -> e.getKey().startsWith("/"))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
        log.debug("收集到的 url-controller 映射:");
        urlControllerMap.forEach((k, v) -> {
            log.debug("{}->{}", k, v);
        });
    }
​
    @Override
    public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        String url = request.getRequestURI();
        Controller controller = urlControllerMap.get(url);
        log.debug("路径{}匹配到{}", url, controller);
        if (controller == null) {
            return null;
        }
        return new HandlerExecutionChain(controller);
    }
}

同样的,可以用自定义HandlerAdapter取代SimpleControllerHandlerAdapter:

@Component
static class MyHandlerAdapter implements HandlerAdapter {
​
    /**
     * 是否支持控制器方法调用
     *
     * @param handler the handler object to check
     * @return
     */
    @Override
    public boolean supports(Object handler) {
        // 只能调用实现了 Controller 接口的控制器 bean
        return handler instanceof Controller;
    }
​
    /**
     * 调用控制器方法
     *
     * @param request  current HTTP request
     * @param response current HTTP response
     * @param handler  the handler to use. This object must have previously been passed
     *                 to the {@code supports} method of this interface, which must have
     *                 returned {@code true}.
     * @return
     * @throws Exception
     */
    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof Controller controller) {
            return controller.handleRequest(request, response);
        }
        return null;
    }
​
    @Override
    public long getLastModified(HttpServletRequest request, Object handler) {
        return -1;
    }
}

HandlerAdapter接口的getLastModified方法已经被废弃,简单返回-1即可。

完整示例可以看这里。

RouterFunctionMapping

RouterFunctionMapping使用RequestPredicate作为路径映射依据,将路径映射到HandlerFunction上。

在配置类在中添加:

@Bean
public RouterFunctionMapping routerFunctionMapping(){
    return new RouterFunctionMapping();
}

这个映射器对应的适配器是HandlerFunctionAdapter:

@Bean
public HandlerFunctionAdapter handlerFunctionAdapter(){
    return new HandlerFunctionAdapter();
}

路径映射规则和控制器方法封装在RouterFunction类型中:

@Bean
public RouterFunction<ServerResponse> helloRouterFunction(){
    return RouterFunctions.route(RequestPredicates.GET("/hello"), new HandlerFunction<ServerResponse>() {
        @Override
        public ServerResponse handle(ServerRequest request) throws Exception {
            return ServerResponse.ok().body("Hello World");
        }
    });
}

@Bean
public RouterFunction<ServerResponse> byeRouterFunction(){
    return RouterFunctions.route(RequestPredicates.GET("/bye"), new HandlerFunction<ServerResponse>() {
        @Override
        public ServerResponse handle(ServerRequest request) throws Exception {
            return ServerResponse.ok().body("bye");
        }
    });
}

RouterFunctions.route可以创建一个RouterFunction,第一个参数接收RequestPredicate类型,代表一个路径映射规则,比如RequestPredicates.GET("/hello"就表示按照请求方法是GET,路径是/hello进行映射。第二个参数表示映射规则匹配后需要执行的控制器方法,用HandlerFunction类型表示,其返回值类型是ServerResponse或其子类。可以通过ServerResponse.ok().body("bye")这样的方式返回一个状态码是 200,响应体是bye的ServerResponse对象。

完整示例见这里。

SimpleUrlHandlerMapping

SimpleUrlHandlerMapping可以对静态资源进行路径映射,对应的适配器是HttpRequestHandlerAdapter:

@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
    SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
    Map<String, ResourceHttpRequestHandler> urlMap = context.getBeansOfType(ResourceHttpRequestHandler.class);
    handlerMapping.setUrlMap(urlMap);
    return handlerMapping;
}

@Bean
public HttpRequestHandlerAdapter httpRequestHandlerAdapter() {
    return new HttpRequestHandlerAdapter();
}

要注意的是,一般的HandlerMapping会在生命周期方法afterPropertiesSet中从容器收集路径映射,但SimpleUrlHandlerMapping并没有这么做,因此需要手动为其完成路径映射的初始化工作(handlerMapping.setUrlMap)。

静态资源的路径映射表示为ResourceHttpRequestHandler类型:

@Bean("/**")
public ResourceHttpRequestHandler htmlResourceHttpRequestHandler() {
    ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler();
    resourceHttpRequestHandler.setLocations(List.of(new ClassPathResource("html/")));
    return resourceHttpRequestHandler;
}

@Bean("/img/**")
public ResourceHttpRequestHandler imagesResourceHttpRequestHandler() {
    ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler();
    resourceHttpRequestHandler.setLocations(List.of(new ClassPathResource("images/")));
    return resourceHttpRequestHandler;
}

处理的路径由 Bean 名称指定(可以使用通配符),比如/**表示可以匹配任意的路径请求,/img/**表示匹配以/img/开头的请求。请求的目标资源目录由resourceHttpRequestHandler.setLocations指定。这里通过ClassPathResource设置类路径下的指定目录中的资源。

最终的效果是访问 http://localhost:8080/img/2.jpg 时,返回给浏览器的是 /images/2.jpg 文件。访问 http://localhost:8080/index.html 时,返回给浏览器的是 /html/index.html 文件。

完整示例看这里。

ResourceResolver

在ResourceHttpRequestHandler的生命周期方法afterPropertiesSet中,会初始化一个ResourceResolver列表:

image-20250704151457745

默认情况列表中只包含一个基本的实现PathResourceResolver,这个资源解析器的效果是返回路径对应的资源文件。可以通过人为添加ResourceResolver列表的方式扩展ResourceHttpRequestHandler的功能:

@Bean("/**")
public ResourceHttpRequestHandler htmlResourceHttpRequestHandler() {
    ResourceHttpRequestHandler resourceHttpRequestHandler = new ResourceHttpRequestHandler();
    resourceHttpRequestHandler.setResourceResolvers(List.of(
            new CachingResourceResolver(new ConcurrentMapCache("resourceCache")), // 缓存
            new EncodedResourceResolver(), // 压缩
            new PathResourceResolver())); // 获取资源文件
    resourceHttpRequestHandler.setLocations(List.of(new ClassPathResource("html/")));
    return resourceHttpRequestHandler;
}

CachingResourceResolver的作用是缓存请求资源,第二次请求会从缓存中获取,不再使用原始资源文件。创建时候需要指定一个缓存实现方式,这里使用的是ConcurrentMapCache。

  • 注意,这里三个 ResourceResolver 定义顺序是有意义的,请求先要查找有没有缓存,再查找有没有压缩后的资源,如果都没有,才通过路径获取原始资源文件。

  • 这里依次调用 ResourceResolver 处理对静态资源的请求,实际上使用了职责链模式。

应用启动后,对同一个页面第二次请求,就会看到日志信息:

image-20250704152407118

需要在 logback 中开启相应的日志,级别为 TRACE。

EncodedResourceResolver的作用是使用压缩后的资源文件,这样可以缩小网络传输的数据体积。EncodedResourceResolver并不会自动压缩,需要在请求进入服务器前就为资源文件准备好相应的压缩文件。

为配置类添加生命周期方法:

@PostConstruct
public void postConstruct() throws IOException {
    // 为 html 文件生成压缩文件
    // 获取 html 文件
    ClassPathResource classPathResource = new ClassPathResource("html/");
    File dir = classPathResource.getFile();
    File[] files = dir.listFiles(ff -> ff.getName().endsWith(".html"));
    if (files == null) {
        return;
    }
    for (File file : files) {
        // 生成压缩文件
        FileInputStream fileInputStream = new FileInputStream(file);
        GZIPOutputStream gzipOutputStream = new GZIPOutputStream(new FileOutputStream(file.getAbsolutePath() + ".gz"));
        byte[] buffer = new byte[1024];
        do{
            int len = fileInputStream.read(buffer);
            gzipOutputStream.write(buffer, 0, len);
        }
        while (fileInputStream.available() > 0);
        fileInputStream.close();
        gzipOutputStream.close();
    }
}

应用启动后会在 target 目录生成压缩后的资源文件:

image-20250704155003894

从浏览器调试信息中也能看到,返回的响应报文使用了 gzip 压缩:

image-20250704155148011

完整示例看这里。

WelcomePageHandlerMapping

使用WelcomePageHandlerMapping可以在访问根路径(http://localhost:8080/)的时候让页面返回指定的欢迎页。在配置类中添加:

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context){
    Resource indexHtmlResource = context.getResource("classpath:html/index.html");
    return new WelcomePageHandlerMapping(null, context, indexHtmlResource, "/**");
}

WelcomePageHandlerMapping在创建的时候需要指定容器和欢迎页的资源对象(如果是静态资源页)。

注意,WelcomePageHandlerMapping类没有指定访问限定符,也就是说它是protected,因此测试代码必须在包org.springframework.boot.autoconfigure.web.servlet下才能使用这个类。

WelcomePageHandlerMapping的构造器中实际上是为根路径添加了一个控制器,这个控制器实现了Controller接口:

image-20250704161954727

因此还需要像之前那样,添加一个能调用Controller接口的控制器的适配器:

@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter(){
    return new SimpleControllerHandlerAdapter();
}

应用启动后能看到:

image-20250704162217680

完整示例见这里。

总结

Spring 主要有这几种 HandlerMapping:

  • RequestMappingHandlerMapping,最主要的 HandlerMapping,用于解析@RequestMapping注解,生成路径和HandlerMethod的映射。

  • WelcomePageHandlerMapping,用于将根路径映射到欢迎页,会生成一个实现了Controller接口的控制器用于处理请求。

  • BeanNameUrlHandlerMapping,用于将 Bean 名称作为路径进行映射,映射到的目标控制器都实现了Controller接口。

  • RouterFunctionMapping,将路径映射到匿名函数(RouterFunction)

  • SimpleUrlHandlerMapping,映射静态资源文件。

它们可以同时使用,优先级从上到下,RequestMappingHandlerMapping的优先级最高,SimpleUrlHandlerMapping的优先级最低。

主要有以下几种HandlerAdapter:

  • RequestMappingHandlerAdapter,用于处理和调用HandlerMethod类型的 handler

  • SimpleControllerHandlerAdapter,用于处理和调用实现了Controller接口的 handler

  • HandlerFunctionAdapter,用于处理和调用匿名函数(HandlerFunction)类型的 handler

  • HttpRequestHandlerAdapter,用于处理静态资源(HttpRequestHandler)类型的 handler

可以看到,HandlerMapping 的主要用途是将请求映射(Mapping)到不同的处理器(Handler),因此它叫处理器映射(Handler Mapping)。而由 HandlerMapping 生成的不同类型的处理器,由不同的 HandlerAdapter 处理和调用,因此它叫做处理器适配器(Handler Adapter)。

本文的完整示例可以从这里获取。

参考资料

  • 黑马程序员Spring视频教程,深度讲解spring5底层原理

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: spring boot
最后更新:2025年7月5日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号