【探索Spring底层】13.深入DispatcherServlet

news2025/2/2 22:55:44

文章目录

  • 1. DispatcherServlet概述
  • 2. DispatcherServlet的初始化时机
  • 3. DispatcherServlet初始化执行的操作
  • 4. RequestMappingHandlerMapping 基本用途
  • 5. RequestMappingHandlerAdapter 基本用途
  • 6. 参数和返回值解析器

1. DispatcherServlet概述

DispatcherServlet是SpringMVC的核心——前端控制器,但是其本质其实也就是一个HttpServlet。它主要用来拦截符合要求的外部请求,并把请求分发到不同的控制器去处理,根据控制器处理后的结果,生成相应的响应发送到客户端。

DispatcherServlet作为统一访问点,主要进行全局的流程控制。

img


2. DispatcherServlet的初始化时机

DispatcherServlet什么时候初始化呢?

下面我们通过实践来得出结果

首先准备一个内嵌tomcat的容器

public class A20 {
    private static final Logger log = LoggerFactory.getLogger(A20.class);

    public static void main(String[] args) throws Exception {
        //AnnotationConfig:支持java配置类的形式来构建容器
        //ServletWebServer:支持内嵌容器
     AnnotationConfigServletWebServerApplicationContext context =
                new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);

    }
}

AnnotationConfigServletWebServerApplicationContext这个类是可以基于Java配置类的形式构造容器,同时也支持内嵌容器。

详情可以查看:【探索Spring底层】2.容器的实现_起名方面没有灵感的博客-CSDN博客

接下来编写配置类

@Configuration
@ComponentScan
public class WebConfig {
    // ⬅️内嵌 web 容器工厂
    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory(ServerProperties serverProperties) {
        return new TomcatServletWebServerFactory(serverProperties.getPort());
    }

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

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

完成这些之后启动A20main方法

image-20221220153620153

可以发现tomcat启动之后,DispatcherServlet并无初始化。

通过日志我们可以发现启动的服务的端口为8080,这时候我们在浏览器访问localhost:8080

当在浏览器发起请求的一瞬间,观察控制台可以发现DispatcherServlet开始初始化了。

image-20221220153901600

因此不难看出,在首次使用到DispatcherServlet的时候,才会由tomcat服务器来初始化

在初始化时会从 Spring 容器中找一些 Web 需要的组件, 如 HandlerMapping、HandlerAdapter 等,并逐一调用它们的初始化

那么有没有可不可以修改DispatcherServlet初始化的时机呢,让它在tomcat启动的时候就初始化

答案是可以的

@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistrationBean(
    DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties) {
    DispatcherServletRegistrationBean registrationBean = new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    //在tomcat启动的时候就初始化DispatcherServlet
    registrationBean.setLoadOnStartup(1);
    return registrationBean;
}

3. DispatcherServlet初始化执行的操作

DispatcherServlet的初始化操作,离不开这个类中的onRefresh方法

@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
    //文件上传
    initMultipartResolver(context);
    //本地化信息
    initLocaleResolver(context);
    initThemeResolver(context);
    //做路径映射
    initHandlerMappings(context);
    //适配不同形式的控制器方法
    initHandlerAdapters(context);
    //解析异常
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

initStrategies方法中分别调用了9个方法,这些方法都类似,9个方法其实也就是初始化9类组件

就下面的initHandlerMappings来说

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
	//判断是否要到父容器中寻找
    if (this.detectAllHandlerMappings) {
        // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
        Map<String, HandlerMapping> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            // We keep HandlerMappings in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        //在当前容器找
        try {
            HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
            //容器中有优先使用容器的
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, we'll add a default HandlerMapping later.
        }
    }

    // Ensure we have at least one HandlerMapping, by registering
    // a default HandlerMapping if no other mappings are found.
    //如果容器中没有
    if (this.handlerMappings == null) {
        //使用默认的
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
                         "': using default strategies from DispatcherServlet.properties");
        }
    }

    for (HandlerMapping mapping : this.handlerMappings) {
        if (mapping.usesPathPatterns()) {
            this.parseRequestPath = true;
            break;
        }
    }
}

不难看出,这里九大组件都是使用默认的。image-20221220160026077


4. RequestMappingHandlerMapping 基本用途

RequestMappingHandlerMapping的作用是在容器启动后将系统中所有控制器方法的请求条件(RequestMappingInfo)和控制器方法(HandlerMethod)的对应关系注册到RequestMappingHandlerMapping Bean的内存中,待接口请求系统的时候根据请求条件和内存中存储的系统接口信息比对,再执行对应的控制器方法。

其工作流程如下:先到当前容器下找到所有的控制类,然后进一步查看这个控制类有哪一些方法,如果方法上加了如@GetMapping等,便会将其记录下来,记录它们的路径、对应的控制器方法

// 如果用 DispatcherServlet 初始化时默认添加的组件, 并不会作为 bean, 给测试带来困扰
// ⬅️1. 加入RequestMappingHandlerMapping
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
    return new RequestMappingHandlerMapping();
}

前面说到九大组件会先在容器中寻找看看有没有对应的Bean,如果没有就会使用默认的,但是使用默认的话,创建出来的RequestMappingHandlerMapping会直接称为DispatcherServlet的其中一个成员变量,在容器中无法获取,对我们的测试有很大影响,因此需要我们自己将RequestMappingHandlerMapping放到容器中。

public class A20 {
    private static final Logger log = LoggerFactory.getLogger(A20.class);

    public static void main(String[] args) throws Exception {
        //AnnotationConfig:支持java配置类的形式来构建容器
        //ServletWebServer:支持内嵌容器
        AnnotationConfigServletWebServerApplicationContext context =
                new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);

         作用 解析 @RequestMapping 以及派生注解,生成路径与控制器方法的映射关系, 在初始化时就生成
        RequestMappingHandlerMapping handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
        //
         获取映射结果
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
        handlerMethods.forEach((k, v) -> {
            System.out.println(k + "=" + v);
        });
    }
}

通过getHandlerMethods就能得到映射结果,运行可发现控制类中的所有请求的路径和对应的方法名都获取成功了

  • key 是 RequestMappingInfo 类型,包括请求路径、请求方法等信息
  • value 是 HandlerMethod 类型,包括控制器方法对象、控制器对象
  • 有了这个 Map,就可以在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet

image-20221220161141153

准备好之后进行一下模拟请求

 请求来了,获取控制器方法  返回处理器执行链对象
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test1");
HandlerExecutionChain chain = handlerMapping.getHandler(request);
System.out.println(chain);

image-20221220161614148

在请求到达时,快速完成映射,找到 HandlerMethod 并与匹配的拦截器一起返回给 DispatcherServlet


5. RequestMappingHandlerAdapter 基本用途

RequestMappingHandlerAdapter翻译过来叫处理器适配器,其作用就是去调用被@RequestMapping标注的控制器方法

同样的,先将其放在容器中

RequestMappingHandlerAdapter 里面有个非常重要的方法protected ModelAndView invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod),但是这个是保护方法,可以稍微做处理方法作用域

public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
    @Override
    public ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        return super.invokeHandlerMethod(request, response, handlerMethod);
    }
}

放在容器中

@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {

    MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();

    return handlerAdapter;
}

发送请求

 请求来了,获取控制器方法  返回处理器执行链对象
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test1");

MockHttpServletResponse response = new MockHttpServletResponse();
HandlerExecutionChain chain = handlerMapping.getHandler(request);
System.out.println(chain);
//
System.out.println(">>>>>>>>>>>>>>>>>>>>>");
 HandlerAdapter 作用: 调用控制器方法
MyRequestMappingHandlerAdapter handlerAdapter = context.getBean(MyRequestMappingHandlerAdapter.class);
handlerAdapter.invokeHandlerMethod(request, response, (HandlerMethod) chain.getHandler());

image-20221220162807858

test1方法确实被调用了!!!


6. 参数和返回值解析器

通过这两个方法就可以的出当前所有的参数解析器和返回值解析器了

System.out.println(">>>>>>>>>>>>>>>>>>>>> 所有参数解析器");
for (HandlerMethodArgumentResolver resolver : handlerAdapter.getArgumentResolvers()) {
    System.out.println(resolver);
}

System.out.println(">>>>>>>>>>>>>>>>>>>>> 所有返回值解析器");
for (HandlerMethodReturnValueHandler handler : handlerAdapter.getReturnValueHandlers()) {
    System.out.println(handler);
}

image-20221220163119653

但是要想进一步了解其是怎么实现的,最好的方法就是模拟实现

@PutMapping("/test3")
public ModelAndView test3(@Token String token) {
    log.debug("test3({})", token);
    return null;
}

这里准备一个映射地址/test3,里面的@Token是为了获取请求头的Token,怎么实现呢?

首先先来实现一下参数解析器

自定义一个TokenArgumentResolver实现HandlerMethodArgumentResolver接口

public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    // 是否支持某个参数
    public boolean supportsParameter(MethodParameter parameter) {
        Token token = parameter.getParameterAnnotation(Token.class);
        return token != null;
    }

    @Override
    // 解析参数
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return webRequest.getHeader("token");
    }
}

这里是先执行supportsParameter方法,判断是否有@Token注解,如果有才会执行resolveArgument方法

这样

虽然这个参数解析器完成了,我们还需要配置一下,因此MyRequestMappingHandlerAdapter并不知有新的参数解析器

@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {
    TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
    MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
    handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));
    return handlerAdapter;
}

这样一个参数解析器就搞好了

接下来实现一下返回值解析器

返回值处理器可以根据返回值不同而作出不同的选择。

通过也可以根据方法上是否有某一注解做出一些不同的处理。

@RequestMapping("/test4")
@Yml
public User test4() {
    log.debug("test4");
    return new User("张三", 18);
}
public class YmlReturnValueHandler implements HandlerMethodReturnValueHandler {
    @Override
    //判断是否有注解
    public boolean supportsReturnType(MethodParameter returnType) {
        Yml yml = returnType.getMethodAnnotation(Yml.class);
        return yml != null;
    }

    @Override                   //  返回值
    public void handleReturnValue(Object returnValue, MethodParameter returnType,
                                  ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        // 1. 转换返回结果为 yaml 字符串
        String str = new Yaml().dump(returnValue);

        // 2. 将 yaml 字符串写入响应
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        response.setContentType("text/plain;charset=utf-8");
        response.getWriter().print(str);

        // 3. 设置请求已经处理完毕
        mavContainer.setRequestHandled(true);
    }
}

@Bean
public MyRequestMappingHandlerAdapter requestMappingHandlerAdapter() {
    TokenArgumentResolver tokenArgumentResolver = new TokenArgumentResolver();
    YmlReturnValueHandler ymlReturnValueHandler = new YmlReturnValueHandler();
    MyRequestMappingHandlerAdapter handlerAdapter = new MyRequestMappingHandlerAdapter();
    handlerAdapter.setCustomArgumentResolvers(List.of(tokenArgumentResolver));
    handlerAdapter.setCustomReturnValueHandlers(List.of(ymlReturnValueHandler));
    return handlerAdapter;
}

具体操作和参数解析器类似。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/103926.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AMS启动流程——APP启动过程

AMS流程图 基本慨念 1.zygote zygote意为“受精卵“。Android是基于Linux系统的&#xff0c;而在Linux中&#xff0c;所有的进程都是由init进程直接或者是间接fork出来的&#xff0c;zygote进程也不例外。 在Android系统里面&#xff0c;zygote是一个进程的名字。Android是基…

Effective C++(三):资源管理

个人读书记录&#xff0c;不适用教学内容。 目录 条款13&#xff1a;以对象管理资源 条款14&#xff1a;在资源管理类中小心copying行为 条款15&#xff1a;在资源管理类中提供对原始资源的访问 条款16&#xff1a;成对使用new和delete时要采取相同形式 条款17&#xff1a…

cubeIDE开发, STM32实时时钟(RTC)写入及读取日历时间开发要点

一、RTC简介 实时时钟的缩写是RTC(Real_Time Clock)&#xff0c;核心是晶振&#xff0c;晶振频率一般为32768 Hz 。它为分频计数器提供精确的与低功耗的实基信号。它可以用于产生秒、分、时、日等信息。为了确保时钟长期的准确性&#xff0c;晶振必须正常工作&#xff0c;不能够…

中小企业的公司财务管理系统

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 本课题研究对象是中小企业财务管理系统&#xff0c;设计采用自己开发实践和所学知 识&#xff0c;系统部分主要分为以下…

【圣诞树代码】送她六棵圣诞树,祝她圣诞快乐~(送女朋友必备!)

“ 六棵圣诞树&#xff0c;满足她圣诞愿望 ” 距离25号圣诞节只有几天了&#xff0c;程序员有属于程序员的浪漫&#xff0c;这不来了~ 如果一颗圣诞树不够&#xff0c;那就送她六棵&#xff0c;祝她圣诞快乐~ 直接上效果图—— 01 02 03 04 05 06 代码如下&#xff1a; 第…

[1184]FinalShell下载安装

文章目录FinalShell介绍初步使用更换背景图连接记录删除FinalShell介绍 官网&#xff1a;http://www.hostbuf.com/ http://www.hostbuf.com/?install_fs FinalShell 简介&#xff1a; FinalShell 相当于 xshell ftp 的组合&#xff0c;即&#xff1a;FinalShell xshell f…

【计算机考研408】中断处理流程

中断请求 中断源是请求CPU中断的设备或事件&#xff0c;一台计算机允许有多个中断源。每个中断源向CPU发出中断请求的时间是随机的。 中断响应判优 中断响应优先级是指CPU响应中断请求的先后顺序。当多个中断源同时提出请求时&#xff0c;需通过中断判优逻辑来确定响应哪个中…

Python asyncore socket客户端开发基本使用

目录 介绍 1.定义类并且继承 asyncore.dispatcher 2.实现类中的回调代码 调用父类方法 创建 socket 对象 连接服务器 3.创建对象并且执行 asyncore.loop 进入运行循环 服务端示例代码 运行结果 注意&#xff1a; 介绍 asyncore库是python的一个标准库&#xff0c;提…

作为程序员,你离拿offer就差这个免费且好用的简历制作工具

为什么你的简历总是石沉大海&#xff1f;为什么你投递的岗位迟迟得不到回应&#xff1f;除了岗位招满、HR没看到以及竞争太激烈等客观因素外&#xff0c;最重要的是——“简历”出现了问题。 “简历”就像名片一样&#xff0c;在面试和求职的过程中&#xff0c;一份优质的简历…

数字孪生城市项目的关键技术展望

智慧城市是社会空间、物理空间和信息系统三元有机融合的条件下城市智慧化转型的新型态, 运用以数字孪生为代表的新一代信息化技术优化城市系统, 提升城市品质和综合竞争力, 从而实现可持续发展成为智慧城市构建的新趋势。 数字孪生城市项目的关键技术 北京智汇云舟科技有限公司…

chrome的几种存储storage模式

Storage生命周期存放数据大小与服务器通信localStore 除非被清除&#xff0c;否则永远保存 一般5MB 仅在客户端(即浏览器)中SessionStorage仅在当前会话下有效&#xff0c;关闭页面或者浏览器后被清除 一般5MB 保存&#xff0c;不参与和服务器的通信Cookies一般由服务器生成&a…

荣盛生物将再次上会:前三季度收入约2亿元,曾被暂缓审议

近日&#xff0c;上海证券交易所披露的信息显示&#xff0c;上海荣盛生物药业股份有限公司&#xff08;下称“荣盛生物”&#xff09;将于2022年12月26日接受科创板上市委员会的现场审议&#xff08;即“上会”&#xff09;。据贝多财经了解&#xff0c;荣盛生物曾于11月7月7日…

TCP滑动窗口、流量控制、拥塞控制

TCP滑动窗口、流量控制、拥塞控制一、滑动窗口二、流量控制三、拥塞控制一、滑动窗口 上篇博客我们介绍了TCP报文结构、确认应答机制、超时重传机制、连接管理机制。 TCP保证了可靠传输&#xff0c;但是失去了效率。那么怎么样尽可能提高传输效率呢&#xff1f;&#xff1f;&a…

react笔记_07组件实例化对象的三大属性

目录前提状态(state)作用状态使用总结属性(props)展开运算符复习props-作为属性传入数据props-使用展开运算符展开对象props-进行数据类型限制propTypes语法校验规则举例说明defaultProps语法举例说明refs字符串形式的ref语法举例说明注意点回调形式的ref什么叫做回调&#xff…

RT-Thread 简介

1.RT-Thread 概述 RT-Thread&#xff0c;全称是Real Time-Thread&#xff0c;顾名思义&#xff0c;它是一个嵌入式实时多线程操作系统&#xff0c; 基本属性之一是支持多任务&#xff0c;允许多个任务同时运行并不意味着处理器在同一时刻真地执行了多个任务。 事实上&#xff…

Redis实战——签到统计(BitMap的用法)

1. 什么是BitMap 我们针对签到功能完全可以通过mysql来完成&#xff0c;比如说以下这张表 但是&#xff0c;用户一次签到&#xff0c;就是一条记录&#xff0c;假如有1000万用户&#xff0c;平均每人每年签到次数为10次&#xff0c;则这张表一年的数据量为 1亿条。 每签到一次…

js实现图片的放大缩小(鼠标长按拖拽、鼠标滚轮控制放大缩小)

系列文章目录 文章目录系列文章目录背景与效果图1.背景如下&#xff08;功能图&#xff09;&#xff1a;2.效果图如下&#xff1a;拖拽后的效果缩放的效果放大的效果一、功能&#xff1a;支持鼠标长按拖拽1.鼠标事件&#xff1a;2.拖拽功能流程3.拖拽部分代码如下二、功能&…

HTTP报文详解

个人博客地址&#xff1a; http://xiaohe-blog.top/ 文章目录1. 请求1.1 请求行1.2 请求头1.3 空白行1.4 请求体2. 响应2.1 状态行2.2 响应头2.3 空白行2.4 响应体2.5 HTTP报文总结3. HTTP方法4. GET与POST的区别5. 状态码1. 请求 1.1 请求行 请求方式 请求地址 请求协议版本号…

指针进阶1 2

字符指针 int main() {char ch q;char* pa &ch;return 0; }不仅可以这样写还可以指向一个字符串 int main() {/*char ch q;char* pa &ch;*/char* ph "hello world";printf("%c\n", *ph);//打印遇到\0才会结束&#xff0c;没数据会打印随机值…

pybind11学习 | VS2022下安装配置

pybind11是一个只有头文件&#xff08;header-only&#xff09;的轻量级库&#xff0c;其主要目的是建立C的Python代码绑定&#xff0c;实现C和Python无缝连接。我学习这个工具的目的&#xff0c;是为了能够在Python中调用C代码实现一些计算密集型任务&#xff0c;同时培养自己…