首页 > 其他分享 >SpringMVC 学习笔记

SpringMVC 学习笔记

时间:2024-11-09 15:41:38浏览次数:4  
标签:请求 SpringMVC 笔记 学习 处理器 DispatcherServlet 方法 public

概述

SpringMVC 中的 MVC 即模型-视图-控制器,该框架围绕一个 DispatcherServlet 改计而成,DispatcherServlet 会把请求分发给各个处理器,并支持可配置的处理器映射和视图渲染等功能

SpringMVC 的工作流程如下所示:

  1. 客户端发起 HTTP 请求:客户端将请求提交到 DispatcherServlet
  2. 寻找处理器:DispatcherServlet 控制器查询一个或多个 HandlerMapping,找到处理该请求的 Controller
  3. 调用处理器:DispatcherServlet 将请求提交到 Controller
  4. 调用业务处理逻辑并返回结果:Controller 在调用业务处理逻辑后,返回 ModelAndView
  5. 处理视图映射并返回模型:DispatcherServlet 查询一个或多个 ViewResolver 视图解析器,找到 ModelAndView 指定的视图
  6. HTTP 响应:视图负责将结果在客户端浏览器上谊染和展示

DispatcherServlet

在 Java 中可以使用 Servlet 来处理请求,客户端每次发出请求,Servlet 会调用 service 方法来处理,SpringMVC 通过创建 DispatchServlet 来统一接收请求并分发处理

1. 创建 DispatcherServlet

在 Tomcat 中创建 DispatcherServlet 的方式有两种:

第一种方式是通过 web.xml,Tomcat 会在启动时加载根路径下 /WEB-INF/web.xml 配置文件,根据其中的配置加载 Servlet,Listener,Filter 等,下面是 SpringMVC 的常见配置:

<servlet>
    <servlet-name>dispatcher</servlet>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <!--DispatchServlet 持有的 WebApplicationContext-->
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
        <!-- 1:tomcat 启动时创建 DispatcherServlet,0:tomcat 启动时不创建 DispatcherServlet,接收到请求才创建 -->
        <load-on-startup>1</load-on-startup>
    </init-param>
</servlet>

<servlet-mapping>
    <servlet-name>dispatch</servlet-name>
    <servlet-pattern>/*</servlet-pattern>
</servlet-mapping>

第二种方式是通过 WebApplicationInitializer,简单来说就是 Tomcat 会探测并加载 ServletContainerInitalizer 的实现类,并调用他的 onStartup 方法,而 SpringMVC 提供了对应的实现类 SpringServletContainerInitializer。而 SpringServletContainerInitializer 又会探测并加载 ClassPath 下 WebApplicationContextInitializer 的实现类,调用它的 onStartUp 方法

因此我们可以继承 WebApplicationContextInitializer 实现 onStartUp 方法,在其中以代码的方式配置 DispatchServlet

public class MyWebAppInitializer implements WebApplicationInitializer {
 
    @Override
    public void onStartup(ServletContext container) {

        // 创建 dispatcher 持有的上下文容器
        AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
        dispatcherContext.register(DispatcherConfig.class);

        // 注册、配置 dispatcher servlet
        ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/*");
    }
}

在创建 DispatcherServlet 时,其内部会创建一个 Spring 容器 WebApplicationContext,目的是通过 Bean 的方式管理 Web 应用中的对象

2. DispatcherServlet 初始化

DispatcherServlet 是 Servlet 的实现类,Servlet的生命周期分为三个阶段:初始化、运行和销毁。初始化阶段会调用 init() 方法,DispatcherServlet 经过一系列封装,最终会调用 initStrategies 方法进行初始化,在这里我们重点关注 initHandlerMappings 和 initHandlerAdapters

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

initHandlerMappings 方法负责加载 HandlerMappings 也就是处理器映射器,如果程序员没有配置,那么 SpringMVC 也有默认提供的 HandlerMapping。每个 HandlerMapping 会以 Bean 的形式保持在容器,并执行各自的初始化方法。

默认的 HandlerMapping 有以下两种:

  • RequestMappingHandlerMapping:根据请求 URL 映射到对应 @RequestMapping 方法
  • BeanNameUrlHandlerMapping:根据请求 URL 映射到对应的 Bean 的名称(如该 Bean 的名称为 /test),这个 Bean 会提供一个处理请求逻辑的方法

RequestMappingHandlerMapping 在初始化的过程中会从处理器 bean(即被 @Controller 注解)中找出所有的处理方法(即被 @RequestMapping 注解),把处理方法的 @RequestMapping 注解解析成 RequestMappingInfo 对象,再把处理方法对象包装成 HandlerMethod 对象。然后把 RequestMappingInfo 和 HandlerMethod 对象以 map 的形式缓存起来,key 为 RequestMappingInfo,value 为 HandlerMethod,日后将请求映射到处理器时会使用到

BeanNameUrlHandlerMapping 在初始化的过程中会扫描 Spring 容器中所有的 bean,获取每个 bean 的名称以及对应的 Bean 保持起来。将每个 bean 的名称与请求的 URL 路径进行匹配,如果 bean 的名称与 URL 路径匹配(忽略大小写),那么就以匹配的 Bean 作为处理该请求的处理器。匹配 Bean 的实现如下:

@Componet("/welcome*")
public class WelcomeController implements Controller {

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response)   {
        ...
    }
}

或者

@Componet("/welcome*")
public class WelcomeController implements HttpRequestHandler {

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response)   {
        ...
    }
}

initHandlerAdapters 方法负责加载适配器,同样以 Bean 的形式保持在容器并执行初始化方法。如果程序员没有配置,那么 SpringMVC 也有默认提供的 HandlerAdapter。处理请求时,根据请求找到对应的处理器对象后,就会适配得到一个 HandlerAdapter,由 HandlerAdapter 执行处请求

SpringMVC 默认的适配器有:

  • RequestMappingHandlerAdapter:适配处理器是 HandlerMethod 对象
  • HandlerFunctionAdapter:适配处理器是HandlerFunction对象
  • HttpRequestHandlerAdapter:适配处理器是 HttpRequestHandler 对象
  • SimpleControerHandlerAdapter:适配处理器是 Controller 对象

父子容器

前面提到过,初始化 DispatcherServlet 时其内部会跟着创建一个 Spring 容器,那如果在 web.xml 中配置了两个不同的 DispatcherServlet,那么就会有两个分属不同 DispatcherServlet 的 Spring 容器

<!-- 第一个 DispatcherServlet -->
<servlet>
    <servlet-name>app1</servlet>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring1.xml</param-value>
        <load-on-startup>1</load-on-startup>
    </init-param>
</servlet>

<servlet-mapping>
    <servlet-name>app1</servlet-name>
    <servlet-pattern>/app1/*</servlet-pattern>
</servlet-mapping>

<!-- 第二个 DispatcherServlet -->
<servlet>
    <servlet-name>app2</servlet>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring2.xml</param-value>
        <load-on-startup>1</load-on-startup>
    </init-param>
</servlet>

<servlet-mapping>
    <servlet-name>app2</servlet-name>
    <servlet-pattern>/app2/*</servlet-pattern>
</servlet-mapping>

出现多个 DispatcherServlet 一般是解决多版本的问题,比如有一个 TestV1Controller 在 app1 这个 DispatcherServlet,现在多了一个升级版 TestV2Controller,就可以放在 app2,使用不同的映射路径

而有时候我们只希望区分不同的 Controller,而通用的 Service 并不需要在每个容器都保存一份,就可以配置父容器,将 Service 放在父容器。DispatcherServlet 初始化时会自动寻找是否存在父容器。

<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-spring.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring1.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>

</web-app>

ContextLoaderListener 被配置到监听器列表,ServletContext 初始化时会使用 context-param 中参数名为 contextConfigLocation 设置的配置文件初始化父容器


SpringMVC 处理请求

SpringMVC 处理请求流程可分如下步骤:

  1. 根据路径找到对应的 Handler
  2. 解析参数并绑定
  3. 执行方法
  4. 解析返回值

1. 根据请求寻找 Handler

请求到来会执行 DispatcherServlet 的 getHandler 方法。遍历所有 HanlderMapping,每个 HandlerMapping 都是根据请求寻找 Handler,但寻找的方式不一样,比如 RequestMappingHandlerMapping 就是根据请求路径寻找 HandlerMethod, BeanNameUrlHandlerMapping 则是将请求路径映射到对应的 Bean 的名称。通过遍历 HandlerMapping,直到请求能找到对应的 Handler

不同的 HanlderMapping 所对应的 Handler 类型也不同,因此要找到对应类型的适配器。遍历所有 HandlerAdapter,如果找对适配的 HandlerAdapter 就返回,执行适配器的 handle 方法

2. 解析参数并执行方法

以 RequestMappingHandlerMapping 为例,Handler 的实际类型是 HandlerMethod,适配的是 RequestMappingHandlerAdapter。执行 invokeHandlerMethod 方法,解析 @initBinder 注解的方法并保存,解析 @SessionAttributes 注解设置的键值对,解析 @ModelAttribute 注解的方法,上述解析的结果将保存在 ModelFactory 对象,ModelFactory 用来初始化 Model 对象,初始化时将 @SessionAttributes 和 @ModelAttribute 设置的值保存到 Model 对象

接下来是创建参数解析器 argumentResolvers 和返回值解析器 returnValueHandlers。解析器有多种类型,对应不同的场景,例如使用 @PathVariable 注解传参就使用 PathVariableMethodArgumentResolver 解析器对象,返回值是 ModelAndView 对象则用 ModelAndViewMethodReturnValueHandler 解析器对象

获取方法参数,方法参数的类型是 MethodParameter,不仅包含了参数的名称,还包括参数的信息,比如是否有 @ReqeustParam 注解。遍历方法参数,并逐一用参数解析器遍历,找到适用的解析器进行解析,再根据参数名称从请求中获取参数值。如果定义了类型转换器,那就对参数类型进行转换。最后使用反射执行真正的方法逻辑

3. 解析返回值

拿到返回值后也是遍历寻找合适的返回值解析器进行处理,比如开发中经常会使用 @ResponseBody 注解返回 json,就会使用 RequestResponseBodyMethodProcessor 处理器进行处理,该处理器同时还承担了参数解析的作用。解析的过程中需要用到消息转换器 HttpMessageConverter,其作用是将方法的返回值转换为接收端(如浏览器)能接受的响应类型,SpringMVC 同样提供了默认的转换器。比如使用 @ResponseBody 注解的方法返回了 String 类型的返回值,那么就会遍历判断哪个消息转换器能处理 String 类型的返回值,在 RequestResponseBodyMethodProcessor 处理器中默认使用 StringHttpMessageConverter。接下来是内容协商,即是找到客户端能接受并且服务端能提供的内容类型,比如客户端希望优先返回 text/plain 类型的内容,而 StringHttpMessageConverter 能支持该类型,那么就使用 StringHttpMessageConverter 将方法返回值写入响应报文返回给客户端。如果我们希望方法直接返回对象类型并自动序列化为 json,那么就需要自定义消息转换器,此时 SpringMVC 将不再提供默认的转换器而是直接使用自定义的转换器,比如引入 MappingJackson2HttpMessageConverter 便能支持对象类型返回值转换为 json 并返回给客户端

如果不使用 @ResponseBody 注解,那么就会使用 ModelAndView 保存视图路径和数据。SpringMVC 同样提供了默认的视图解析器 ViewResolver,它会根据方法返回的 url 在 tomcat内部(不经由 DispatcherServlet 转发,而是使用原生 Servlet)进行一次转发请求到对应的视图文件如 jsp。如果 url 带有前缀 forward: 就表示这是一次转发请求,比如 forward:/app/test,SpringMVC 会去掉该前缀,使用 /app/test 重新交由 DispatcherServlet 转发交由对应处理器处理。如果 url 带有前缀 redirect:,比如 redirect:/test,SpringMVC 会去掉该前缀,给客户端的响应写上重定向头以及重定向地址即 /test,客户端会重新发送请求。转发和重定向的区别在于:转发请求是同一个,重定向则每次都是新的请求。转发时由于经过 DispatcherServlet,所以每次都会新建 Model,而重定向则会自动将 Model 中的参数拼接到重定向的 url


@EnableWebMvc

使用 @EnableWebMvc 注解可以帮助我们在代码中自定义 SpringMVC 配置,比如添加拦截器。使用 @EnableWebMvc 注解的配置类必须继承 WebMvcConfigurer 类

需要注意的是,@EnableWebMvc 是较旧的配置 SpringMVC 的方式。如果使用 SpringBoot,它提供了自动配置,通常不需要显式使用 @EnableWebMvc,只需要在配置文件配置即可

@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {

    @Autowired
    private BeforMethodInteceptor beforMethodInteceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {    
        // 注册自定义拦截器,添加拦截路径和排除拦截路径
        registry.addInterceptor(beforMethodInteceptor) //添加拦截器
                   .addPathPatterns("/**") //添加拦截路径
                   .excludePathPatterns(  //添加排除拦截路径
                           "/index",
                           "/login",
                           ...
                           );
        super.addInterceptors(registry);        
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // 配置视图解析器
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("");
        viewResolver.setSuffix(".html");
        viewResolver.setCache(false);
        viewResolver.setContentType("text/html;charset=UTF-8");
        viewResolver.setOrder(0);        
        registry.viewResolver(viewResolver);
        super.configureViewResolvers(registry);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 定义静态资源位置和 URL 映射规则
        // 例如,将所有以 /static/ 开头的 URL 映射到 /resources/ 目录下的静态资源
        registry.addResourceHandler("/static/**")
                .addResourceLocations("/resources/");
    }

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 添加 JSON 消息转换器
        converters.add(new MappingJackson2HttpMessageConverter());
    }

   @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 跨域配置 
        registry.addMapping("/**")  // 配置允许跨域的路径
                .allowedOrigins("*")  // 配置允许访问的跨域资源的请求域名
                .allowedMethods("PUT,POST,GET,DELETE,OPTIONS")  // 配置允许访问该跨域资源服务器的请求方法
                .allowedHeaders("*"); // 配置允许请求 header 的访问
        super.addCorsMappings(registry);
    }
}

@EnableWebMvc 注解导入了 DelegatingWebMvcConfiguration 配置类,该类会将所有 WebMvcConfigurer 接口的实现类找到并保存起来。DelegatingWebMvcConfiguration 配置类还实现了 Aware 回调接口,因此会在 Spring 容器生命周期过程中调用回调接口,从而实现自定义配置

标签:请求,SpringMVC,笔记,学习,处理器,DispatcherServlet,方法,public
From: https://www.cnblogs.com/Yee-Q/p/18431349

相关文章

  • (Lin的实施运维笔记06)解决Tomcat服务器在控制台窗口中的乱码问题
    产生乱码的根本原因就是编码和解码不一致,比较常见的编码格式有Unicode、ASCll码、GBK、UTF-8等,Tomcat控制台的乱码问题只需要把日志配置文件中的UTF-8格式改成GBK格式就行解决方法:1、找到Tomcat的安装目录下conf文件夹2、打开conf文件夹中的logging.properties文件,并搜索找......
  • 基于YOLO11/v10/v8/v5深度学习的煤矿传送带异物检测系统设计与实现【python源码+Pyqt5
    《------往期经典推荐------》一、AI应用软件开发实战专栏【链接】项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.......
  • 机器学习实战:从理论到实践的探索之旅
    机器学习实战:从理论到实践的探索之旅在当今这个数据驱动的时代,机器学习作为人工智能的一个核心分支,正以前所未有的速度改变着我们的生活和工作方式。从智能推荐系统到自动驾驶汽车,从医疗诊断到金融风险评估,机器学习技术无处不在,其强大的数据处理和模式识别能力为各行各业带......
  • 大模型时代的思考:你是否在被反向“驯化”? 大多数人都要小心陷入ChatLLMs构建的蜜糖陷
    下面的内容只是一种可能性的论述,存在不确定性,提出的目的,不是危言耸听、而是提前找到应对之法-预防,因为阅历有限,还未到35,所以存在一些不足和片面的地方,还原补充。阿里云新用户优惠引言最近我无意中读到保罗·格雷厄姆的新文章《WritesandWrite-Nots》,让我有些感触。作......
  • 【吴恩达机器学习笔记】9.1-Logistic 回归的梯度下降
    使用同步更新来执行更新的办法罗杰斯特回归的梯度下降这张图片展示了逻辑回归中的梯度下降算法。逻辑回归是一种广泛使用的分类算法,它使用一个逻辑函数来预测事件发生的概率。梯度下降是一种优化算法,用于最小化损失函数,从而找到最佳的模型参数。图片中的内容可......
  • 在很多游戏问题中规划算法表现的要比强化学习算法还好,那么为什么还要研究RL
    根据前段时间分享的对一些游戏,如《俄罗斯方块》、《贪吃蛇》、《2048》游戏上来看,可以知道一个精调好的规划算法(启发式算法),在人为给定的一些预设条件下运行,其最终的算法性能会比一般的RL算法实现的效果要好,但是为什么我们还要研究RL算法呢,那么是不是说明RL算法这种AI算法就没有太......
  • Qt 学习第 天:文件和事件
    一、创建widget对象(文件)二、设计ui界面放一个label标签上去,设置成box就可以显示边框了三、新建Mylabel类四、提升ui界面的label标签为Mylabel五、修改mylabel.h,mylabel.cpp#ifndefMYLABEL_H#defineMYLABEL_H#include<QLabel>classMylabel:publicQLabel{......
  • StarUML建模工具安装学习与汉化最新零基础详细教程【一键式下载】(适用于Windows、MacO
    StarUML破解安装下载教程前言:StarUML破解与汉化安装下载教程,仅供学习研究和交流使用,禁止作为商业用途或其他非法用途!仓库作者:X1a0He,经仓库作者授权使用。目录StarUML破解安装下载教程1.下载准备1.1一键式准备【懒人准备】1.2学习式准备1.2.1学习准备2.window......
  • 2024-2025-1 20241413 《计算机基础与程序设计》第七周学习总结
    这个作业属于哪个课程https://edu.cnblogs.com/campus/besti/2024-2025-1-CFAP这个作业要求在哪里https://www.cnblogs.com/rocedu/p/9577842.html#WEEK07作业目标数组与链表基于数组和基于链表实现数据结构无序表与有序表树图子程序与参数--------作业......
  • c++学习:封装继承多态
    目录封装封装的定义封装的好处封装的实例继承继承的定义继承的好处继承的实例多态多态的定义多态的好处多态的实例封装封装的定义封装是面向对象编程(OOP)中的一个核心概念,它指的是将数据(属性)和操作这些数据的函数(方法)结合在一起的过程,以此来模拟现实世界中的实......