SpringMVC

一、SpringMVC简介

(1)什么是MVC

  1. MVC 定义 :是一种软件架构思想,将软件按模型Model)、视图View)、控制器Controller)划分
  2. 各层含义及作用
    1. 模型层(M) :即 JavaBean,用于处理数据。包含实体类 Bean (存储业务数据,如 Student、User)和业务处理 Bean(Service 或 Dao 对象,处理业务逻辑与数据访问)
    2. 视图层(V) :工程中的 htmljsp 等页面,用于与用户交互、展示数据
    3. 控制层(C) :工程中的 servlet ,负责接收请求和响应浏览器
  3. 工作流程 :(1)用户通过视图层发请求到服务器,(2)控制器接收请求,(3)调用模型层处理,(4)处理结果返回控制器,(5)控制器找对应视图,渲染数据后响应给浏览器

(2)什么是SpringMVC

  1. SpringMVC 与 Spring 关系:SpringMVC 是 Spring 的后续子项目
  2. SpringMVC 用途:是 Spring 为表述层开发提供的完整解决方案
  3. 业界选择:表述层框架历经 Strust、WebWork、Strust2 等更迭后,业界普遍选 SpringMVC 作为 Java EE 项目表述层开发首选
  4. 三层架构说明 :包含表述层(含前台页面和后台 servlet)、业务逻辑层、数据访问层

(3)SpringMVC的特点

  1. 与 Spring 集成佳 :是 Spring 家族原生产品,++能和 IOC 容器等无缝对接++
  2. 请求响应处理++基于原生 Servlet++,用 ++DispatcherServlet++ 统一处理请求和响应
  3. 功能覆盖全 :++全方位覆盖表述层各细分问题++,提供全面解决方案
  4. 开发效率高 :++代码简洁++,可大幅提升开发效率
  5. 组件使用灵活 :内部++组件化程度高++,组件即插即用,按需配置
  6. 性能表现优:性能卓越,适合大型、超大型互联网项目

二、HelloWorld

(1)开发环境

(2)创建maven模块

  1. 添加web模块

  2. 打包方式:war

  3. 引入依赖:

    XML 复制代码
    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.atguigu.mvc</groupId>
        <artifactId>springMVC-demo1</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>war</packaging>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <!-- SpringMVC -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-webmvc</artifactId>
                <version>6.0.11</version>
            </dependency>
            <!-- 日志 -->
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>1.4.8</version>
            </dependency>
            <!-- ServletAPI -->
            <dependency>
                <groupId>jakarta.servlet</groupId>
                <artifactId>jakarta.servlet-api</artifactId>
                <version>6.0.0</version>
                <scope>provided</scope>
            </dependency>
            <!-- Spring5和Thymeleaf整合包 -->
            <dependency>
                <groupId>org.thymeleaf</groupId>
                <artifactId>thymeleaf-spring6</artifactId>
                <version>3.1.2.RELEASE</version>
            </dependency>
        </dependencies>
    </project>
  4. :由于 ++Maven 的传递性++,我们不必将所有需要的包全部配置依赖,而是++配置最顶端的依赖,其他靠传递性导入++

  5. 上述依赖配置的说明

(3)配置web.xml

  1. 默认配置方式

    XML 复制代码
    <!-- 配置SpringMVC的前端控制器,对浏览器发送的请求统一进行处理 -->
    <servlet>
        <servlet-name>springMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>springMVC</servlet-name>
        <!--
            设置springMVC的核心控制器所能处理的请求的请求路径
            /所匹配的请求可以是/login或.html或.js或.css方式的请求路径
            但是/不能匹配.jsp请求路径的请求
        -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
  2. 扩展配置方式

    XML 复制代码
    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
    	version="4.0">
    
    	<!-- 配置SpringMVC的前端控制器,对浏览器发送的请求统一进行处理 -->
    	<servlet>
    		<servlet-name>springMVC</servlet-name>
    		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    		<!-- 通过初始化参数指定SpringMVC配置文件的位置和名称 -->
    		<init-param>
    			<!-- contextConfigLocation为固定值 -->
    			<param-name>contextConfigLocation</param-name>
    			<!-- 使用classpath:表示从类路径查找配置文件,例如maven工程中的src/main/resources -->
    			<param-value>classpath:springMVC.xml</param-value>
    		</init-param>
    		<!--
                 作为框架的核心组件,在启动过程中有大量的初始化操作要做
                而这些操作放在第一次请求时才执行会严重影响访问速度
                因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时
            -->
    		<load-on-startup>1</load-on-startup>
    	</servlet>
    	<servlet-mapping>
    		<servlet-name>springMVC</servlet-name>
    		<!--
                设置springMVC的核心控制器所能处理的请求的请求路径
                /所匹配的请求可以是/login或.html或.js或.css方式的请求路径
                但是/不能匹配.jsp请求路径的请求
            -->
    		<url-pattern>/</url-pattern>
    	</servlet-mapping>
    </web-app>

(4)创建请求控制器

  1. 前端控制器统一拦截浏览器请求,根据 <url-pattern> 配置的 / 或 /* 拦截相应请求,它再依据请求具体内容,将不同请求交给处理具体请求的类,即请求控制器。不同请求处理过程有别,所以需创建这些请求控制器

  2. 请求控制器里每个处理请求的方法称为控制器方法

  3. Spring MVC 的控制器由 POJO(普通 Java 类)担任,要用 @Controller 注解将其标识为控制层组件,交由 Spring 的 IoC 容器管理,Spring MVC 才能识别该控制器

    java 复制代码
    @Controller
    public class HelloController {
        
    }

(5)创建SpringMVC的配置文件

  1. 配置文件怎么写:

    XML 复制代码
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
                               http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd 
                               http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
                              ">
    
        <!-- 自动扫描包 -->
        <context:component-scan base-package="com.atguigu.mvc.controller"/>
    
        <!-- 配置Thymeleaf视图解析器 -->
        <bean id="viewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
            <property name="order" value="1"/>
            <property name="characterEncoding" value="UTF-8"/>
            <property name="templateEngine">
                <bean class="org.thymeleaf.spring6.SpringTemplateEngine">
                    <property name="templateResolver">
                        <bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
    
                            <!-- 视图前缀 -->
                            <property name="prefix" value="/WEB-INF/templates/"/>
    
                            <!-- 视图后缀 -->
                            <property name="suffix" value=".html"/>
                            <property name="templateMode" value="HTML"/>
                            <property name="characterEncoding" value="UTF-8" />
                        </bean>
                    </property>
                </bean>
            </property>
        </bean>
    
        <!--
           处理静态资源,例如html、js、css、jpg
          若只设置该标签,则只能访问静态资源,其他请求则无法访问
          此时必须设置<mvc:annotation-driven/>解决问题
         -->
        <mvc:default-servlet-handler/>
    
        <!-- 开启mvc注解驱动 -->
        <mvc:annotation-driven>
            <mvc:message-converters>
                <!-- 处理响应中文内容乱码 -->
                <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                    <property name="defaultCharset" value="UTF-8" />
                    <property name="supportedMediaTypes">
                        <list>
                            <value>text/html</value>
                            <value>application/json</value>
                        </list>
                    </property>
                </bean>
            </mvc:message-converters>
        </mvc:annotation-driven>
    </beans>
  2. 怎么配置Thymeleaf视图解析器

  3. 关于SpringMVC里的注解驱动

(6)测试HelloWorld

  1. 实现对首页的访问

    java 复制代码
    package com.atguigu.mvc.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    public class HelloController {
        // @RequestMapping注解:处理请求和控制器方法之间的映射关系
        // @RequestMapping注解的value属性可以通过请求地址匹配请求,/表示的当前工程的上下文路径
        // localhost:8080/springMVC/
        @RequestMapping("/")
        public String index() {
            //设置视图名称
            return "index";
        }
    }
  2. 在主页index.html中设置超链接

    html 复制代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首页</title>
    </head>
    <body>
        <h1>首页</h1>
        <a th:href="@{/hello}">HelloWorld</a><br/>
    </body>
    </html>
  3. 在请求控制器中创建处理请求的方法

    java 复制代码
    @RequestMapping("/hello")
    public String HelloWorld() {
        return "target";
    }

(7)总结

浏览器发请求,若符合 DispatcherServlet 的 url - pattern 则被其处理。DispatcherServlet(在web.xml中配置的) 读取 SpringMVC 配置文件,经组件扫描(context:component-scan)找控制器(请求控制器上面有@Controller注解),将请求地址++与 @RequestMapping 的 value 匹配++,匹配成功则对应方法处理请求。该方法返回视图名 ,由视图解析器加前后缀成路径,用 Thymeleaf 渲染后转发到对应页面

三、@RequestMapping注解

(1)@RequestMapping注解的功能

  1. @RequestMapping 注解作用:将请求与处理请求的控制器方法关联,建立映射关系
  2. SpringMVC 请求处理:接收到指定请求后,依据映射关系找到对应控制器方法处理请求

(2)@RequestMapping注解的位置

  1. @RequestMapping标识一个类:设置映射请求的请求路径的初始信息

  2. @RequestMapping标识一个方法 :设置映射请求请求路径的具体信息

    java 复制代码
    @Controller
    @RequestMapping("/test")
    public class RequestMappingController {
    
    	//此时请求映射所映射的请求的请求路径为:/test/testRequestMapping
        @RequestMapping("/testRequestMapping")
        public String testRequestMapping(){
            return "success";
        }
    
    }

(3)@RequestMapping注解的value属性

  1. @RequestMapping注解的value属性通过请求的请求地址匹配请求映射
  2. @RequestMapping注解的value属性是一个字符串类型的数组,++表示该请求映射能够匹配多个请求地址所对应的请求++
  3. @RequestMapping注解的value属性必须设置,至少通过请求地址匹配请求映射

(4)@RequestMapping注解的method属性

  1. @RequestMapping注解的method属性++通过请求的请求方式 (get或post)匹配请求映射++
  2. @RequestMapping注解的method属性是一个RequestMethod类型的数组,++表示该请求映射能够匹配多种请求方式的请求++
  3. 若当前请求的请求地址满足请求映射的value属性,但是请求方式不满足method属性,则浏览器报错405:++Request method 'POST' not supported++
  4. @RequestMapping 派生注解:Spring MVC 为处理指定请求方式提供了 ++@RequestMapping 的派生注解++,包括 @GetMapping (处理GET 请求)、@PostMapping (处理 POST 请求)、@PutMapping (处理 PUT 请求)、@DeleteMapping (处理 DELETE 请求)
  5. 常用请求方式及浏览器支持情况:常用请求方式有 GET、POST、PUT、DELETE
  6. 但++目前浏览器仅支持 GETPOST++。若在 form 表单提交时将 method 设置为 ++PUT 或 DELETE++ 等其他请求方式字符串,会++按默认的 GET 请求处理++
  7. 发送 PUT 和 DELETE 请求方法:若要发送 PUT 和 DELETE 请求,需借助 Spring 提供的过滤器 HiddenHttpMethodFilter,在 RESTful 部分会详细讲解

(5)@RequestMapping注解的params属性(了解)

  1. @RequestMappingparams 属性作用:++通过请求的请求参数匹配请求映射++
  2. params 属性类型 :是一个字符串类型的数组
  3. 匹配关系表达式
    1. "param" :匹配的++请求必须携带++ param 请求参数
    2. "!param":匹配的++请求必须不能携带++ param 请求参数
    3. "param=value":匹配的请求必须携带 param 请求参数且其值为 value
    4. "param!=value":匹配的请求必须携带 param 请求参数且其值不为 value

(6)@RequestMapping注解的headers属性(了解)

  1. @RequestMappingheaders 属性作用:通过请求的请求头信息匹配请求映射
  2. headers 属性类型:是一个字符串类型的数组
  3. 匹配关系表达式
    1. "header":匹配的请求必须携带 header 请求头信息
    2. "!header":匹配的请求必须不能携带 header 请求头信息
    3. "header=value":匹配的请求必须携带 header 请求头信息且其值为 value
    4. "header!=value":匹配的请求必须携带 header 请求头信息且其值不为 value
  4. 请求匹配情况处理 :若请求满足 @RequestMapping 的 value 和 method 属性,但不满足 headers 属性,页面显示 404 错误(资源未找到)
  5. 请求参数的位置

(7)SpringMVC支持ant风格的路径

  1. :表示任意的单个字符
  2. * :表示任意的0个或多个字符
  3. ** :表示任意的一层或多层目录
  4. 注意 :在使用**时,只能使用/**/xxx的方式

(8)SpringMVC支持路径中的占位符(重点)

  1. 请求方式对比 :原始请求方式是将数据以参数形式附在 URL 后,如 /deleteUser**?id=1** ;++RESTful 风格则将数据嵌入路径++,如 /deleteUser**/1**
  2. Spring MVC 占位符用途 :在 Spring MVC 里,++路径占位符常用于 RESTful 风格++,用于处理请求路径中传输的数据
  3. 占位符表示与数据赋值 :在 @RequestMapping 注解的 value 属性中++用 {xxx} 表示传输的数据++,再通过 @PathVariable 注解++占位符表示的数据 赋给控制器方法的形参++
  4. 举例

四、SpringMVC获取请求参数

(1)通过servletAPI获取

当把 HttpServletRequest ++作为控制器方法的形参++时,该 HttpServletRequest 类型的参数代表的是封装了++当前请求报文的对象++

java 复制代码
@RequestMapping("/testParam")
public String testParam(HttpServletRequest request){
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    System.out.println("username:"+username+",password:"+password);
    return "success";
}

(2)通过控制器方法的形参获取请求参数

  1. 当浏览器发送请求并匹配到对应的请求映射后,若++控制器方法的形参中存在与请求参数同名的参数++,DispatcherServlet 会自动 将请求中的参数值赋值给这些同名的方法形参
  2. 举例
  3. 多同名请求参数的接收方式 :当++请求的参数中有多个同名请求参数++时,可在控制器方法形参中用字符串数组字符串类型来接收
  4. 字符串数组形参接收结果 :使用++字符串数组形参++,数组会包含每个同名请求参数的数据
  5. 字符串形参接收结果 :使用++字符串形参++,该参数值是各同名请求参数数据用逗号拼接的结果

(3)@RequestParam

  1. @RequestParam 作用:用于建立请求参数和控制器方法形参的映射关系
  2. @RequestParam 属性
    1. value :指定为形参赋值的请求参数名
    2. required :设置请求参数是否必须传输,默认 true。++true 时必须传 该参数,未传无 defaultValue 则报 400++;++false 时可不传,未传则形参为 null++
    3. defaultValue :无论 required 取值如何,当指定请求参数++未传输++或++值为空字符串++时,用此默认值为形参赋值

(4)@RequestHeader

  1. @RequestHeader 作用 :用于建立请求头信息与控制器方法形参之间的映射关系
  2. @RequestHeader 属性 :包含 value、required、defaultValue 三个属性,且这三个属性的用法与 @RequestParam 注解的对应属性用法相同

(5)@CookieValue

  1. @CookieValue 作用 :用于建立 cookie 数据控制器方法形参之间的映射关系

  2. @CookieValue 属性 :具备 value、required、defaultValue 三个属性,且这三个属性的用法和 @RequestParam 注解中对应属性的用法一致

  3. 关于Cookie

    复制代码
    Set - Cookie: <name>=<value>[; <Max - Age>=<age>] [; Expires=<date>] [; Domain=<domain_name>] [; Path=<some_path>] [; Secure] [; HttpOnly] [; SameSite=<mode>]
    复制代码
    Set - Cookie: user_id=abc123; Max - Age=3600; Domain=.example.com; Path=/; Secure; HttpOnly; SameSite=Strict

(6)通过POJO获取请求参数

  1. 控制器方法形参设为实体类类型,若浏览器请求参数名实体类属性名一致,请求参数会为对应属性赋值
  2. 举例
  3. 原理

(7)解决获取请求参数的乱码问题

解决++获取请求参数的乱码++问题,可以使用SpringMVC提供的编码过滤器CharacterEncodingFilter,但是必须在web.xml中进行注册

XML 复制代码
<!--配置springMVC的编码过滤器-->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

五、域对象共享数据

(1)使用servletAPI向request域对象共享数据

java 复制代码
@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request){
    request.setAttribute("testScope", "hello,servletAPI");
    return "success";
}

(2)使用ModelAndView向request域对象共享数据

java 复制代码
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
    /**
     * ModelAndView有Model和View的功能
     * Model主要用于向请求域共享数据
     * View主要用于设置视图,实现页面跳转
     */
    ModelAndView mav = new ModelAndView();
    //向请求域共享数据
    mav.addObject("testScope", "hello,ModelAndView");
    //设置视图,实现页面跳转
    mav.setViewName("success");
    return mav;
}

(3)使用Model向request域对象共享数据

java 复制代码
@RequestMapping("/testModel")
public String testModel(Model model){
    model.addAttribute("testScope", "hello,Model");
    return "success";
}

(4)使用map向request域对象共享数据

java 复制代码
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map){
    map.put("testScope", "hello,Map");
    return "success";
}

(5)使用ModelMap向request域对象共享数据

java 复制代码
@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap){
    modelMap.addAttribute("testScope", "hello,ModelMap");
    return "success";
}

(6)Model、ModelMap、Map的关系

(7)向session域共享数据

java 复制代码
@RequestMapping("/testSession")
public String testSession(HttpSession session){
    session.setAttribute("testSessionScope", "hello,session");
    return "success";
}

(8)向application域共享数据

java 复制代码
@RequestMapping("/testApplication")
public String testApplication(HttpSession session){
	ServletContext application = session.getServletContext();
    application.setAttribute("testApplicationScope", "hello,application");
    return "success";
}

六、SpringMVC的视图

  1. SpringMVC 中视图实现 View 接口,作用是渲染数据,把模型 Model 里的数据展示给用户
  2. SpringMVC 视图种类多,默认包含转发视图和重定向视图
  3. 工程引入 jstl 依赖时,转发视图会自动转为 JstlView
  4. 使用 Thymeleaf 视图技术,且在 SpringMVC 配置文件中配置其视图解析器后,解析得到的是 ThymeleafView

(1)ThymeleafView

控制器方法++设置无任何前缀的视图名称++时,该名称会++由 SpringMVC 配置文件里的视图解析器解析++,解析时将视图名称与视图前缀后缀拼接成最终路径,再通过转发 方式实现页面跳转

java 复制代码
@RequestMapping("/testHello")
public String testHello(){
    return "hello";
}

(2)转发视图

  1. SpringMVC 中++默认转发视图**++是 InternalResourceView
  2. SpringMVC 创建转发视图的情况:控制器方法设置的视图名称以 "forward:" 为前缀时,创建 InternalResourceView 视图;该视图名称不经过 SpringMVC 配置文件中视图解析器解析,去掉 "forward:" 前缀,剩余部分作为最终路径以转发方式跳转

(3)重定向视图

  1. SpringMVC 默认的重定向视图是 RedirectView
  2. 若控制器方法设置的视图名称以 "redirect:" 为前缀,会创建 RedirectView 视图
  3. 该视图名称不经过 SpringMVC 配置文件中视图解析器解析,去掉 "redirect:" 前缀后,剩余部分作为最终路径进行重定向跳转
  4. :重定向视图在解析时,会先将redirect:前缀去掉,然后会++判断剩余部分是否以/开头 ,若是则会自动拼接上下文路径**++
  5. 转发视图重定向视图

(4)视图控制器view-controller

  1. 当控制器方法中,++仅仅用来实现页面跳转++,即只需要设置视图名称时 ,可以++处理器方法 使用view-controller标签进行表示++
XML 复制代码
<!--
	path:设置处理的请求地址
	view-name:设置请求地址所对应的视图名称
-->
<mvc:view-controller path="/testView" view-name="success"></mvc:view-controller>
  1. 在 SpringMVC 里,若设置了++任意一个 view - controller++,++其他控制器中 通过注解* 配置的请求映射就会全部失效*++。为了让这些基于注解的请求映射正常工作,需要在 SpringMVC 的核心配置文件里添加**<mvc:annotation-driven />**标签来开启 MVC 注解驱动

七、RESTFul

(1)RESTFul简介

  1. REST 定义 :REST 即 Representational State Transfer,表现层资源状态转移
  2. 资源
    1. 是看待服务器的一种方式,服务器由众多离散资源构成,资源是服务器上可命名的抽象概念
    2. 不仅可代表文件系统中的文件、数据库中的表等具体事物,可按需设计得非常抽象
    3. 以名词为核心组织,一个资源可由一个或多个 URI 标识,URI 是资源名称和 Web 地址,客户端通过 URI 与资源交互
  3. 资源的表述
    1. 是对资源在特定时刻状态的描述,可在客户端 - 服务器端间转移
    2. 有多种格式,如 HTML、XML、JSON、纯文本、图片、视频、音频等,其格式可通过协商机制确定,请求和响应方向的表述通常格式不同
  4. 状态转移
  5. 资源的状态与资源的表示形式

(2)RESTFul的实现

  1. HTTP 操作动词与对应操作 :在 HTTP 协议中,GETPOSTPUTDELETE 四个动词分别对应获取资源新建资源更新资源删除资源四种基本操作
  2. REST 风格 URL 设计 :++REST 风格++提倡 URL 地址采用统一风格设计,单词间用斜杠分隔,不使用问号键值对携带请求参数,而是++将数据作为 URL 地址的一部分++,以确保风格一致
  3. 对比

(3)HiddenHttpMethodFilter

  1. 问题背景 :因浏览器仅支持 GET 和 POST 请求,++需解决发送 PUTDELETE请求的问题++

  2. 解决方案 :SpringMVC 提供 HiddenHttpMethodFilter,可++将 POST 请求转换为 DELETE 或 PUT 请求++

  3. 处理条件

    1. ++请求方式必须为 POST++
    2. 必须传输请求参数 _method
  4. 转换规则 :满足条件时,HiddenHttpMethodFilter将请求方式转换为请求参数 _method 的值,该值即为最终请求方式

  5. 在web.xml中注册HiddenHttpMethodFilter

    XML 复制代码
    <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
  6. 注:

    1. 目前为止,SpringMVC中提供了两个过滤器:CharacterEncodingFilterHiddenHttpMethodFilter
    2. 在web.xml中注册时,必须先注册CharacterEncodingFilter,再注册HiddenHttpMethodFilter
  7. 举例

八、RESTFul案例

(1)准备工作

  1. 和传统 CRUD 一样,实现对员工信息的增删改查

  2. 准备实体类:

    java 复制代码
    package com.atguigu.mvc.bean;
    
    public class Employee {
    
       private Integer id;
       private String lastName;
    
       private String email;
       //1 male, 0 female
       private Integer gender;
       
       public Integer getId() {
          return id;
       }
    
       public void setId(Integer id) {
          this.id = id;
       }
    
       public String getLastName() {
          return lastName;
       }
    
       public void setLastName(String lastName) {
          this.lastName = lastName;
       }
    
       public String getEmail() {
          return email;
       }
    
       public void setEmail(String email) {
          this.email = email;
       }
    
       public Integer getGender() {
          return gender;
       }
    
       public void setGender(Integer gender) {
          this.gender = gender;
       }
    
       public Employee(Integer id, String lastName, String email, Integer gender) {
          super();
          this.id = id;
          this.lastName = lastName;
          this.email = email;
          this.gender = gender;
       }
    
       public Employee() {
       }
    }
  3. 准备DAO模拟数据:

    java 复制代码
    package com.atguigu.mvc.dao;
    
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    import com.atguigu.mvc.bean.Employee;
    import org.springframework.stereotype.Repository;
    
    
    @Repository
    public class EmployeeDao {
    
       private static Map<Integer, Employee> employees = null;
       
       static{
          employees = new HashMap<Integer, Employee>();
    
          employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1));
          employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1));
          employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0));
          employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0));
          employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1));
       }
       
       private static Integer initId = 1006;
       
       public void save(Employee employee){
          if(employee.getId() == null){
             employee.setId(initId++);
          }
          employees.put(employee.getId(), employee);
       }
       
       public Collection<Employee> getAll(){
          return employees.values();
       }
       
       public Employee get(Integer id){
          return employees.get(id);
       }
       
       public void delete(Integer id){
          employees.remove(id);
       }
    }

(2)功能清单

(3)具体功能:访问首页

  1. 配置 view-controller

    XML 复制代码
    <mvc:view-controller path="/" view-name="index"/>
  2. 创建页面

    html 复制代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8" >
        <title>Title</title>
    </head>
    <body>
    <h1>首页</h1>
    <a th:href="@{/employee}">访问员工信息</a>
    </body>
    </html>

(4)具体功能:查询所有员工数据

  1. 控制器方法:

    java 复制代码
    @RequestMapping(value = "/employee", method = RequestMethod.GET)
    public String getEmployeeList(Model model){
        Collection<Employee> employeeList = employeeDao.getAll();
        model.addAttribute("employeeList", employeeList);
        return "employee_list";
    }
  2. 创建employee_list.html:

    html 复制代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Employee Info</title>
        <script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
    </head>
    <body>
    
        <table border="1" cellpadding="0" cellspacing="0" style="text-align: center;" id="dataTable">
            <tr>
                <th colspan="5">Employee Info</th>
            </tr>
            <tr>
                <th>id</th>
                <th>lastName</th>
                <th>email</th>
                <th>gender</th>
                <th>options(<a th:href="@{/toAdd}">add</a>)</th>
            </tr>
            <tr th:each="employee : ${employeeList}">
                <td th:text="${employee.id}"></td>
                <td th:text="${employee.lastName}"></td>
                <td th:text="${employee.email}"></td>
                <td th:text="${employee.gender}"></td>
                <td>
                    <a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${employee.id}}">delete</a>
                    <a th:href="@{'/employee/'+${employee.id}}">update</a>
                </td>
            </tr>
        </table>
    </body>
    </html>

(5)具体功能:删除

  1. 创建处理delete请求方式的表单:

    html 复制代码
    <!-- 作用:通过超链接控制表单的提交,将post请求转换为delete请求 -->
    <form id="delete_form" method="post">
        <!-- HiddenHttpMethodFilter要求:必须传输_method请求参数,并且值为最终的请求方式 -->
        <input type="hidden" name="_method" value="delete"/>
    </form>
  2. 删除超链接绑定点击事件

    1. 引入vue.js

      html 复制代码
      <script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
    2. 删除超链接

      html 复制代码
      <a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${employee.id}}">delete</a>
    3. 通过vue处理点击事件

      html 复制代码
      <script type="text/javascript">
          var vue = new Vue({
              el:"#dataTable",
              methods:{
                  //event表示当前事件
                  deleteEmployee:function (event) {
                      //通过id获取表单标签
                      var delete_form = document.getElementById("delete_form");
                      //将触发事件的超链接的href属性为表单的action属性赋值
                      delete_form.action = event.target.href;
                      //提交表单
                      delete_form.submit();
                      //阻止超链接的默认跳转行为
                      event.preventDefault();
                  }
              }
          });
      </script>
  3. 控制器方法:

    java 复制代码
    @RequestMapping(value = "/employee/{id}", method = RequestMethod.DELETE)
    public String deleteEmployee(@PathVariable("id") Integer id){
        employeeDao.delete(id);
        return "redirect:/employee";
    }

(6)具体功能:跳转到添加数据页面

  1. 配置 view-controller

    XML 复制代码
    <mvc:view-controller path="/toAdd" view-name="employee_add"></mvc:view-controller>
  2. 创建employee_add.html

    html 复制代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Add Employee</title>
    </head>
    <body>
    
    <form th:action="@{/employee}" method="post">
        lastName:<input type="text" name="lastName"><br>
        email:<input type="text" name="email"><br>
        gender:<input type="radio" name="gender" value="1">male
        <input type="radio" name="gender" value="0">female<br>
        <input type="submit" value="add"><br>
    </form>
    
    </body>
    </html>

(7)具体功能:执行保存

java 复制代码
@RequestMapping(value = "/employee", method = RequestMethod.POST)
public String addEmployee(Employee employee){
    employeeDao.save(employee);
    return "redirect:/employee";
}

(8)具体功能:跳转到更新数据页面

  1. 修改超链接:

    html 复制代码
    <a th:href="@{'/employee/'+${employee.id}}">update</a>
  2. 控制器方法:

    java 复制代码
    @RequestMapping(value = "/employee/{id}", method = RequestMethod.GET)
    public String getEmployeeById(@PathVariable("id") Integer id, Model model){
        Employee employee = employeeDao.get(id);
        model.addAttribute("employee", employee);
        return "employee_update";
    }
  3. 创建employee_update.html

    html 复制代码
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Update Employee</title>
    </head>
    <body>
    
    <form th:action="@{/employee}" method="post">
        <input type="hidden" name="_method" value="put">
        <input type="hidden" name="id" th:value="${employee.id}">
        lastName:<input type="text" name="lastName" th:value="${employee.lastName}"><br>
        email:<input type="text" name="email" th:value="${employee.email}"><br>
        <!--
            th:field="${employee.gender}"可用于单选框或复选框的回显
            若单选框的value和employee.gender的值一致,则添加checked="checked"属性
        -->
        gender:<input type="radio" name="gender" value="1" th:field="${employee.gender}">male
        <input type="radio" name="gender" value="0" th:field="${employee.gender}">female<br>
        <input type="submit" value="update"><br>
    </form>
    
    </body>
    </html>

(9)具体功能:执行更新

java 复制代码
@RequestMapping(value = "/employee", method = RequestMethod.PUT)
public String updateEmployee(Employee employee){
    employeeDao.save(employee);
    return "redirect:/employee";
}

九、HttpMessageConventer

  1. HttpMessageConverter,报文信息转换器 ,将请求报文转换为Java对象 ,或将Java对象转换为响应报文
  2. HttpMessageConverter提供了两个注解和两个类型:@RequestBody@ResponseBodyRequestEntityResponseEntity

(1)@RequestBody

  1. @RequestBody可以获取请求体 ,需要在控制器方法设置一个形参 ,++使用*@RequestBody**进行标识*++,当前请求的请求体就会为当前注解所标识的形参赋值

  2. 举例

    html 复制代码
    <form th:action="@{/testRequestBody}" method="post">
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        <input type="submit">
    </form>
    java 复制代码
    @RequestMapping("/testRequestBody")
    public String testRequestBody(@RequestBody String requestBody){
        System.out.println("requestBody:"+requestBody);
        return "success";
    }
  3. 输出结果:requestBody:username=admin&password=123456

(2)RequestEntity

  1. RequestEntity 是用于封装请求报文 的类型。若要使用它,需在控制器方法的形参里设置 RequestEntity 类型的参数。当请求 到来时,当前请求的请求报文会自动赋值给这个形参。之后,可通过调用该形参的 getHeaders() 方法获取请求头信息,调用 getBody() 方法获取请求体信息

  2. 使用举例

    java 复制代码
    @RequestMapping("/testRequestEntity")
    public String testRequestEntity(RequestEntity<String> requestEntity){
        System.out.println("requestHeader:"+requestEntity.getHeaders());
        System.out.println("requestBody:"+requestEntity.getBody());
        return "success";
    }
  3. 输出结果

    1. requestHeader:[host:"localhost:8080", connection:"keep-alive", content-length:"27", cache-control:"max-age=0", sec-ch-ua:"" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"", sec-ch-ua-mobile:"?0", upgrade-insecure-requests:"1", origin:"http://localhost:8080", user-agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36"]
    2. requestBody:username=admin&password=123

(3)@ResponseBody

  1. @ResponseBody用于标识一个控制器方法,可以++将该方法的返回值 直接作为响应报文的响应体响应到浏览器++

  2. 使用举例

    java 复制代码
    @RequestMapping("/testResponseBody")
    @ResponseBody
    public String testResponseBody(){
        return "success";
    }
  3. 输出结果:浏览器页面显示success

(4)SpringMVC处理json

  1. @ResponseBody处理json的步骤:

    1. a>导入jackson的依赖:

      XML 复制代码
      <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.12.1</version>
      </dependency>
    2. b>在SpringMVC的核心配置文件中开启mvc的注解驱动,此时在HandlerAdaptor中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,可以++ 响应到浏览器的Java对象转换为Json格式的字符串**++

      XML 复制代码
      <mvc:annotation-driven />
    3. c>在处理器方法上使用@ResponseBody注解进行标识

    4. d>++将Java对象 直接作为控制器方法的返回值返回,就会自动转换为Json格式的字符串**++

      java 复制代码
      @RequestMapping("/testResponseUser")
      @ResponseBody
      public User testResponseUser(){
          return new User(1001,"admin","123456",23,"男");
      }
  2. 浏览器的页面中展示的结果: {"id":1001,"username":"admin","password":"123456","age":23,"sex":"男"}

(5)SpringMVC处理ajax

  1. 请求超链接:

    XML 复制代码
    <div id="app">
    	<a th:href="@{/testAjax}" @click="testAjax">testAjax</a><br>
    </div>
  2. 通过vue和axios处理点击事件:

    javascript 复制代码
    <script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
    <script type="text/javascript" th:src="@{/static/js/axios.min.js}"></script>
    <script type="text/javascript">
        var vue = new Vue({
            el:"#app",
            methods:{
                testAjax:function (event) {
                    axios({
                        method:"post",
                        url:event.target.href,
                        params:{
                            username:"admin",
                            password:"123456"
                        }
                    }).then(function (response) {
                        alert(response.data);
                    });
                    event.preventDefault();
                }
            }
        });
    </script>
  3. 控制器方法:

    java 复制代码
    @RequestMapping("/testAjax")
    @ResponseBody
    public String testAjax(String username, String password){
        System.out.println("username:"+username+",password:"+password);
        return "hello,ajax";
    }

(6)@RestController注解

@RestController注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解

(7)ResponseBody

  1. ResponseEntity用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文
  2. 返回响应体 和返回响应报文

十、文件上传和下载

(1)文件下载

使用ResponseEntity实现下载文件的功能

java 复制代码
@RequestMapping("/testDown")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    //获取ServletContext对象
    ServletContext servletContext = session.getServletContext();
    //获取服务器中文件的真实路径
    String realPath = servletContext.getRealPath("/static/img/1.jpg");
    //创建输入流
    InputStream is = new FileInputStream(realPath);
    //创建字节数组
    byte[] bytes = new byte[is.available()];
    //将流读到字节数组中
    is.read(bytes);
    //创建HttpHeaders对象设置响应头信息
    MultiValueMap<String, String> headers = new HttpHeaders();
    //设置要下载方式以及下载文件的名字
    headers.add("Content-Disposition", "attachment;filename=1.jpg");
    //设置响应状态码
    HttpStatus statusCode = HttpStatus.OK;
    //创建ResponseEntity对象
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
    //关闭输入流
    is.close();
    return responseEntity;
}

(2)文件上传

  1. 文件上传 要求form表单的请求方式必须为post,并且添加属性enctype="multipart/form-data"
  2. SpringMVC中将上传的文件封装到 MultipartFile 对象中,通过此对象可以获取文件相关信息
  3. 上传步骤:
    1. 添加依赖:

      XML 复制代码
      <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
      <dependency>
          <groupId>commons-fileupload</groupId>
          <artifactId>commons-fileupload</artifactId>
          <version>1.3.1</version>
      </dependency>
    2. 在SpringMVC的配置文件中添加配置:

      XML 复制代码
      <!--必须通过文件解析器的解析才能将文件转换为MultipartFile对象-->
      <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>
    3. 控制器方法:

      java 复制代码
      @RequestMapping("/testUp")
      public String testUp(MultipartFile photo, HttpSession session) throws IOException {
          //获取上传的文件的文件名
          String fileName = photo.getOriginalFilename();
          //处理文件重名问题
          String hzName = fileName.substring(fileName.lastIndexOf("."));
          fileName = UUID.randomUUID().toString() + hzName;
          //获取服务器中photo目录的路径
          ServletContext servletContext = session.getServletContext();
          String photoPath = servletContext.getRealPath("photo");
          File file = new File(photoPath);
          if(!file.exists()){
              file.mkdir();
          }
          String finalPath = photoPath + File.separator + fileName;
          //实现上传功能
          photo.transferTo(new File(finalPath));
          return "success";
      }

十一、拦截器

(1)拦截器配置

  1. SpringMVC中的拦截器用于++拦截控制器方法的执行++

  2. SpringMVC中的++拦截器需要实现HandlerInterceptor++

  3. SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置:

    XML 复制代码
    <bean class="com.atguigu.interceptor.FirstInterceptor"></bean>
    <ref bean="firstInterceptor"></ref>
    <!-- 以上两种配置方式都是对DispatcherServlet所处理的所有的请求进行拦截 -->
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/testRequestEntity"/>
        <ref bean="firstInterceptor"></ref>
    </mvc:interceptor>
    <!-- 
    	以上配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求,通过mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求
    -->
  4. 关于/*和/**

(2)拦截器的三个抽象方法

Spring MVC 拦截器三个抽象方法梳理如下:

  1. preHandle :在++控制器方法执行前++调用,返回 boolean 值控制请求。返回 true 放行请求并调用控制器方法;返回 false 则拦截请求,不调用控制器方法
  2. postHandle :在++控制器方法执行完毕后++调用
  3. afterCompletion :在++视图模型数据*处理完成*++,视图渲染结束后调用

(3)多个拦截器的执行顺序

  1. 所有拦截器 preHandle() 都返回true
    1. 执行顺序与 SpringMVC 配置文件中的配置顺序相关
    2. preHandle() ++配置顺序执行++
    3. ++postHandle() 和 afterCompletion() 按配置反序执行++
  2. 某个 拦截器的 preHandle() 返回 false
    1. 返回 false 的拦截器及其之前拦截器的 preHandle() 会执行
    2. 所有拦截器的 postHandle() 都不执行
    3. 返回 false 的拦截器之前拦截器的 afterCompletion() 会执行

十一、异常处理器

(1)基于配置的异常处理

  1. SpringMVC提供了一个处理++控制器方法执行过程中所出现的异常++的接口:HandlerExceptionResolver

  2. HandlerExceptionResolver接口的实现类有:DefaultHandlerExceptionResolverSimpleMappingExceptionResolver

  3. SpringMVC提供了自定义的异常处理器 SimpleMappingExceptionResolver,使用方式:

    XML 复制代码
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <property name="exceptionMappings">
            <props>
            	<!--
            		properties的键表示处理器方法执行过程中出现的异常
            		properties的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面
            	-->
                <prop key="java.lang.ArithmeticException">error</prop>
            </props>
        </property>
        <!--
        	exceptionAttribute属性设置一个属性名,将出现的异常信息在请求域中进行共享
        -->
        <property name="exceptionAttribute" value="ex"></property>
    </bean>

(2)基于注解的异常处理

java 复制代码
//@ControllerAdvice将当前类标识为异常处理的组件
@ControllerAdvice
public class ExceptionController {

    //@ExceptionHandler用于设置所标识方法处理的异常
    @ExceptionHandler(ArithmeticException.class)
    //ex表示当前请求处理中出现的异常对象
    public String handleArithmeticException(Exception ex, Model model){
        model.addAttribute("ex", ex);
        return "error";
    }

}

十二、注解配置SpringMVC

使用配置类和注解代替web.xml和SpringMVC配置文件的功能

(1)创建初始化类,代替web.xml

  1. Servlet 3.0 容器配置查找机制:Servlet 3.0 环境下,容器会在类路径中查找实现 javax.servlet.ServletContainerInitializer 接口的类,用其配置 Servlet 容器

  2. Spring 的接口实现:Spring 提供了 ServletContainerInitializer 接口的实现类 SpringServletContainerInitializer,它会查找实现 WebApplicationInitializer 的类并将配置任务交给它们

  3. Spring 的便利基础实现 :Spring 3.2 引入了便利的 WebApplicationInitializer 基础实现类 AbstractAnnotationConfigDispatcherServletInitializer,当自定义类扩展它并部署到 Servlet 3.0 容器时,容器会自动发现并使用该类配置 Servlet 上下文

  4. 代码:

    java 复制代码
    public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {
    
        /**
         * 指定spring的配置类
         * @return
         */
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return new Class[]{SpringConfig.class};
        }
    
        /**
         * 指定SpringMVC的配置类
         * @return
         */
        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class[]{WebConfig.class};
        }
    
        /**
         * 指定DispatcherServlet的映射规则,即url-pattern
         * @return
         */
        @Override
        protected String[] getServletMappings() {
            return new String[]{"/"};
        }
    
        /**
         * 添加过滤器
         * @return
         */
        @Override
        protected Filter[] getServletFilters() {
            CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
            encodingFilter.setEncoding("UTF-8");
            encodingFilter.setForceRequestEncoding(true);
            HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
            return new Filter[]{encodingFilter, hiddenHttpMethodFilter};
        }
    }
  5. 说明

(2)创建SpringConfig配置类,代替spring的配置文件

java 复制代码
@Configuration
public class SpringConfig {
	//ssm整合之后,spring的配置信息写在此类中
}

(3)创建WebConfig配置类,代替SpringMVC的配置文件

java 复制代码
@Configuration
//扫描组件
@ComponentScan("com.atguigu.mvc.controller")
//开启MVC注解驱动
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    //使用默认的servlet处理静态资源
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    //配置文件上传解析器
    @Bean
    public CommonsMultipartResolver multipartResolver(){
        return new CommonsMultipartResolver();
    }

    //配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        FirstInterceptor firstInterceptor = new FirstInterceptor();
        registry.addInterceptor(firstInterceptor).addPathPatterns("/**");
    }
    
    //配置视图控制
    
    /*@Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
    }*/
    
    //配置异常映射
    /*@Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
        Properties prop = new Properties();
        prop.setProperty("java.lang.ArithmeticException", "error");
        //设置异常映射
        exceptionResolver.setExceptionMappings(prop);
        //设置共享异常信息的键
        exceptionResolver.setExceptionAttribute("ex");
        resolvers.add(exceptionResolver);
    }*/

    //配置生成模板解析器
    @Bean
    public ITemplateResolver templateResolver() {
        WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
        // ServletContextTemplateResolver需要一个ServletContext作为构造参数,可通过WebApplicationContext 的方法获得
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(
                webApplicationContext.getServletContext());
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        templateResolver.setCharacterEncoding("UTF-8");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        return templateResolver;
    }

    //生成模板引擎并为模板引擎注入模板解析器
    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

    //生成视图解析器并未解析器注入模板引擎
    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setCharacterEncoding("UTF-8");
        viewResolver.setTemplateEngine(templateEngine);
        return viewResolver;
    }


}

(4)测试功能

java 复制代码
@RequestMapping("/")
public String index(){
    return "index";
}

十三、SpringMVC的执行流程

(1)SpringMVC常用组件

  1. DispatcherServlet
    1. 含义前端控制器,不需要工程师开发,由框架提供
    2. 作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求
  2. HandlerMapping
    1. 含义处理器映射器,不需要工程师开发,由框架提供
    2. 作用:根据请求的url、method等信息查找Handler,即控制器方法
  3. Handler
    1. 含义处理器,需要工程师开发
    2. 作用:在DispatcherServlet的控制下Handler对具体的用户请求进行处理
  4. HandlerAdapter
    1. 含义处理器适配器,不需要工程师开发,由框架提供
    2. 作用:通过HandlerAdapter对处理器(控制器方法)进行执行
  5. ViewResolver
    1. 含义视图解析器,不需要工程师开发,由框架提供
    2. 作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView
  6. View
    1. 含义:视图
    2. 作用:将模型数据通过页面展示给用户

(2)DispatcherServlet初始化过程

DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度

  1. 初始化 WebApplicationContext

    java 复制代码
    protected WebApplicationContext initWebApplicationContext() {
        // 寻找上级仓库:从商场的管理处(ServletContext)那里了解一下,是否已经存在一个更大的总仓库(根 WebApplicationContext)
        WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;
    
        // 使用预分配的仓库:如果在创建商场的这个区域(DispatcherServlet)时,已经预先分配了一个仓库
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                // 检查这个仓库是否已经准备好投入使用
                if (!cwac.isActive()) { 
                    // 要是还没准备好,就把之前找到的总仓库设为它的上级仓库
                    if (cwac.getParent() == null) { 
                        cwac.setParent(rootContext);
                    }
                    // 然后对这个仓库进行整理和布置(刷新)
                    configureAndRefreshWebApplicationContext(cwac); 
                }
            }
        }
        // 查找现有仓库:如果创建时没有预先分配仓库,就在商场管理处的记录里查找一下,看是否有已经建好且可用的仓库
        if (wac == null) { 
            wac = findWebApplicationContext();
        }
        // 新建仓库:如果前面两种方式都没有找到合适的仓库,那就按照既定的规则,自己动手新建一个仓库
        if (wac == null) { 
            wac = createWebApplicationContext(rootContext);
        }
    
        // 整理仓库:如果新建的仓库还没有进行整理和布置,就手动对其进行整理
        if (!this.refreshEventReceived) { 
            synchronized (this.onRefreshMonitor) {
                onRefresh(wac);
            }
        }
    
        // 公开仓库位置:把这个仓库的位置信息(将 WebApplicationContext)存放到商场管理处的公共信息区域(ServletContext)
        if (this.publishContext) { 
            String attrName = getServletContextAttributeName();
            getServletContext().setAttribute(attrName, wac);
        }
    
        return wac;
    }
  2. 创建 WebApplicationContext

    java 复制代码
    protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
        // 检查仓库设计方案:确保要建造的仓库符合 ConfigurableWebApplicationContext 类型的设计标准
        Class<?> contextClass = getContextClass();
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException(
                "Fatal initialization error in servlet with name '" + getServletName() +
                "': custom WebApplicationContext class [" + contextClass.getName() +
                "] is not of type ConfigurableWebApplicationContext");
        }
        // 开始建造仓库:使用类似于建筑图纸自动施工的方式(反射),按照设计方案把仓库建造起来
        ConfigurableWebApplicationContext wac =
            (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    
        // 设置仓库环境和上级:为仓库设置好合适的环境,同时把之前找到的总仓库设为它的上级仓库
        wac.setEnvironment(getEnvironment()); 
        wac.setParent(parent);
        // 确定商品摆放规则:告诉仓库管理人员商品应该按照什么样的规则进行摆放
        String configLocation = getContextConfigLocation(); 
        if (configLocation != null) {
            wac.setConfigLocation(configLocation);
        }
        // 完成仓库建设:最后对仓库进行全面的整理和布置,使仓库达到可以正式投入使用的状态
        configureAndRefreshWebApplicationContext(wac); 
    
        return wac;
    }
  3. 初始化策略:

    java 复制代码
    protected void initStrategies(ApplicationContext context) {
        // 处理大型货物(文件上传)的工具
        initMultipartResolver(context); 
        // 处理不同地区顾客需求(区域解析)的人员
        initLocaleResolver(context); 
        // 负责仓库外观和风格(主题解析)的设计师
        initThemeResolver(context); 
        // 为顾客需求匹配合适商品(处理器映射)的导购员
        initHandlerMappings(context); 
        // 帮助顾客使用商品(处理器适配器)的服务员
        initHandlerAdapters(context); 
        // 处理顾客投诉和问题(异常解析)的客服人员
        initHandlerExceptionResolvers(context); 
        // 负责引导顾客找到心仪商品区域(视图解析)的引导员
        initRequestToViewNameTranslator(context); 
        // 管理仓库中商品展示区域(视图解析)的工作人员
        initViewResolvers(context); 
        // 管理临时寄存物品(FlashMap 管理)的工作人员
        initFlashMapManager(context); 
    }
  4. 总结:

(3)DispatcherServlet调用组件处理请求

  1. processRequest():

    java 复制代码
    // processRequest 方法:顾客进门的准备流程
    // 想象一个大型的购物仓库,顾客走进仓库大门,这时候仓库工作人员要做好一系列准备工作
    protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        // 工作人员在本子上记下顾客进入仓库的时间,这就好比记录请求开始的时间,方便后续统计处理时长
        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
    
        // 工作人员询问顾客平时习惯用哪种语言交流,并且记住之前其他顾客的语言偏好,以便后续恢复正常状态
        // 这就如同保存之前的语言环境,为后续处理请求提供合适的交流环境
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        LocaleContext localeContext = buildLocaleContext(request);
    
        // 工作人员记录下顾客之前提出的一些特殊要求,比如对商品摆放的特殊需求等,方便后续恢复原样
        // 这类似于保存请求属性,维持工作状态的连贯性
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
    
        // 工作人员根据仓库的规定,为顾客安排好服务流程,比如先去哪个区域挑选商品,需要经过哪些检查环节等
        // 这就像是注册异步管理器和拦截器,规划好处理请求的流程
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
    
        // 工作人员把仓库里与顾客服务相关的区域清理干净,摆放好必要的工具和资料,为正式服务顾客做准备
        // 这相当于初始化上下文持有者,提供必要的处理环境
        initContextHolders(request, localeContext, requestAttributes);
    
        try {
            // 一切准备就绪后,工作人员带着顾客进入仓库内部,开始为顾客提供具体的服务
            // 这就对应调用 doService 方法开始处理请求
            doService(request, response);
        }
        catch (ServletException | IOException ex) {
            // 如果在服务过程中出现了一些意外情况,比如仓库突然停电等,工作人员会记录下这个意外情况,并向上级报告
            // 就像处理请求时遇到异常,记录异常信息并抛出
            failureCause = ex;
            throw ex;
        }
        catch (Throwable ex) {
            failureCause = ex;
            throw new NestedServletException("Request processing failed", ex);
        }
        finally {
            // 服务结束后,工作人员把之前记录的顾客特殊要求恢复原状
            resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                // 标记这次服务已经完成
                requestAttributes.requestCompleted();
            }
            // 记录服务的结果(比如顾客是否满意等)
            logResult(request, response, failureCause, asyncManager);
            // 并通知相关部门这次服务已经结束
            publishRequestHandledEvent(request, response, startTime, failureCause);
        }
    }
  2. doService():

    java 复制代码
    // doService 方法:给顾客介绍仓库信息
    // 工作人员带着顾客进入仓库内部后,开始给顾客介绍仓库的一些基本信息,方便顾客购物
    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 工作人员再次和顾客确认他们的购物需求,比如想买什么类型的商品、预算是多少等,并记录下来
        // 这就像记录请求信息,明确顾客的基本需求
        logRequest(request);
    
        // 如果顾客是和其他团队一起进来的,工作人员会把这个团队之前的一些特殊安排记录下来,比如之前预定的商品区域等
        // 以便后续恢复,这相当于若为包含请求,保存请求属性快照
        Map<String, Object> attributesSnapshot = null;
        if (WebUtils.isIncludeRequest(request)) {
            attributesSnapshot = new HashMap<>();
            Enumeration<?> attrNames = request.getAttributeNames();
            while (attrNames.hasMoreElements()) {
                String attrName = (String) attrNames.nextElement();
                if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                    attributesSnapshot.put(attrName, request.getAttribute(attrName));
                }
            }
        }
    
        // 工作人员向顾客介绍仓库的一些重要设施和区域,比如仓库的整体布局、不同商品的存放位置、可以使用的支付方式等
        // 这就如同将 WebApplicationContext、区域解析器等重要信息设置到请求中,让顾客了解仓库的基本情况
        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
        request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
    
        // 工作人员告诉顾客仓库最近有一些临时的优惠活动信息,这些信息只在这次购物中有效
        // 并且提醒顾客如果之前有未使用完的优惠也可以一并使用,这就像处理 FlashMap 相关信息,传递请求间的数据
        if (this.flashMapManager != null) {
            FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
            if (inputFlashMap != null) {
                request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
            }
            request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
            request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
        }
    
        // 工作人员根据顾客的购物需求,在仓库的地图上为顾客规划出一条合适的购物路径,让顾客能更方便地找到他们想要的商品
        // 这类似于解析请求路径并缓存
        RequestPath requestPath = null;
        if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) {
            requestPath = ServletRequestPathUtils.parseAndCache(request);
        }
    
        try {
            // 介绍完基本信息后,工作人员把顾客交给专门负责这个区域的导购员,让导购员带着顾客去具体挑选商品
            // 这就对应调用 doDispatch 方法开始真正的分配处理
            doDispatch(request, response);
        }
        finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                // 如果顾客不是单独购物,在服务结束后,工作人员会把之前记录的团队特殊安排恢复原样
                if (attributesSnapshot != null) {
                    restoreAttributesAfterInclude(request, attributesSnapshot);
                }
                // 并且清理掉为顾客规划路径时使用的地图等工具
                if (requestPath != null) {
                    ServletRequestPathUtils.clearParsedRequestPath(request);
                }
            }
        }
    }
  3. doDispatch():

    java 复制代码
    // doDispatch 方法:为顾客分配导购员并购物
    // 导购员接到顾客后,开始带着顾客在仓库里挑选商品
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
    
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    
        try {
            ModelAndView mv = null;
            Exception dispatchException = null;
    
            try {
                // 导购员先检查顾客是否携带了大型的包裹或者货物,如果是,会安排专门的地方存放,以免影响购物
                // 这就像检查是否是文件上传请求
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);
    
                // 导购员根据顾客的购物需求,在仓库的导购方案库里找到最适合这个顾客的方案
                // 就像通过 getHandler 方法找到合适的 HandlerExecutionChain
                // 如果找不到合适的方案,导购员会告诉顾客目前没有符合他们需求的服务
                // 这就如同找不到合适的 HandlerExecutionChain 时返回错误信息
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) {
                    noHandlerFound(processedRequest, response);
                    return;
                }
    
                // 找到合适的方案后,导购员会领取一些必要的工具,比如商品目录、价格标签等,以便更好地为顾客服务
                // 这就像为 HandlerExecutionChain 配备 HandlerAdapter
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    
                // 导购员查看顾客想要购买的商品是否有最新的版本或者是否有库存更新等信息
                // 如果商品的信息没有变化,并且顾客只是简单查看商品信息,导购员可能会建议顾客可以下次再来购买
                // 这类似于处理 last - modified 头信息
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
    
                // 在正式带着顾客去挑选商品之前,导购员会和顾客再次确认一些注意事项,比如仓库的退换货政策等
                // 确保顾客了解购物流程,这就像调用拦截器的 preHandle 方法,进行购物前的检查
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }
    
                // 导购员带着顾客按照规划好的路径,在仓库里挑选商品,根据顾客的需求推荐合适的商品
                // 并帮助顾客比较不同商品的优缺点,这就像 HandlerAdapter 调用控制器方法,得到 ModelAndView
                // 即找到顾客需要的商品和对应的展示方式
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
                if (asyncManager.isConcurrentHandlingStarted()) {
                    // 如果在挑选商品的过程中,出现了一些需要等待的情况,比如某个商品需要从其他仓库调货
                    // 导购员会告诉顾客可以先去做其他事情,等商品到货了再通知他们
                    // 这就如同处理异步情况,直接返回等待后续处理
                    return;
                }
    
                // 如果顾客没有明确提出商品的展示方式,导购员会按照仓库的默认方式为顾客展示商品
                // 这就像应用默认视图名
                applyDefaultViewName(processedRequest, mv);
                // 顾客挑选完商品后,导购员会再次和顾客确认商品的信息,比如数量、价格等,确保没有遗漏或者错误
                // 这就像调用拦截器的 postHandle 方法,进行购物后的检查
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                // 如果在挑选商品的过程中出现了一些异常情况,比如商品缺货、价格错误等
                // 导购员会记录下这些异常情况,并向上级报告
                // 这就像处理请求时出现异常,记录异常信息
                dispatchException = ex;
            }
            catch (Throwable err) {
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            // 挑选完商品后,导购员会带着顾客去收银台进行结算,处理商品的付款和出库等手续
            // 这就对应调用 processDispatchResult 方法进行后续处理
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            // 如果在购物过程中出现了异常情况,导购员会在结算完成后,和顾客再次沟通
            // 解释异常情况的原因,并确保顾客满意,这就像若出现异常,触发拦截器的 afterCompletion 方法处理异常情况
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                                   new NestedServletException("Handler processing failed", err));
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                if (mappedHandler != null) {
                    // 如果出现了异步等待的情况,导购员会在顾客离开后,继续跟进商品到货的情况,并及时通知顾客
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // 如果顾客携带了大型包裹,在结算完成后,导购员会帮助顾客取回包裹,并清理存放包裹的地方
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }
  4. processDispatchResult():

    java 复制代码
    // processDispatchResult 方法:结账和送客
    // 顾客挑选完商品来到收银台后,进行最后的结账和离开仓库的流程
    private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                                       @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
                                       @Nullable Exception exception) throws Exception {
    
        boolean errorView = false;
    
        // 如果在购物过程中出现了异常情况,比如商品质量问题、价格计算错误等
        // 收银员会根据具体情况进行处理,比如为顾客更换商品、调整价格等,并给顾客一个新的购物清单
        // 这就像如果有异常,处理异常并得到 ModelAndView
        if (exception != null) {
            if (exception instanceof ModelAndViewDefiningException) {
                logger.debug("ModelAndViewDefiningException encountered", exception);
                mv = ((ModelAndViewDefiningException) exception).getModelAndView();
            }
            else {
                Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
                mv = processHandlerException(request, response, handler, exception);
                errorView = (mv != null);
            }
        }
    
        // 如果顾客有购物清单,收银员会根据清单上的商品信息进行结算,打印出购物小票,并为顾客提供发票等
        // 这就像如果有 ModelAndView,渲染视图,完成结账操作
        // 如果之前因为异常情况调整了购物清单,收银员会清除之前错误的信息
        // 这就如同若有错误视图,清除错误请求属性
        if (mv != null && !mv.wasCleared()) {
            render(mv, request, response);
            if (errorView) {
                WebUtils.clearErrorRequestAttributes(request);
            }
        }
        else {
            if (logger.isTraceEnabled()) {
                logger.trace("No view rendering, null ModelAndView returned.");
            }
        }
    
        if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            // 如果顾客在购物过程中遇到了需要等待的情况,并且现在还没有处理完
            // 收银员会告诉顾客可以先离开,等事情处理好后会通知他们
            // 这就像若为异步处理则直接返回
            return;
        }
    
        // 结账完成后,收银员会感谢顾客的光临,并引导顾客离开仓库
        // 同时,仓库的工作人员会对顾客走过的区域进行清理,为下一位顾客做好准备
        // 这就像调用拦截器的 afterCompletion 方法,送顾客离开仓库并进行收尾工作
        if (mappedHandler != null) {
            mappedHandler.triggerAfterCompletion(request, response, null);
        }
    }

(4)SpringMVC的执行流程

  1. 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获
  2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射
    1. 不存在
      1. 再判断是否配置了mvc:default-servlet-handler
      2. 如果没配置,则控制台报映射查找不到,客户端展示404错误
      3. 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误
    2. 存在则执行下面的流程
      1. 根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回
      2. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter
      3. 如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(...)方法【正向】
      4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作
      5. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象
      6. 此时将开始执行拦截器的postHandle(...)方法【逆向】
      7. 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver进行视图解析,根据Model和View,来渲染视图
      8. 渲染视图完毕执行拦截器的afterCompletion(...)方法【逆向】
      9. 将渲染结果返回给客户端
  3. 关于url和uri:
相关推荐
一只叫煤球的猫3 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9653 小时前
tcp/ip 中的多路复用
后端
bobz9653 小时前
tls ingress 简单记录
后端
皮皮林5514 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友5 小时前
什么是OpenSSL
后端·安全·程序员
bobz9655 小时前
mcp 直接操作浏览器
后端
前端小张同学7 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook7 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康8 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在8 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net