第三十五讲-HandlerMapping与HandlerAdapter-3

第三十五讲-HandlerMapping与HandlerAdapter-3

1. SimpleUrlHandlerMapping和HttpRequestHandlerAdapter

这讲我们再介绍一组HandlerMapping和HandlerAdapter:SimpleUrlHandlerMappingHttpRequestHandlerAdapter

这一组是用来处理静态资源的,更具体一点就是:

  1. SimpleUrlHandlerMapping做静态资源映射的
  2. ResourceHttpRequestHandler作为处理器处理静态资源
  3. HttpRequestHandlerAdapter用来调用处理器

下面我们通过例子来演示一下静态资源的处理过程:

package org.springframework.boot.autoconfigure.web.servlet;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
import org.springframework.web.servlet.resource.CachingResourceResolver;
import org.springframework.web.servlet.resource.EncodedResourceResolver;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;

@Configuration
public class WebConfig {
    @Bean // ⬅️内嵌 web 容器工厂
    public TomcatServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory(8080);
    }

    @Bean // ⬅️创建 DispatcherServlet
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }

    @Bean // ⬅️注册 DispatcherServlet, Spring MVC 的入口
    public DispatcherServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
        return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    }


    @Bean
    public SimpleUrlHandlerMapping simpleUrlHandlerMapping(ApplicationContext context) {
        SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
        // 找到所有类型为ResourceHttpRequestHandler的处理器类,返回为一个Map集合,
        // 这个Map集合的key就是bean的名字,例如:/**, /img/**, map的值就是对应路径的处理器类
        // 这个map K-V映射关系就是路径与静态资源处理器之间的映射关系。
        // 将来访问静态资源时,会根据路由找到对应的处理器并进行相应的处理。
        Map<String, ResourceHttpRequestHandler> map = context.getBeansOfType(ResourceHttpRequestHandler.class);
        handlerMapping.setUrlMap(map);
        System.out.println(map);
        return handlerMapping;
    }

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

    /*
        /index.html
        /r1.html
        /r2.html

        /**
     */
    // 创建一个Handler处理器,用来处理html静态资源请求
    @Bean("/**")
    public ResourceHttpRequestHandler handler1() {
        ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
        // 设置静态资源的目录
        handler.setLocations(List.of(new ClassPathResource("static/"))); 
        return handler;
    }

    /*
        /img/1.jpg
        /img/2.jpg
        /img/3.jpg

        /img/**
     */

    // 用来处理图片静态资源的处理器
    @Bean("/img/**")
    public ResourceHttpRequestHandler handler2() {
        ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
        handler.setLocations(List.of(new ClassPathResource("images/")));
        return handler;
    }
}

我们编写

主方法测试一下:

package org.springframework.boot.autoconfigure.web.servlet;

import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.servlet.function.HandlerFunction;

public class A35 {
    public static void main(String[] args) {
        AnnotationConfigServletWebServerApplicationContext context
                = new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
    }
}

image-20240819201526653

image-20240819201559815

image-20240819201620442

tips:我们可以在控制台看到静态资源请求路径和对应的处理器之间的关系:

{/**=ResourceHttpRequestHandler [Classpath [static/]], /img/**=ResourceHttpRequestHandler [Classpath [images/]]}

我们来总结一下:

  1. 首先由SimpleUrlHandlerMapping来记录静态资源路径和对应处理器之间的映射关系。

  2. 当客户端发送的请求利用通配符的方式进行匹配,匹配成功之后就会由对应的静态资源处理器来处理器请求。

  3. 紧接着由HttpRequestHandlerAdapter来调用对应的处理器,来执行静态资源的处理。

2. ResourceHttpRequestHandler的优化-CachingResourceResolver

接下来我们讲讲对ResourceHttpRequestHandler的优化。我们首先看一下ResourceHttpRequestHandler的初始化方法, 这个方法里有一个比较关键的步骤,就是设置资源解析器。如下面的注释:

@Override
	public void afterPropertiesSet() throws Exception {
		resolveResourceLocations();

		if (logger.isWarnEnabled() && CollectionUtils.isEmpty(getLocations())) {
			logger.warn("Locations list is empty. No resources will be served unless a " +
					"custom ResourceResolver is configured as an alternative to PathResourceResolver.");
		}

        // 设置资源解析器-->也就是说寻找这些静态资源依赖于资源解析器
		if (this.resourceResolvers.isEmpty()) {
			this.resourceResolvers.add(new PathResourceResolver());
		}

		initAllowedLocations();

		// Initialize immutable resolver and transformer chains
		this.resolverChain = new DefaultResourceResolverChain(this.resourceResolvers);
		this.transformerChain = new DefaultResourceTransformerChain(this.resolverChain, this.resourceTransformers);

		if (this.resourceHttpMessageConverter == null) {
			this.resourceHttpMessageConverter = new ResourceHttpMessageConverter();
		}
		if (this.resourceRegionHttpMessageConverter == null) {
			this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter();
		}

		ContentNegotiationManager manager = getContentNegotiationManager();
		if (manager != null) {
			setMediaTypes(manager.getMediaTypeMappings());
		}

		@SuppressWarnings("deprecation")
		org.springframework.web.accept.PathExtensionContentNegotiationStrategy strategy =
				initContentNegotiationStrategy();
		if (strategy != null) {
			setMediaTypes(strategy.getMediaTypes());
		}
	}

我们可以看到,在静态资源处理器初始化的时候会加入一些资源解析器,也就是说Spring MVC寻找静态资源就是依赖于这些资源解析器。

我们从代码中可以得知,初始化方法中就加入了PathResourceResolver一个资源解析器。该资源解析器是功能最基本的解析器,起作用就是从磁盘上读取静态文件

除了PathResourceResolver资源解析器,Spring还提供了其它资源解析器,用于增强Spring解析静态资源的能力。下面呢,我们就来添加新的静态资源解析器来解析静态资源。

// 创建一个Handler处理器,用来处理html静态资源请求
@Bean("/**")
public ResourceHttpRequestHandler handler1() {
    ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
    // 设置静态资源的目录
    handler.setLocations(List.of(new ClassPathResource("static/")));
    handler.setResourceResolvers(List.of(
            new CachingResourceResolver(new ConcurrentMapCache("cache1")),	// 用于在读取资源的时候加入缓存(第一次从磁盘中读,下一次从缓存中读)
            new EncodedResourceResolver(),	// 用于读取压缩资源的静态资源解析器
            new PathResourceResolver()		// 用于从磁盘上读取静态资源的解析器
    ));
    return handler;
}

重启服务器测试:

image-20240819205337580

20:53:10.406 [http-nio-8080-exec-1] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/r1.html", parameters={}

我们发现第一次访问静态资源走的是DispatcherServlet,接着我们再次访问一次:

20:53:50.517 [http-nio-8080-exec-2] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/r1.html", parameters={}
20:53:50.520 [http-nio-8080-exec-2] DEBUG org.springframework.web.servlet.CachingResourceResolver - Resource resolved from cache

我们发现,第二次访问的静态资源是从缓存获取到的!从而提高了性能。

3. ResourceHttpRequestHandler的优化-EncodedResourceResolver

接下来我们看一下用于处理压缩静态资源文件的解析器EncodedResourceResolver, 它的作用是读取压缩后的文件,压缩后的文件可以大大减少网络传输所需的数据量,这对于html这种文本文件来讲,压缩比还是很高的。我们使用上面的测试类来测试一下。

我们首先看一下原始r2.html文件的大小:

image-20240819210425858

我们发现6226byte。

我们从浏览器访问r2.html并查看一下r2.html是多大:

image-20240819210856595

我们发现,返回给客户端的r2.html还是6byte多,并没有发生什么变化,为什么这里没有用到压缩呢?

原来啊,是需要我们手动的生成压缩文件才可以!

我们可以手动编写一段压缩文件的代码(此处非重点):

// 压缩文件
@PostConstruct
@SuppressWarnings("all")
public void initGzip() throws IOException {
    Resource resource = new ClassPathResource("static");
    File dir = resource.getFile();
    // 生成每个html文件的压缩文件,后缀为gz格式
    for (File file : dir.listFiles(pathname -> pathname.getName().endsWith(".html"))) {
        System.out.println(file);
        try (FileInputStream fis = new FileInputStream(file); GZIPOutputStream fos = new GZIPOutputStream(new FileOutputStream(file.getAbsoluteFile() + ".gz"))) {
            byte[] bytes = new byte[8 * 1024];
            int len;
            while ((len = fis.read(bytes)) != -1) {
                fos.write(bytes, 0, len);
            }
        }
    }
}

我们来看一下r2.html压缩后的文件r2.html.gz的大小:

image-20240819211434610

我们发现压缩后只有不到3byte, 我们重新测试一下:

image-20240819211553003

我们发现传输过程中r2.html大小为不到3byte。使用压缩的好处就是网络传输的数据量小了!

4. 特殊的HandlerMapping-欢迎页映射器(欢迎页处理)

欢迎页处理器的作用是把一个访问根路径下的请求映射到一个欢迎页上,如下面的代码:

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext context) {
    // 将http://localhost:8080/映射到http://localhost:8080/index.html,其中的index.html为我们自定义的页面
    // 欢迎静态资源页面:index.html
    Resource resource = context.getResource("classpath:static/index.html");
    return new WelcomePageHandlerMapping(null, context, resource, "/**");
    // Controller 接口
}

// 识别Controller接口的处理器-->用于处理WelcomePageHandlerMapping
@Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
    return new SimpleControllerHandlerAdapter();
}

重启测试 http://localhost:8080

image-20240819213001635

我们来看一下欢迎页处理器的执行流程:

  1. WelcomePageHandlerMapping, 映射欢迎页(即只映射 '/')
    1. 它内置了 handler ParameterizableViewController 作用是不执行逻辑, 仅根据视图名找视图
    2. 视图名固定为 forward:index.html /**
  2. SimpleControllerHandlerAdapter, 调用 handler
    1. 转发至 /index.html
    2. 处理 /index.html 又会走上面的静态资源处理流程

5. 对于HandlerMapping与HandlerAdapter的总结

我们一共介绍了五大映射器和四大适配器。我们对上述的功能做一个小结:

a. HandlerMapping 负责建立请求与控制器之间的映射关系

  • RequestMappingHandlerMapping (与 @RequestMapping 匹配)
    • WelcomePageHandlerMapping (/)
      • BeanNameUrlHandlerMapping (与 bean 的名字匹配 以 / 开头)
      • RouterFunctionMapping (函数式 RequestPredicate, HandlerFunction)
      • SimpleUrlHandlerMapping (静态资源 通配符 /** /img/**)
        之间也会有顺序问题, boot 中默认顺序如上
        b. HandlerAdapter 负责实现对各种各样的 handler 的适配调用
      • RequestMappingHandlerAdapter 处理:@RequestMapping 方法
        参数解析器、返回值处理器体现了组合模式
      • SimpleControllerHandlerAdapter 处理:Controller 接口
      • HandlerFunctionAdapter 处理:HandlerFunction 函数式接口
      • HttpRequestHandlerAdapter 处理:HttpRequestHandler 接口 (静态资源处理)
        这也是典型适配器模式体现
        c. ResourceHttpRequestHandler.setResourceResolvers 这是典型责任链模式体现
posted @   LilyFlower  阅读(6)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示