第1章 初始SpringMVC
1.1 学习本套教程前的知识储备
- JavaSE
- HTML+CSS+JavaScript
- Vue
- AJAX + axios
- Thymeleaf
- Servlet
- Maven
- Spring
1.2 什么是MVC
MVC架构模式相关课程,在老杜的JavaWeb课程中已经详细的讲解了,如果没有学过的,可以看这个视频:https://www.bilibili.com/video/BV1Z3411C7NZ
MVC是一种软件架构模式(是一种软件架构设计思想,不止Java开发中用到,其它语言也需要用到),它将应用分为三块:
- M:Model(模型)
- V:View(视图)
- C:Controller(控制器)
应用为什么要被分为三块,优点是什么?
- 低耦合,扩展能力增强
- 代码复用性增强
- 代码可维护性增强
- 高内聚,让程序员更加专注业务的开发
MVC将应用分为三块,每一块各司其职,都有自己专注的事情要做,他们属于分工协作,互相配合:
- Model:负责业务处理及数据的收集。
- View:负责数据的展示
- Controller:负责调度。它是一个调度中心,它来决定什么时候调用Model来处理业务,什么时候调用View视图来展示数据。
MVC架构模式如下所示:
MVC架构模式的描述:前端浏览器发送请求给web服务器,web服务器中的Controller接收到用户的请求,Controller负责将前端提交的数据进行封装,然后Controller调用Model来处理业务,当Model处理完业务后会返回处理之后的数据给Controller,Controller再调用View来完成数据的展示,最终将结果响应给浏览器,浏览器进行渲染展示页面。
面试题:什么是三层模型,并说一说MVC架构模式与三层模型的区别?
三层模型:
MVC 和三层模型都采用了分层结构来设计应用程序,都是降低耦合度,提高扩展力,提高组件复用性。区别在于:他们的关注点不同,三层模型更加关注业务逻辑组件的划分。
MVC架构模式关注的是整个应用程序的层次关系和分离思想。现代的开发方式大部分都是MVC架构模式结合三层模型一起用。
1.3 什么是SpringMVC
1.3.1 SpringMVC概述
SpringMVC是一个实现了MVC架构模式的Web框架,底层基于Servlet实现。
SpringMVC已经将MVC架构模式实现了,因此只要我们是基于SpringMVC框架写代码,编写的程序就是符合MVC架构模式的。(MVC的架子搭好了,我们只需要添添补补)
Spring框架中有一个子项目叫做Spring Web,Spring Web子项目当中包含很多模块,例如:
- Spring MVC
- Spring WebFlux
- Spring Web Services
- Spring Web Flow
- Spring WebSocket
- Spring Web Services Client
可见 SpringMVC是Spring Web子项目当中的一个模块。因此也可以说SpringMVC是Spring框架的一部分。
所以学习SpringMVC框架之前要先学习Spring框架中的IoC和AOP等内容。
另外,使用SpringMVC框架的时候同样也可以使用IoC和AOP。
以下就是Spring官方给出的Spring架构图,其中Web中的servlet指的就是Spring MVC:
1.3.2 SpringMVC帮我们做了什么
SpringMVC框架帮我们做了什么,与纯粹的Servlet开发有什么区别?
- 入口控制:SpringMVC框架通过DispatcherServlet作为入口控制器,负责接收请求和分发请求。而在Servlet开发中,需要自己编写Servlet程序,并在web.xml中进行配置,才能接受和处理请求。
- 在SpringMVC中,表单提交时可以自动将表单数据绑定到相应的JavaBean对象中,只需要在控制器方法的参数列表中声明该JavaBean对象即可,无需手动获取和赋值表单数据。而在纯粹的Servlet开发中,这些都是需要自己手动完成的。
- IoC容器:SpringMVC框架通过IoC容器管理对象,只需要在配置文件中进行相应的配置即可获取实例对象,而在Servlet开发中需要手动创建对象实例。
- 统一处理请求:SpringMVC框架提供了拦截器、异常处理器等统一处理请求的机制,并且可以灵活地配置这些处理器。而在Servlet开发中,需要自行编写过滤器、异常处理器等,增加了代码的复杂度和开发难度。
- 视图解析:SpringMVC框架提供了多种视图模板,如JSP、Freemarker、Velocity等,并且支持国际化、主题等特性。而在Servlet开发中需要手动处理视图层,增加了代码的复杂度。
总之,与Servlet开发相比,SpringMVC框架可以帮我们节省很多时间和精力,减少代码的复杂度,更加专注于业务开发。同时,也提供了更多的功能和扩展性,可以更好地满足企业级应用的开发需求。
1.3.3 SpringMVC框架的特点
- 轻量级:相对于其他Web框架,Spring MVC框架比较小巧轻便。(只有几个几百KB左右的Jar包文件)
- 模块化:请求处理过程被分成多个模块,以模块化的方式进行处理。
- 控制器模块:Controller
- 业务逻辑模块:Model
- 视图模块:View
- 依赖注入:Spring MVC框架利用Spring框架的依赖注入功能实现对象的管理,实现松散耦合。
- 易于扩展:提供了很多口子,允许开发者根据需要插入自己的代码,以扩展实现应用程序的特殊需求。
- Spring MVC框架允许开发人员通过自定义模块和组件来扩展和增强框架的功能。
- Spring MVC框架与其他Spring框架及第三方框架集成得非常紧密,这使得开发人员可以非常方便地集成其他框架,以获得更好的功能。
- 易于测试:支持单元测试框架,提高代码质量和可维护性。 (对SpringMVC中的Controller测试时,不需要依靠Web服务器。)
- 自动化配置:提供自动化配置,减少配置细节。
- Spring MVC框架基于约定大于配置的原则,对常用的配置约定进行自动化配置。
- 灵活性:Spring MVC框架支持多种视图技术,如JSP、FreeMarker、Thymeleaf、FreeMarker等,针对不同的视图配置不同的视图解析器即可。
1.4 本套教程相关版本
- JDK版本:Java21
- Maven版本:3.9.6
- Tomcat版本:10
- Spring版本:6.1.4
- SpringMVC版本:6.1.4
- IDEA版本:2023.3
- Thymeleaf版本:3.1.2
1.5 第一个SpringMVC程序
1.5.1 创建Maven模块
第一步:创建Empty Project,起名:springmvc。
第二步:设置springmvc工程的JDK版本:Java21。
第三步:设置maven。
第四步:创建Maven模块
第五步:将pom.xml文件中的打包方式修改为war
<groupId>com.powernode.springmvc</groupId>
<artifactId>springmvc-001</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 打包方式设置为war方式 -->
<packaging>war</packaging>
第六步:添加以下依赖
<dependencies>
<!-- Spring MVC依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.4</version>
</dependency>
<!--日志框架Logback依赖-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.3</version>
</dependency>
<!--Servlet依赖-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<!--指定该依赖的范围,provided表示这个依赖由第三方容器来提供-->
<!--打war包的时候,这个依赖不会打入war包内。因为这个依赖由其它容器来提供的。-->
<scope>provided</scope>
</dependency>
<!--Spring6和Thymeleaf整合依赖-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
</dependencies>
1.5.2 添加web支持
第一步:在main目录下创建一个webapp目录
如果创建的webapp目录没有小圆点,按下图步骤解决:
第二步:添加web.xml配置文件
注意 web.xml 文件的位置:E:\Spring MVC\code\springmvc\springmvc-001*src\main\webapp\WEB-INF\web.xml*
注意版本选择:6.0
添加web支持后的目录结构:
1.5.3 配置web.xml文件
Spring MVC是一个web框架,在javaweb中谁来负责接收请求,处理请求,以及响应呢?当然是Servlet。在SpringMVC框架中已经为我们写好了一个Servlet,它的名字叫做:DispatcherServlet,我们称其为前端控制器。既然是Servlet,那么它就需要在web.xml文件中进行配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<!--SpringMVC提供的前端控制器,SpringMVC中最核心的类-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<!-- /* 表示任何一个请求都交给DispatcherServlet来处理 -->
<!-- / 表示当请求不是xx.jsp的时候,DispatcherServlet来负责处理本次请求-->
<!-- jsp本质就是Servlet,因此如果请求是jsp的话,应该走它自己的Servlet,而不应该走DispatcherServlet -->
<!-- 因此我们的 url-pattern 使用 / -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
DispatcherServlet是SpringMVC框架为我们提供的最核心的类,它是整个SpringMVC框架的前端控制器,负责接收HTTP请求、将请求路由到处理程序、处理响应信息,最终将响应返回给客户端。DispatcherServlet是Web应用程序的主要入口点之一,它的职责包括:
- 接收客户端的HTTP请求:DispatcherServlet监听来自Web浏览器的HTTP请求,然后根据请求的URL将请求数据解析为Request对象。
- 处理请求的URL:DispatcherServlet将请求的URL(Uniform Resource Locator)与处理程序进行匹配,确定要调用哪个控制器(Controller)来处理此请求。
- 调用相应的控制器:DispatcherServlet将请求发送给找到的控制器处理,控制器将执行业务逻辑,然后返回一个模型对象(Model)。
- 渲染视图:DispatcherServlet将调用视图引擎,将模型对象呈现为用户可以查看的HTML页面。
- 返回响应给客户端:DispatcherServlet将为用户生成的响应发送回浏览器,响应可以包括表单、JSON、XML、HTML以及其它类型的数据
1.5.4 编写控制器FirstController
DispatcherServlet接收到请求之后,会根据请求路径分发到对应的Controller,Controller来负责处理请求的核心业务。在SpringMVC框架中Controller是一个普通的Java类(一个普通的POJO类,不需要继承任何类或实现任何接口),需要注意的是:POJO类要纳入IoC容器来管理,POJO类的生命周期由Spring来管理,因此要使用注解标注:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
/**
* ClassName: FirstController
* Description:
* Datetime: 2024/3/13 11:56
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class FirstController {
}
1.5.5 配置springmvc-servlet.xml文件
SpringMVC框架有它自己的配置文件,该配置文件的名字默认为:<servlet-name>-servlet.xml,默认存放的位置是WEB-INF 目录下:
<?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: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/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--Spring MVC框架的配置文件-->
<!--组件扫描-->
<context:component-scan base-package="com.powernode.springmvc.controller"/>
<!--配置视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<!--作用于视图渲染的过程中,可以设置视图渲染后输出时采用的编码字符集-->
<property name="characterEncoding" value="UTF-8"/>
<!--如果配置多个视图解析器,它来决定优先使用哪个视图解析器,它的值越小优先级越高-->
<property name="order" value="1"/>
<!--当 ThymeleafViewResolver 渲染模板时,会使用该模板引擎来解析、编译和渲染模板-->
<property name="templateEngine">
<bean class="org.thymeleaf.spring6.SpringTemplateEngine">
<!--用于指定 Thymeleaf 模板引擎使用的模板解析器。模板解析器负责根据模板位置、模板资源名称、文件编码等信息,加载模板并对其进行解析-->
<property name="templateResolver">
<bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<!--设置模板文件的位置(前缀)-->
<property name="prefix" value="/WEB-INF/templates/"/>
<!--设置模板文件后缀(后缀),Thymeleaf文件扩展名不一定是html,也可以是其他,例如txt,大部分都是html-->
<!--将来要在 xxxx.thymeleaf 文件中编写符合 Thymeleaf 语法格式的字符串:Thymeleaf 模板字符串。-->
<property name="suffix" value=".thymeleaf"/>
<!--设置模板类型,例如:HTML,TEXT,JAVASCRIPT,CSS等-->
<property name="templateMode" value="HTML"/>
<!--用于模板文件在读取和解析过程中采用的编码字符集-->
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
</beans>
在WEB-INF目录下新建springmvc-servlet.xml文件,并且提供以上配置信息。
以上配置主要两项:
- 第一项:组件扫描。spring扫描这个包中的类,将这个包中的类实例化并纳入IoC容器的管理。
- 第二项:视图解析器。视图解析器(View Resolver)的作用主要是将Controller方法返回的逻辑视图名称解析成实际的视图对象。视图解析器将解析出的视图对象返回给DispatcherServlet,并最终由DispatcherServlet将该视图对象转化为响应结果,呈现给用户。// SpringMVC框架,其中的V是View,是一个单独的组件。这个组件专门负责展示,可以灵活切换模板引擎(JSP,FreeMarket,Thymeleaf)
注意:如果采用了其它视图,请配置对应的视图解析器,例如:
- JSP的视图解析器:InternalResourceViewResolver
- FreeMarker视图解析器:FreeMarkerViewResolver
- Velocity视图解析器:VelocityViewResolver
1.5.6 提供视图
在WEB-INF目录下新建templates目录,在templates目录中新建html文件,例如:first.html,并提供以下代码:
<!DOCTYPE html>
<!--指定 th 命名空间,让 Thymeleaf 标准表达式可以被解析和执行-->
<!--th不是固定的,可以指定其它的命名空间,只不过大部分情况下用th-->
<!--表示程序中出现的 th 开头的后面代码都是 Thymeleaf语法,需要被 Thymeleaf识别-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>first springmvc</title>
</head>
<body>
<h1>我的第一个Spring MVC程序</h1>
</body>
</html>
对于每一个Thymeleaf文件来说 xmlns:th="http://www.thymeleaf.org" 是必须要写的,为了方便后续开发,可以将其添加到html模板文件中:
1.5.7 控制器FirstController处理请求返回逻辑视图名称
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: FirstController
* Description:
* Datetime: 2024/3/13 11:56
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class FirstController {
// 请求映射
// 这个方法是一个实例方法
// 这个方法目前返回一个String字符串
// 返回值代表的是一个逻辑视图名称
@RequestMapping(value="/haha")
public String 名字随意(){
System.out.println("正在处理请求....");
// 返回逻辑视图名称(决定跳转到哪个页面)
return "first";
}
}
1.5.8 测试
第一步:配置Tomcat服务器
第二步:部署web模块到Tomcat服务器
第三步:启动Tomcat服务器。如果在控制台输出的信息有中文乱码,请修改tomcat服务器配置文件:apache-tomcat-10.1.19\conf\logging.properties
第四步:打开浏览器,在浏览器地址栏上输入地址:http://localhost:8080/springmvc/haha
后端控制台输出:
1.5.9 执行流程总结
- 浏览器发送请求:http://localhost:8080/springmvc/haha
- SpringMVC的前端控制器DispatcherServlet接收到请求
- DispatcherServlet根据请求路径 /haha 映射到 FirstController#名字随意(),调用该方法
- FirstController#名字随意() 处理请求
- FirstController#名字随意() 返回逻辑视图名称 first 给视图解析器
- 视图解析器找到 /WEB-INF/templates/first.html 文件,并进行解析,生成视图解析对象返回给前端控制器DispatcherServlet
- 前端控制器DispatcherServlet响应结果到浏览器。
1.5.10 一个Controller可以编写多个方法
一个Controller可以提供多个方法,每个方法通常是处理对应的请求,例如:
@Controller
public class FirstController {
@RequestMapping(value="/haha")
public String 名字随意(){
System.out.println("正在处理请求....");
// 返回逻辑视图名称(决定跳转到哪个页面)
return "first";
}
@RequestMapping("/other")
public String other(){
System.out.println("正在处理其它请求...");
return "other";
}
}
提供 other.html 文件
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>other</title>
</head>
<body>
<h1>other ...</h1>
</body>
</html>
在 first.html 文件中,添加超链接,用超链接发送 /other 请求:
<!DOCTYPE html>
<!--指定 th 命名空间,让 Thymeleaf 标准表达式可以被解析和执行-->
<!--th不是固定的,可以指定其它的命名空间,只不过大部分情况下用th-->
<!--表示程序中出现的 th 开头的后面代码都是 Thymeleaf语法,需要被 Thymeleaf识别-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>first springmvc</title>
</head>
<body>
<h1>我的第一个Spring MVC程序</h1>
<!-- th: 表示后面的代码可以编写Thymeleaf语法,可以被Thymeleaf语法解析 -->
<!-- Thymeleaf检测到以 / 开始,表示绝对路径,自动会将webapp的上下文路径加上去 -->
<!-- 最终的效果是:href="/springmvc/other" -->
<a th:href="@{/other}">other请求</a>
</body>
</html>
启动Tomcat,打开浏览器,输入请求路径:http://localhost:8080/springmvc/haha
点击超链接:other请求
1.6 第二个SpringMVC程序
1.6.1 创建Maven模块
- pom.xml文件中添加依赖
- springmvc依赖
- logback依赖
- servlet依赖(scope为provided)
- thymeleaf与spring6整合依赖
- 打包方式war
1.6.2 添加web支持
webapp目录没有小蓝点怎么办?添加web支持
1.6.3 配置web.xml文件
重点:SpringMVC配置文件的名字和路径是可以手动设置的,如下:
<?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">
<!--配置前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--通过Servlet的初始化参数来指定Spring MVC配置文件的名字和位置-->
<init-param>
<param-name>contextConfigLocation</param-name>
<!--指定了Spring MVC配置文件的名字是: springmvc.xml-->
<!--指定了Spring MVC配置文件存放的路径是:类的根路径-->
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!--在web服务器启动的时候,就初始化DispatcherServlet-->
<!--这是优化方式,可以提高用户第一次发送请求的体验。第一次请求的效率较高。-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
通过来设置SpringMVC配置文件的路径和名字。在DispatcherServlet的init方法执行时设置的。
1建议加上,这样可以提高用户第一次访问的效率。表示在web服务器启动时初始化DispatcherServlet。
1.6.4 编写IndexController
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: IndexController
* Description:
* Datetime: 2024/3/13 15:47
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class IndexController {
@RequestMapping("/")
public String toIndex(){
return "index";
}
}
表示请求路径如果是:http://localhost:8080/springmvc/ ,则进入 /WEB-INF/templates/index.html 页面。
这就是项目的首页效果!!!!!
1.6.5 在resources目录下配置springmvc.xml文件
配置内容和之前一样,一个是视图解析器,一个是组件扫描。
1.6.6 提供视图
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>index page</title>
</head>
<body>
<h1>index page</h1>
</body>
</html>
1.6.7 测试
部署到web服务器,启动web服务器,打开浏览器,在地址栏上输入:http://localhost:8080/springmvc/
1.7 课堂笔记
1. 第一个Spring MVC程序的开发流程
1.1 创建Maven模块
第一步:创建一个空的工程springmvc
第二步:设置JDK版本
第三步:设置Maven
第四步:创建Maven模块(我这里创建的是一个普通的Maven模块)
第五步:在pom文件中设置打包方式:war
第六步:引入依赖:
springmvc依赖
logback依赖
thymeleaf和spring6整合依赖
servlet依赖(scope设置为provided,表示这个依赖最终由第三方容器来提供。)
1.2 给Maven模块添加web支持
在模块下的src\main目录下新建 webapp目录(默认是带有小蓝点的,没有小蓝点,自己添加Web支持就有小蓝点了。)
另外,在添加web支持的时候,需要添加web.xml文件,注意添加的路径。
1.3 在web.xml文件中配置:前端控制器(SpringMVC框架内置的一个类:DispatcherServlet),所有的请求都应该经过这个DispatcherServlet的处理。
重点:<url-pattern>/</url-pattern>
这里的 / 表示:除xx.jsp结尾的请求路径之外的所有请求路径。
也就是说,只要不是JSP请求路径,那么一定会走DispatcherServlet。
1.4 编写FirstController,在类上标注 @Controller 注解,纳入IoC容器的管理。
当然,也可以采用 @Component注解进行标注。 @Controller 只是 @Component 注解的别名。
1.5 配置/编写 SpringMVC框架自己的配置文件:
这个配置文件有默认的名字:<servlet-name>-servlet.xml
这个配置文件有默认的存放位置:WEB-INF 目录下。
两个配置:
第一个:配置组件扫描
第二个:配置视图解析器
1.6 提供视图
在/WEB-INF/templates目录下新建 first.thymeleaf 文件
在该文件中编写符合 Thymeleaf 语法格式的字符串(编写Thymeleaf的模板语句)
1.7 提供请求映射
@RequestMapping("/test")
public String hehe(){
// 处理业务逻辑....
// 返回一个逻辑视图名称
return "first";
}
最终会将逻辑视图名称转换为物理视图名称:
逻辑视图名称:first
物理视图名称:前缀 + first + 后缀
最终路径是:/WEB-INF/templates/first.thymeleaf
使用Thymeleaf模板引擎,将/WEB-INF/templates/first.thymeleaf转换成html代码,最终响应给浏览器。
1.8 测试
配置Tomcat服务器
解决Tomcat服务器控制台日志乱码问题
启动Tomcat服务器,在浏览器地址栏上直接发送请求:http://localhost:8080/springmvc/test
2. Spring MVC中的配置文件,名字是可以指定的,位置也是可以指定的,怎么指定?
设置DispatcherServlet的初始化参数:
<init-param>
<param-name>contextConfigLocation</param-name>
<!--指定了Spring MVC配置文件的名字是: springmvc.xml-->
<!--指定了Spring MVC配置文件存放的路径是:类的根路径-->
<param-value>classpath:springmvc.xml</param-value>
</init-param>
建议,在web服务器启动的时候,初始化DispatcherServlet,这样用户第一次发送请求时,效率较高。体验好。
<load-on-startup>1</load-on-startup>
第2章 RequestMapping注解
2.1 RequestMapping的作用
@RequestMapping
注解是 Spring MVC 框架中的一个控制器映射注解,用于将请求映射到相应的处理方法上。具体来说,它可以将指定 URL 的请求绑定到一个特定的方法或类上,从而实现对请求的处理和响应。
2.2 RequestMapping的出现位置
通过RequestMapping的源码可以看到RequestMapping注解只能出现在类上或者方法上。
2.3 类上与方法上结合使用
我们先来看,在同一个web应用中,是否可以有两个完全一样的RequestMapping。测试一下:假设两个RequestMapping,其中一个是展示用户详细信息,另一个是展示商品详细信息。提供两个Controller,一个是UserController,另一个是ProductController。如下:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: UserController
* Description:
* Datetime: 2024/3/13 16:40
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class UserController {
@RequestMapping("/detail")
public String toDetail(){
return "detail";
}
}
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: ProductController
* Description:
* Datetime: 2024/3/13 16:40
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class ProductController {
@RequestMapping("/detail")
public String toDetail(){
return "detail";
}
}
以上两个Controller的RequestMapping相同,都是"/detail",我们来启动服务器看会不会出现问题:异常发生了,异常信息如下
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping':
Ambiguous mapping. Cannot map 'userController' method
com.powernode.springmvc.controller.UserController#toDetail()
to { [/detail]}: There is already 'productController' bean method
com.powernode.springmvc.controller.ProductController#toDetail() mapped.
以上异常信息大致的意思是:不明确的映射。无法映射UserController中的toDetail()方法,因为已经在ProductController中映射过了!!!!
通过测试得知,在同一个webapp中,RequestMapping必须具有唯一性。怎么解决以上问题?两种解决方案:
- 第一种方案:将方法上RequestMapping的映射路径修改的不一样。
- 第二种方案:在类上添加RequestMapping的映射路径,以类上的RequestMapping作为命名空间,来加以区分两个不同的映射。
2.3.1 第一种方案
将方法上RequestMapping的映射路径修改的不一样。
@RequestMapping("/user/detail")
public String toDetail(){
return "/user/detail";
}
@RequestMapping("/product/detail")
public String toDetail(){
return "/product/detail";
}
再次启动web服务器,会发现没有再报错了。
为这两个请求分别提供对应的视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>商品详情页面</title>
</head>
<body>
<h1>商品详情</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户详情页面</title>
</head>
<body>
<h1>用户详情</h1>
</body>
</html>
在首页面添加两个超链接:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>index page</title>
</head>
<body>
<h1>index page</h1>
<a th:href="@{/user/detail}">用户详情</a><br>
<a th:href="@{/product/detail}">商品详情</a><br>
</body>
</html>
启动Tomcat服务器,并测试:http://localhost:8080/springmvc/
点击用户详情,点击商品详情,都可以正常显示:
2.3.2 第二种方案
在类上和方法上都使用RequestMapping注解来进行路径的映射。假设在类上映射的路径是"/a",在方法上映射的路径是"/b",那么整体表示映射的路径就是:“/a/b”
在第一种方案中,假设UserController类中有很多方法,每个方法的 RequestMapping注解中都需要以"/user"开始,显然比较啰嗦,干脆将"/user"提升到类级别上,例如:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: UserController
* Description:
* Datetime: 2024/3/13 16:40
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/detail")
public String toDetail(){
return "/user/detail";
}
}
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: ProductController
* Description:
* Datetime: 2024/3/13 16:40
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/detail")
public String toDetail(){
return "/product/detail";
}
}
经过测试,程序可以正常执行!!!
2.4 RequestMapping注解的value属性
2.4.1 value属性的使用
value属性是该注解最核心的属性,value属性填写的是请求路径,也就是说通过该请求路径与对应的控制器的方法绑定在一起。另外通过源码可以看到value属性是一个字符串数组:
既然是数组,就表示可以提供多个路径,也就是说,在SpringMVC中,多个不同的请求路径可以映射同一个控制器的同一个方法:
编写新的控制器:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: RequestMappingTestController
* Description: 测试 RequestMapping 注解
* Datetime: 2024/3/14 9:14
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class RequestMappingTestController {
// 对于注解来说:如果是一个数组,数组中只有一个元素,大括号是可以省略的
//@RequestMapping(value = "/testVal1")
// 对于注解来说,如果只使用了一个value属性,那么value也是可以省略的。
//@RequestMapping("/testVal2")
//@RequestMapping(path = {"/testVal1", "/testVal2"})
@RequestMapping(value = {"/testValue1", "/testValue2"})
public String testValue(){
return "testValue";
}
}
提供视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test Value</title>
</head>
<body>
<h1>Test RequestMapping's Value</h1>
</body>
</html>
在index.html文件中添加两个超链接:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>index page</title>
</head>
<body>
<h1>index page</h1>
<a th:href="@{/user/detail}">用户详情</a><br>
<a th:href="@{/product/detail}">商品详情</a><br>
<!--测试RequestMapping的value属性-->
<a th:href="@{/testValue1}">testValue1</a><br>
<a th:href="@{/testValue2}">testValue2</a><br>
</body>
</html>
启动服务器,测试,点击以下的两个超链接,发送请求,都可以正常访问到同一个控制器上的同一个方法:
2.4.2 Ant风格的value
value是可以用来匹配路径的,路径支持模糊匹配,我们把这种模糊匹配称之为Ant风格。关于路径中的通配符包括:
- ?,代表任意一个字符
- *,代表0到N个任意字符
- **,代表0到N个任意字符,并且路径中可以出现路径分隔符 /
注意:** 通配符在使用时,左右不能出现字符,只能是 /
测试一下这些通配符,在 RequestMappingTestController 中添加以下方法:
@RequestMapping("/x?z/testValueAnt")
public String testValueAnt(){
return "testValueAnt";
}
提供视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test Value Ant</title>
</head>
<body>
<h1>测试RequestMapping注解的value属性支持模糊匹配</h1>
</body>
</html>
在index.html页面中编写超链接:
<!--测试RequestMapping注解的value属性支持模糊匹配-->
<a th:href="@{/xyz/testValueAnt}">测试value属性的模糊匹配</a><br>
测试结果如下:
通过修改浏览器地址栏上的路径,可以反复测试通配符 ? 的语法:
将 ? 通配符修改为 * 通配符:
//@RequestMapping("/x?z/testValueAnt")
@RequestMapping("/x*z/testValueAnt")
public String testValueAnt(){
return "testValueAnt";
}
打开浏览器直接在地址栏上输入路径进行测试:
将 * 通配符修改为 ** 通配符:
@RequestMapping("/x**z/testValueAnt")
public String testValueAnt(){
return "testValueAnt";
}
注意:/x**z/ 实际上并没有使用通配符 **,本质上还是使用的 *,因为通配符 ** 在使用的时候,左右两边都不能有任何字符,必须是 /。
@RequestMapping("/**/testValueAnt")// 报错了,Spring6报错。Spring5不会报错。
public String testValueAnt(){
return "testValueAnt";
}
启动服务器发现报错了:
以上写法在Spring5的时候是支持的,但是在Spring6中进行了严格的规定,** 通配符只能出现在路径的末尾,例如:
@RequestMapping("/testValueAnt/**")
public String testValueAnt(){
return "testValueAnt";
}
测试结果:
2.4.3 value中的占位符(重点)
到目前为止,我们的请求路径是这样的格式:uri?name1=value1&name2=value2&name3=value3
其实除了这种方式,还有另外一种格式的请求路径,格式为:uri/value1/value2/value3,我们将这样的请求路径叫做 RESTful 风格的请求路径。
RESTful风格的请求路径在现代的开发中使用较多。
普通的请求路径:http://localhost:8080/springmvc/login?username=admin&password=123&age=20
RESTful风格的请求路径:http://localhost:8080/springmvc/login/admin/123/20
如果使用RESTful风格的请求路径,在控制器中应该如何获取请求中的数据呢?可以在value属性中使用占位符,例如:/login/{id}/{username}/{password}
在 RequestMappingTestController 类中添加一个方法:
@RequestMapping(value="/testRESTful/{id}/{username}/{age}")
public String testRESTful(
@PathVariable("id")
int id,
@PathVariable("username")
String username,
@PathVariable("age")
int age){
System.out.println(id + "," + username + "," + age);
return "testRESTful";
}
提供视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test RESTful</title>
</head>
<body>
<h1>测试value属性使用占位符</h1>
</body>
</html>
在 index.html 页面中添加超链接:
<!--测试RequestMapping注解的value属性支持占位符-->
<a th:href="@{/testRESTful/1/zhangsan/20}">测试value属性使用占位符</a>
启动服务器测试:
2.5 RequestMapping注解的method属性
2.5.1 method属性的作用
在Servlet当中,如果后端要求前端必须发送一个post请求,后端可以通过重写doPost方法来实现。后端要求前端必须发送一个get请求,后端可以通过重写doGet方法来实现。当重写的方法是doPost时,前端就必须发送post请求,当重写doGet方法时,前端就必须发送get请求。如果前端发送请求的方式和后端的处理方式不一致时,会出现405错误。
HTTP状态码405,这种机制的作用是:限制客户端的请求方式,以保证服务器中数据的安全。
假设后端程序要处理的请求是一个登录请求,为了保证登录时的用户名和密码不被显示到浏览器的地址栏上,后端程序有义务要求前端必须发送一个post请求,如果前端发送get请求,则应该拒绝。
那么在SpringMVC框架中应该如何实现这种机制呢?可以使用RequestMapping注解的method属性来实现。
通过RequestMapping源码可以看到,method属性也是一个数组:
数组中的每个元素是 RequestMethod,而RequestMethod是一个枚举类型的数据:
因此如果要求前端发送POST请求,该注解应该这样用:
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(){
return "success";
}
接下来,我们来测试一下:
在RequestMappingTestController类中添加以下方法:
@RequestMapping(value="/login", method = RequestMethod.POST)
public String testMethod(){
return "testMethod";
}
提供视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test Method</title>
</head>
<body>
<h1>Login Success!!!</h1>
</body>
</html>
在index.html页面中提供一个登录的form表单,后端要求发送post请求,则form表单的method属性应设置为post:
<!--测试RequestMapping的method属性-->
<form th:action="@{/login}" method="post">
用户名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"/><br>
<input type="submit" value="登录">
</form>
启动服务器,测试:
通过测试,前端发送的请求方式post,后端处理请求的方式也是post,就不会有问题。
当然,如果后端要求前端必须发送post请求,而前端发送了get请求,则会出现405错误,将index.html中form表单提交方式修改为get:
<!--测试RequestMapping的method属性-->
<form th:action="@{/login}" method="get">
用户名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"/><br>
<input type="submit" value="登录">
</form>
再次测试:
因此,可以看出,对于RequestMapping注解来说,多一个属性,就相当于多了一个映射的条件,如果value和method属性都有,则表示只有前端发送的请求路径 + 请求方式都满足时才能与控制器上的方法建立映射关系,只要有一个不满足,则无法建立映射关系。例如:@RequestMapping(value=“/login”, method = RequestMethod.POST) 表示当前端发送的请求路径是 /login,并且发送请求的方式是POST的时候才会建立映射关系。如果前端发送的是get请求,或者前端发送的请求路径不是 /login,则都是无法建立映射的。
2.5.2 衍生Mapping
对于以上的程序来说,SpringMVC提供了另一个注解,使用这个注解更加的方便,它就是:PostMapping,使用该注解时,不需要指定method属性,因为它默认采用的就是POST处理方式:修改RequestMappingTestController代码如下
//@RequestMapping(value="/login", method = RequestMethod.POST)
@PostMapping("/login")
public String testMethod(){
return "testMethod";
}
当前端发送get请求时,测试一下:
当前端发送post请求时,测试一下:
在SpringMVC中不仅提供了 PostMaping注解,像这样的注解还有四个,包括:
- GetMapping:要求前端必须发送get请求
- PutMapping:要求前端必须发送put请求
- DeleteMapping:要求前端必须发送delete请求
- PatchMapping:要求前端必须发送patch请求
2.5.3 web的请求方式
前端向服务器发送请求的方式包括哪些?共9种,前5种常用,后面作为了解:
- GET:获取资源,只允许读取数据,不影响数据的状态和功能。使用 URL 中传递参数或者在 HTTP 请求的头部使用参数,服务器返回请求的资源。(一般用作查询)
- POST:向服务器提交资源,可能还会改变数据的状态和功能。通过表单等方式提交请求体,服务器接收请求体后,进行数据处理。(一般用作新增操作)
- PUT:更新资源,用于更新指定的资源上所有可编辑内容。通过请求体发送需要被更新的全部内容,服务器接收数据后,将被更新的资源进行替换或修改。(一般用作更新操作)
- DELETE:删除资源,用于删除指定的资源。将要被删除的资源标识符放在 URL 中或请求体中。(一般用作删除操作)
- HEAD:请求服务器返回资源的头部,与 GET 命令类似,但是所有返回的信息都是头部信息(响应头),不能包含数据体(响应体)。主要用于资源检测和缓存控制。
- PATCH:部分更改请求。当被请求的资源是可被更改的资源时,请求服务器对该资源进行部分更新,即每次更新一部分。
- OPTIONS:请求获得服务器支持的请求方法类型,以及支持的请求头标志。“OPTIONS *”则返回支持全部方法类型的服务器标志。
- TRACE:服务器响应输出客户端的 HTTP 请求,主要用于调试和测试。
- CONNECT:建立网络连接,通常用于加密 SSL/TLS 连接。
注意:
- 使用超链接以及原生的form表单只能提交get和post请求,put、delete、head请求可以使用发送ajax请求的方式来实现。
- 使用超链接发送的是get请求
- 使用form表单,如果没有设置method,发送get请求
- 使用form表单,设置method=“get”,发送get请求
- 使用form表单,设置method=“post”,发送post请求
- 使用form表单,设置method=“put/delete/head”,发送get请求。(针对这种情况,可以测试一下)
将index.html中登录表单的提交方式method设置为put:
<!--测试RequestMapping的method属性-->
<form th:action="@{/login}" method="put">
用户名:<input type="text" name="username"/><br>
密码:<input type="password" name="password"/><br>
<input type="submit" value="登录">
</form>
修改RequestMappingTestController类的代码:
@RequestMapping(value="/login", method = RequestMethod.PUT)
//@PostMapping("/login")
public String testMethod(){
return "testMethod";
}
测试结果:
通过测试得知,即使form中method设置为put方式,但仍然采用get方式发送请求。
再次修改RequestMappingTestController:
@RequestMapping(value="/login", method = RequestMethod.GET)
//@PostMapping("/login")
public String testMethod(){
return "testMethod";
}
再次测试:
2.5.4 GET和POST的区别
在之前发布的JavaWEB视频中对HTTP请求协议的GET和POST进行了详细讲解,这里就不再赘述,大致回顾一下。
HTTP请求协议之GET请求:(GET请求可以认为没有请求体)
GET /springmvc/login?username=lucy&userpwd=1111 HTTP/1.1 请求行
Host: localhost:8080 请求头
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="95", "Chromium";v="95", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/springmvc/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
空白行
请求体
HTTP请求协议之POST请求:
POST /springmvc/login HTTP/1.1 请求行
Host: localhost:8080 请求头
Connection: keep-alive
Content-Length: 25
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="95", "Chromium";v="95", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
Origin: http://localhost:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://localhost:8080/springmvc/index.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
空白行
username=lisi&userpwd=123 请求体
2.5.4.1 区别是什么
- get请求发送数据的时候,数据会挂在URI的后面,并且在URI后面添加一个“?”,"?"后面是数据。这样会导致发送的数据回显在浏览器的地址栏上。
http://localhost:8080/springmvc/login?username=zhangsan&userpwd=1111
- post请求发送数据的时候,在请求体当中发送。不会回显到浏览器的地址栏上。也就是说post发送的数据,在浏览器地址栏上看不到。
- get请求只能发送普通的字符串。并且发送的字符串长度有限制,不同的浏览器限制不同。这个没有明确的规范。get请求无法发送大数据量。
- post请求可以发送任何类型的数据,包括普通字符串,流媒体等信息:视频、声音、图片。post请求可以发送大数据量,理论上没有长度限制。
- get请求在W3C中是这样说的:get请求比较适合从服务器端获取数据。
- post请求在W3C中是这样说的:post请求比较适合向服务器端传送数据。
- get请求是安全的。因为在正确使用get请求的前提下,get请求只是为了从服务器上获取数据,不会对服务器数据进行修改。
- post请求是危险的。因为post请求是修改服务器端的资源。
- get请求支持缓存。 也就是说当第二次发送get请求时,会走浏览器上次的缓存结果,不再真正的请求服务器。(有时需要避免,怎么避免:在get请求路径后添加时间戳)
- post请求不支持缓存。每一次发送post请求都会真正的走服务器。
2.5.4.2 怎么选择
- 如果你是想从服务器上获取资源,建议使用GET请求,如果你这个请求是为了向服务器提交数据,建议使用POST请求。
- 大部分的form表单提交,都是post方式,因为form表单中要填写大量的数据,这些数据是收集用户的信息,一般是需要传给服务器,服务器将这些数据保存/修改等。
- 如果表单中有敏感信息,建议使用post请求,因为get请求会回显敏感信息到浏览器地址栏上。(例如:密码信息)
- 做文件上传,一定是post请求。要传的数据不是普通文本。
- 其他情况大部分都是使用get请求。
2.6 RequestMapping注解的params属性
2.6.1 params属性的理解
params属性用来设置通过请求参数来映射请求。
对于RequestMapping注解来说:
- value属性是一个数组,只要满足数组中的任意一个路径,就能映射成功
- method属性也是一个数组,只要满足数组中任意一个请求方式,就能映射成功。
- params属性也是一个数组,不过要求请求参数必须和params数组中要求的所有参数完全一致后,才能映射成功。
2.6.2 params属性的4种用法
@RequestMapping(value=“/login”, params={“username”, “password”}) 表示:请求参数中必须包含 username 和 password,才能与当前标注的方法进行映射。
@RequestMapping(value=“/login”, params={“!username”, “password”}) 表示:请求参数中不能包含username参数,但必须包含password参数,才能与当前标注的方法进行映射。
@RequestMapping(value=“/login”, params={“username=admin”, “password”}) 表示:请求参数中必须包含username参数,并且参数的值必须是admin,另外也必须包含password参数,才能与当前标注的方法进行映射。
@RequestMapping(value=“/login”, params={“username!=admin”, “password”}) 表示:请求参数中必须包含username参数,但参数的值不能是admin,另外也必须包含password参数,才能与当前标注的方法进行映射。
注意:如果前端提交的参数,和后端要求的请求参数不一致,则出现400错误!!!
HTTP状态码400的原因:请求参数格式不正确而导致的。
2.6.3 测试params属性
在 RequestMappingTestController 类中添加如下方法:
@RequestMapping(value="/testParams", params = {"username", "password"})
public String testParams(){
return "testParams";
}
提供视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>testParams</title>
</head>
<body>
<h1>测试RequestMapping注解的Params属性</h1>
</body>
</html>
在index.html文件中添加超链接:
<!--测试RequestMapping的params属性-->
<a th:href="@{/testParams(username='admin',password='123')}">测试params属性</a>
当然,你也可以这样写:这样写IDEA会报错,但不影响使用。
<a th:href="@{/testParams?username=admin&password=123}">测试params属性</a><br>
启动服务器,测试:
假如发送请求时,没有传递username参数会怎样?
<a th:href="@{/testParams(password='123')}">测试params属性</a><br>
启动服务器,测试:
提示无效的请求参数,服务器无法或不会处理当前请求。
params属性剩下的三种情况,自行测试!!!!
2.7 RequestMapping注解的headers属性
2.7.1 认识headers属性
headers和params原理相同,用法也相同。
当前端提交的请求头信息和后端要求的请求头信息一致时,才能映射成功。
请求头信息怎么查看?在chrome浏览器中,F12打开控制台,找到Network,可以查看具体的请求协议和响应协议。在请求协议中可以看到请求头信息,例如:
请求头信息和请求参数信息一样,都是键值对形式,例如上图中:
- Referer: http://localhost:8080/springmvc/ 键是Referer,值是http://localhost:8080/springmvc/
- Host: localhost:8080 键是Host,值是localhost:8080
2.7.2 headers属性的4种用法
@RequestMapping(value=“/login”, headers={“Referer”, “Host”}) 表示:请求头信息中必须包含Referer和Host,才能与当前标注的方法进行映射。
@RequestMapping(value=“/login”, headers={“Referer”, “!Host”}) 表示:请求头信息中必须包含Referer,但不包含Host,才能与当前标注的方法进行映射。
@RequestMapping(value=“/login”, headers={“Referer=http://localhost:8080/springmvc/”, “Host”}) 表示:请求头信息中必须包含Referer和Host,并且Referer的值必须是http://localhost:8080/springmvc/,才能与当前标注的方法进行映射。
@RequestMapping(value=“/login”, headers={“Referer!=http://localhost:8080/springmvc/”, “Host”}) 表示:请求头信息中必须包含Referer和Host,并且Referer的值不是http://localhost:8080/springmvc/,才能与当前标注的方法进行映射。
注意:如果前端提交的请求头信息,和后端要求的请求头信息不一致,则出现404错误!!!
2.7.3 测试headers属性
在 RequestMappingTestController 类中添加以下方法:
@RequestMapping(value="/testHeaders", headers = {"Referer=http://localhost:8080/springmvc/"})
public String testHeaders(){
return "testHeaders";
}
提供视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test Headers</title>
</head>
<body>
<h1>测试RequestMapping注解的headers属性</h1>
</body>
</html>
在index.html页面中添加超链接:
<!--测试RequestMapping的headers属性-->
<a th:href="@{/testHeaders}">测试headers属性</a><br>
启动服务器,测试结果:
将后端控制器中的headers属性值进行修改:
@RequestMapping(value="/testHeaders", headers = {"Referer=http://localhost:8888/springmvc/"})
public String testHeaders(){
return "testHeaders";
}
再次测试:
其他情况自行测试!!!!
2.8 课堂笔记
3. @RequestMapping注解可以出现在类上,也可以出现在方法上,例如:
@Controller
@RequestMapping("/a")
public class UserController{
@RequestMapping("/b")
public String index(){
return "index";
}
}
前端浏览器发送的请求路径是 /a/b 的时候,则执行UserController#index()方法。
4. 关于 @RequestMapping 注解的value属性
value属性本身是一个 String[] 字符串数组,说明多个请求路径可以映射同一个处理器方法。
如果注解的属性是数组,并且在使用注解的时候,该数组中只有一个元素,大括号可以省略。
如果使用某个注解的时候,如果只使用了一个value属性,那么value也是可以省略的。
value属性的别名是path。
path属性的别名是value。
5. RequestMapping的value属性支持Ant风格的,支持模糊匹配的路径
? 表示任意一个字符。(除 / ? 之外的其它字符),注意:一定是一个字符哦。不能空着。
* 表示0到N个任意字符。(除 / ? 之外的其它字符)。
** 表示0到N个任意字符。并且路径中可以出现 /
但是 ** 在使用的时候需要注意,** 左边只能是 /。
注意:
如果使用Spring5以及之前的版本,这样写是没问题的:@RequestMapping(value = "/**/testAntValue")
如果使用Spring6以及之后的版本,这样写是报错的:@RequestMapping(value = "/**/testAntValue")
在Spring6当中,** 通配符只能作为路径的末尾出现。
6. 关于@RequestMapping注解的value属性上的占位符(重点)
传统的URL:
/springmvc/login?username=admin&password=123
现在的开发比较流行使用RESTFul风格的URL:
/springmvc/login/admin/123
在SpringMVC当中,如果请求的URL使用的是RESTFul风格的,那么这个数据应该在java程序中如何获取呢?使用占位符方式。
@RequestMapping(value = "/login/{a}/{b}")
public String testRESTFulURL(
@PathVariable("a")
String username,
@PathVariable("b")
String password){
System.out.println("用户名:" + username + ",密码:" + password);
return "ok";
}
7. 关于@RequestMapping注解的method属性,通过该属性可以限制前端发送的请求方式。如果前端发送的请求方式与后端的处理方式不同,则出现405错误
@Controller
public class UserController{
@RequestMapping(value="/user/login", method=RequestMethod.POST)
public String userLogin(){
return "ok";
}
}
表示:当前端发送的请求路径是 /user/login,并且请求方式是POST的时候,才能映射到 UserController#userLogin()方法上。
只要有一个不满足,则无法映射。例如:请求路径对应不上,或者请求方式对应不上,都是无法映射的。
8.衍生Mapping
@PostMapping 注解代替的是:@RequestMapping(value="", method=RequestMethod.POST)
@GetMapping 注解代替的是:@RequestMapping(value="", method=RequestMethod.GET)
....
@PutMapping
@DeleteMapping
@PatchMapping
9.web的请求方式
比较常用的:
GET POST PUT DELETE HEAD
GET:适合查询
POST:适合新增
PUT:适合修改
DELETE:适合删除
HEAD:适合获取响应头信息。
注意:使用form表单提交时,如果method设置为 put delete head,对不起,发送的请求还是get请求。
如果要发送put delete head请求,请发送ajax请求才可以。
10. 关于 RequestMapping注解的 params 属性
@RequestMapping(value="/testParams", params={"username", "password"})
public String testParams(){
return "ok";
}
当RequestMapping注解中添加了params,则表示又添加了新的约束条件。
当请求路径是 /testParams,并且请求携带的参数有 username 和 password的时候,才能映射成功!
关于thymeleaf中怎么发送请求的时候携带数据:
<a th:href="/testParams?name=value&name=value"></a>
<a th:href="/testParams(name='admin',password='1234')"></a>
11. 关于RequestMapping注解的headers属性:
也是一个数组。用来设置请求头的映射。
@RequestMapping(value="/login", headers={"Referer", "Host"})
public String testHeaders(){
return "ok";
}
当请求路径是 /login,并且请求头中包含 Referer,也包含Host的时候,映射成功。
第3章 获取请求数据
假设有这样一个请求:http://localhost:8080/springmvc/register?name=zhangsan&password=123&email=zhangsan@powernode.com
在SpringMVC中应该如何获取请求提交的数据呢?
在SpringMVC中又应该如何获取请求头信息呢?
在SpringMVC中又应该如何获取客户端提交的Cookie数据呢?
3.1 准备
3.1.1 创建模块,添加依赖
<?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.powernode.springmvc</groupId>
<artifactId>springmvc-003</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!--springmvc依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.4</version>
</dependency>
<!--logback依赖-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.3</version>
</dependency>
<!--servlet依赖-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<!--thymeleaf和spring6整合的依赖-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
3.1.2 添加web支持
3.1.3 编写web.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<!--前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--通过初始化参数来指定springmvc配置文件的路径和名字。-->
<init-param>
<param-name>contextConfigLocation</param-name>
<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>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
3.1.4 创建UserController
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: UserController
* Description:
* Datetime: 2024/3/14 20:05
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class UserController {
@RequestMapping("/")
public String toRegisterPage(){
// 返回一个逻辑视图
return "register";
}
}
3.1.5 编写springmvc.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: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/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.springmvc.controller"/>
<!--视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<!--作用于视图渲染的过程中,可以设置视图渲染后输出时采用的编码字符集-->
<property name="characterEncoding" value="UTF-8"/>
<!--如果配置多个视图解析器,它来决定优先使用哪个视图解析器,它的值越小优先级越高-->
<property name="order" value="1"/>
<!--当 ThymeleafViewResolver 渲染模板时,会使用该模板引擎来解析、编译和渲染模板-->
<property name="templateEngine">
<bean class="org.thymeleaf.spring6.SpringTemplateEngine">
<!--用于指定 Thymeleaf 模板引擎使用的模板解析器。模板解析器负责根据模板位置、模板资源名称、文件编码等信息,加载模板并对其进行解析-->
<property name="templateResolver">
<bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<!--设置模板文件的位置(前缀)-->
<property name="prefix" value="/WEB-INF/templates/"/>
<!--设置模板文件后缀(后缀),Thymeleaf文件扩展名不一定是html,也可以是其他,例如txt,大部分都是html-->
<property name="suffix" value=".html"/>
<!--设置模板类型,例如:HTML,TEXT,JAVASCRIPT,CSS等-->
<property name="templateMode" value="HTML"/>
<!--用于模板文件在读取和解析过程中采用的编码字符集-->
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
</beans>
3.1.6 编写register.html文件
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<h3>用户注册</h3>
<hr>
</body>
</html>
3.1.7 部署测试
3.2 使用原生的Servlet API进行获取
原生的Servlet API指的是:HttpServletRequest
在SpringMVC当中,一个Controller类中的方法参数上如果有HttpServletRequest,SpringMVC会自动将当前请求对象
传递给这个参数,因此我们可以通过这个参数来获取请求提交的数据。测试一下。
SpringMVC自动赋值其实就是做个类型判断
在 register.html 中准备一个注册的表单:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<h3>用户注册</h3>
<hr>
<form th:action="@{/register}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
性别:
男 <input type="radio" name="sex" value="1">
女 <input type="radio" name="sex" value="0">
<br>
爱好:
抽烟 <input type="checkbox" name="hobby" value="smoke">
喝酒 <input type="checkbox" name="hobby" value="drink">
烫头 <input type="checkbox" name="hobby" value="perm">
<br>
简介:<textarea rows="10" cols="60" name="intro"></textarea><br>
<input type="submit" value="注册">
</form>
</body>
</html>
先测试这个页面是否可以正常打开,是否可以正常提交数据:
点击注册:F12的方式查看是否提交了数据:
通过测试得知:可以正常提交数据。
接下来在控制器添加一个方法来处理这个注册的请求:
@PostMapping(value="/register")
public String register(HttpServletRequest request){
// 通过当前请求对象获取提交的数据
String username = request.getParameter("username");
String password = request.getParameter("password");
String sex = request.getParameter("sex");
String[] hobbies = request.getParameterValues("hobby");// hobby=smoke&hobby=drink&hobby=tt
String intro = request.getParameter("intro");
System.out.println(username + "," + password + "," + sex + "," + Arrays.toString(hobbies) + "," + intro);
return "success";
}
提供视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>注册成功</title>
</head>
<body>
<h1>注册成功</h1>
</body>
</html>
测试:
这样通过Servlet原生的API获取到提交的数据。但是这种方式不建议使用,因为方法的参数依赖Servlet原生API,Controller的测试将不能单独测试,必须依赖WEB服务器(如Tomcat服务器)才能测试。另外,换句话说,如果在SpringMVC中使用了原生的Servlet,你为什么还要用SpringMVC框架呢!!!!!
junit单元测试就是new对象调方法,往方法里传参数。但是通过Servlet原生的API获取到提交的数据,request等参数是依赖Tomcat服务器传递的,因此Controller不能单独测试
3.3 使用RequestParam注解标注
3.3.1 RequestParam注解的基本使用
RequestParam注解作用:将请求参数
与方法上的形参
映射。
@PostMapping(value = "/register")
public String register(
@RequestParam(value="username")// 如果前端没有提供 username 参数,则报错:400
String a,// 变量名随意
@RequestParam(value="password")
String b,
@RequestParam(value="sex")
String c,
@RequestParam(value="hobby")
String[] d,
@RequestParam(name="intro")
String e) {
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println(Arrays.toString(d));
System.out.println(e);
return "success";
}
注意:对于@RequestParam注解来说,属性有value和name,这两个属性的作用相同,都是用来指定提交数据的name。
例如:发送请求时提交的数据是:name1=value1&name2=value2,则这个注解应该这样写:@RequestParam(value=“name1”)、@RequestParam(value=“name2”)
启动服务器测试:
一定要注意: @RequestParam(value=“name2”) 中value一定不要写错,写错就会出现以下问题:
测试结果:
3.3.2 RequestParam注解的required属性
required属性用来设置该方法参数是否为必须的。
默认情况下,这个参数为 true
,表示方法参数是必需的。如果请求中缺少对应的参数,则会抛出异常。
可以将其设置为false
,false表示不是必须的,如果请求中缺少对应的参数,则方法的参数为null。
测试,修改register方法,如下:
添加了一个 age 形参,没有指定 required 属性时,默认是true,表示必需的,但前端表单中没有年龄age,我们来看报错信息:
错误信息告诉我们:参数age是必需的。没有提供这个请求参数,HTTP状态码 400
如果将 required 属性设置为 false。则该参数则不是必须的,如果请求参数仍然未提供时,我们来看结果:
通过测试得知,如果一个参数被设置为不是必需的
,当没有提交对应的请求参数时,形参默认值null。
当然,如果请求参数中提供了age,则age为真实提交的数据:
3.3.3 RequestParam注解的defaultValue属性
defaultValue属性用来设置形参的默认值,当没有提供对应的请求参数
或者请求参数的值是空字符串""
的时候,方法的形参会采用默认值。
当前端页面没有提交email的时候:
当前端页面提交的email是空字符串的时候:
当前端提交的email不是空字符串的时候:
3.4 依靠控制器方法上的形参名来接收
@RequestParam 这个注解是可以省略的,如果方法形参的名字和提交数据时的name相同,则 @RequestParam 可以省略。
但有一个前提:如果你采用的是Spring6+版本,你需要在pom.xml文件中指定编译参数’-parameter’,配置如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
注意:如果你使用的是Spring5的版本,以上的配置是不需要的。
Controller中的方法只需要这样写:形参的名字必须和提交的数据的name一致!!!!!
@PostMapping(value="/register")
public String register(String username, String password, String sex, String[] hobby, String intro){
System.out.println(username + "," + password + "," + sex + "," + Arrays.toString(hobby) + "," + intro);
return "success";
}
测试结果:
如果形参名和提交的数据的name不一致时:
另外,还有一点,对于提交的hobby数据,也可以采用String来接收,不一定使用数组方式:
@PostMapping(value="/register")
public String register(String username, String password, String sex, String hobby, String intro){
System.out.println(username + "," + password + "," + sex + "," + hobby + "," + intro);
return "success";
}
测试结果:
根据输出结果可以看到多个hobby是采用“,”进行连接的。
3.5 使用POJO类/JavaBean接收请求参数
以上方式大家可以看到,当提交的数据非常多时,方法的形参个数会非常多,这不是很好的设计。在SpringMVC中也可以使用POJO类/JavaBean来接收请求参数。不过有一个非常重要的要求:POJO类的属性名
必须和请求参数的参数名
保持一致。提供以下的JavaBean:
package com.powernode.springmvc.pojo;
import java.util.Arrays;
/**
* ClassName: User
* Description:
* Datetime: 2024/3/15 10:51
* Author: 老杜@动力节点
* Version: 1.0
*/
public class User {
private Long id;
private String username;
private String password;
private String sex;
private String[] hobby;
private String intro;
public User() {
}
public User(Long id, String username, String password, String sex, String[] hobby, String intro) {
this.id = id;
this.username = username;
this.password = password;
this.sex = sex;
this.hobby = hobby;
this.intro = intro;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String[] getHobby() {
return hobby;
}
public void setHobby(String[] hobby) {
this.hobby = hobby;
}
public String getIntro() {
return intro;
}
public void setIntro(String intro) {
this.intro = intro;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", sex='" + sex + '\'' +
", hobby=" + Arrays.toString(hobby) +
", intro='" + intro + '\'' +
'}';
}
}
在控制器方法的形参位置上使用javabean来接收请求参数:
@PostMapping("/register")
public String register(User user){
System.out.println(user);
return "success";
}
执行结果:
底层的实现原理:反射机制。先获取请求参数的名字,因为请求参数的名字就是JavaBean的属性名,通过这种方式给对应的属性赋值。
我们来测试一下:当JavaBean的属性名和请求参数的参数名不一致时,会出现什么问题?(注意:getter和setter的方法名不修改,只修改属性名)
package com.powernode.springmvc.pojo;
import java.util.Arrays;
/**
* ClassName: User
* Description:
* Datetime: 2024/3/15 10:51
* Author: 老杜@动力节点
* Version: 1.0
*/
public class User {
private Long id;
private String uname;
private String upwd;
private String usex;
private String[] uhobby;
private String uintro;
public User() {
}
public User(Long id, String username, String password, String sex, String[] hobby, String intro) {
this.id = id;
this.uname = username;
this.upwd = password;
this.usex = sex;
this.uhobby = hobby;
this.uintro = intro;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return uname;
}
public void setUsername(String username) {
this.uname = username;
}
public String getPassword() {
return upwd;
}
public void setPassword(String password) {
this.upwd = password;
}
public String getSex() {
return usex;
}
public void setSex(String sex) {
this.usex = sex;
}
public String[] getHobby() {
return uhobby;
}
public void setHobby(String[] hobby) {
this.uhobby = hobby;
}
public String getIntro() {
return uintro;
}
public void setIntro(String intro) {
this.uintro = intro;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + uname + '\'' +
", password='" + upwd + '\'' +
", sex='" + usex + '\'' +
", hobby=" + Arrays.toString(uhobby) +
", intro='" + uintro + '\'' +
'}';
}
}
测试结果:
通过测试,我们得知:请求参数名
可以和JavaBean的属性名
不一致。
我们继续将其中一个属性的setter和getter方法名修改一下:
再次测试:
通过测试可以看到:username属性没有赋上值。可见请求参数是否可以赋值到JavaBean对应的属性上,不是取决于属性名,而是setter方法名。
3.6 RequestHeader注解
该注解的作用是:将请求头信息
映射到方法的形参上
。
和RequestParam注解功能相似,RequestParam注解的作用:将请求参数
映射到方法的形参
上。
当然,对于RequestHeader注解来说,也有三个属性:value、required、defaultValue,和RequestParam一样,这里就不再赘述了。
测试:
@PostMapping("/register")
public String register(User user,
@RequestHeader(value="Referer", required = false, defaultValue = "")
String referer){
System.out.println(user);
System.out.println(referer);
return "success";
}
执行结果:
3.7 CookieValue注解
该注解的作用:将请求提交的Cookie数据
映射到方法形参
上
同样是有三个属性:value、required、defaultValue
前端页面中编写发送cookie的代码:
<!--发送Cookie给服务器-->
<script type="text/javascript">
function sendCookie(){
document.cookie = "id=123456789; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/";
document.location = "/springmvc/register";
}
</script>
<button onclick="sendCookie()">向服务器端发送Cookie</button>
后端UserController代码:
@GetMapping("/register")
public String register(User user,
@RequestHeader(value="Referer", required = false, defaultValue = "")
String referer,
@CookieValue(value="id", required = false, defaultValue = "2222222222")
String id){
System.out.println(user);
System.out.println(referer);
System.out.println(id);
return "success";
}
测试结果:
3.8 请求的中文乱码问题
3.8.1 get请求乱码
get请求数据在URI后面提交,这个乱码问题怎么解决呢?解决办法是找到 CATALINA_HOME/config/server.xml文件,找到其中配置端口号的标签,在该标签中添加 URIEncoding=“UTF-8”。但是对于高版本的Tomcat服务器来说,是不需要设置的,例如Tomcat10,Tomcat9,有如下的默认配置,在默认情况下URIEncoding使用的就是UTF-8的编码方式。
但对于低版本的Tomcat服务器,例如:Tomcat8。URIEncoding的默认配置是ISO-8859-1,因此在Tomcat8中需要手动配置server.xml文件:
配置如下:
接下来,我们测试一下,在默认情况下,Tomcat10是否已经解决了get请求乱码问题:
<form th:action="@{/register}" method="get">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
性别:
男 <input type="radio" name="sex" value="1">
女 <input type="radio" name="sex" value="0">
<br>
爱好:
抽烟 <input type="checkbox" name="hobby" value="smoke">
喝酒 <input type="checkbox" name="hobby" value="drink">
烫头 <input type="checkbox" name="hobby" value="perm">
<br>
简介:<textarea rows="10" cols="60" name="intro"></textarea><br>
<input type="submit" value="注册">
</form>
注意,以上表单已经修改为get请求了。
@GetMapping("/register")
public String register(User user){
System.out.println(user);
return "success";
}
测试结果:
3.8.2 post请求乱码
post请求是解决请求体的中文乱码问题。解决办法大家都知道:
request.setCharacterEncoding("UTF-8");
同样,对于高版本的Tomcat10服务器来说,针对请求体中的字符编码也是配置好的,默认也是采用了UTF-8,中文乱码问题也解决了,在这个文件中配置的:apache-tomcat-10.1.19\conf\web.xml
配置内容如下:
通过以上配置可以看到,Tomcat10对请求和响应都设置了默认的字符编码方式为UTF-8
一定要注意:Tomcat9以及之前的版本,以上的配置是没有的。
我们来测试一下,针对Tomcat10来说,SpringMVC会不会有乱码问题:
<form th:action="@{/register}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
性别:
男 <input type="radio" name="sex" value="1">
女 <input type="radio" name="sex" value="0">
<br>
爱好:
抽烟 <input type="checkbox" name="hobby" value="smoke">
喝酒 <input type="checkbox" name="hobby" value="drink">
烫头 <input type="checkbox" name="hobby" value="perm">
<br>
简介:<textarea rows="10" cols="60" name="intro"></textarea><br>
<input type="submit" value="注册">
</form>
注意:以上表单已经修改为post请求
@PostMapping("/register")
public String register(User user, HttpServletRequest request) throws UnsupportedEncodingException {
System.out.println(user);
return "success";
}
测试结果:
通过测试可以看到在Tomcat10当中,默认SpringMVC,发送POST请求,是不会出现乱码问题的。
有可能很多同学使用的不是Tomcat10,如果不是Tomcat10,则会出现乱码问题,我们来模拟一下乱码的产生,将apache-tomcat-10.1.19\conf\web.xml文件中的UTF-8配置修改为ISO-8859-1:
一定要重启Tomcat10,新的配置才能生效,来测试一下是否存在乱码:
那么,在SpringMVC中如何解决请求体的中文乱码问题呢?当然,还是使用request.setCharacterEncoding("UTF-8")
使用它有一个前提条件,要想解决请求体乱码问题,以上代码必须在 request.getParameter("username")
执行之前执行才有效。
也就是说以上代码如果放在Controller的相关方法中执行是无效的,因为Controller的方法在执行之前 DispatcherServlet已经调用了 request.getParameter("username")
方法。因此在Controller方法中使用request.setCharacterEncoding("UTF-8");
无效我们来测试一下:
<form th:action="@{/register}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
性别:
男 <input type="radio" name="sex" value="1">
女 <input type="radio" name="sex" value="0">
<br>
爱好:
抽烟 <input type="checkbox" name="hobby" value="smoke">
喝酒 <input type="checkbox" name="hobby" value="drink">
烫头 <input type="checkbox" name="hobby" value="perm">
<br>
简介:<textarea rows="10" cols="60" name="intro"></textarea><br>
<input type="submit" value="注册">
</form>
注意:以上表单已经修改为post请求
@PostMapping("/register")
public String register(User user, HttpServletRequest request) throws UnsupportedEncodingException {
request.setCharacterEncoding("UTF-8");
System.out.println(user);
return "success";
}
测试结果:
通过测试可以看到:在Controller当中调用request.setCharacterEncoding("UTF-8")
是无法解决POST乱码问题的。
那怎么办呢?怎么样才能在DispatcherServlet之前执行request.setCharacterEncoding("UTF-8")
呢?没错,我相信大家想到了:过滤器Filter。过滤器Filter可以在Servlet执行之前执行。有同学又说了:监听器不行吗?不行。因为我们需要对每一次请求解决乱码,而监听器只在服务器启动阶段执行一次。因此这里解决每一次请求的乱码问题,应该使用过滤器Filter。并且,告诉大家一个好消息,SpringMVC已经将这个字符编码的过滤器提前写好了,我们直接配置好即可:CharacterEncodingFilter
,我们一起看一下它的源码:
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.filter;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Servlet Filter that allows one to specify a character encoding for requests.
* This is useful because current browsers typically do not set a character
* encoding even if specified in the HTML page or form.
*
* <p>This filter can either apply its encoding if the request does not already
* specify an encoding, or enforce this filter's encoding in any case
* ("forceEncoding"="true"). In the latter case, the encoding will also be
* applied as default response encoding (although this will usually be overridden
* by a full content type set in the view).
*
* @author Juergen Hoeller
* @since 15.03.2004
* @see #setEncoding
* @see #setForceEncoding
* @see jakarta.servlet.http.HttpServletRequest#setCharacterEncoding
* @see jakarta.servlet.http.HttpServletResponse#setCharacterEncoding
*/
public class CharacterEncodingFilter extends OncePerRequestFilter {
@Nullable
private String encoding;
private boolean forceRequestEncoding = false;
private boolean forceResponseEncoding = false;
/**
* Create a default {@code CharacterEncodingFilter},
* with the encoding to be set via {@link #setEncoding}.
* @see #setEncoding
*/
public CharacterEncodingFilter() {
}
/**
* Create a {@code CharacterEncodingFilter} for the given encoding.
* @param encoding the encoding to apply
* @since 4.2.3
* @see #setEncoding
*/
public CharacterEncodingFilter(String encoding) {
this(encoding, false);
}
/**
* Create a {@code CharacterEncodingFilter} for the given encoding.
* @param encoding the encoding to apply
* @param forceEncoding whether the specified encoding is supposed to
* override existing request and response encodings
* @since 4.2.3
* @see #setEncoding
* @see #setForceEncoding
*/
public CharacterEncodingFilter(String encoding, boolean forceEncoding) {
this(encoding, forceEncoding, forceEncoding);
}
/**
* Create a {@code CharacterEncodingFilter} for the given encoding.
* @param encoding the encoding to apply
* @param forceRequestEncoding whether the specified encoding is supposed to
* override existing request encodings
* @param forceResponseEncoding whether the specified encoding is supposed to
* override existing response encodings
* @since 4.3
* @see #setEncoding
* @see #setForceRequestEncoding(boolean)
* @see #setForceResponseEncoding(boolean)
*/
public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) {
Assert.hasLength(encoding, "Encoding must not be empty");
this.encoding = encoding;
this.forceRequestEncoding = forceRequestEncoding;
this.forceResponseEncoding = forceResponseEncoding;
}
/**
* Set the encoding to use for requests. This encoding will be passed into a
* {@link jakarta.servlet.http.HttpServletRequest#setCharacterEncoding} call.
* <p>Whether this encoding will override existing request encodings
* (and whether it will be applied as default response encoding as well)
* depends on the {@link #setForceEncoding "forceEncoding"} flag.
*/
public void setEncoding(@Nullable String encoding) {
this.encoding = encoding;
}
/**
* Return the configured encoding for requests and/or responses.
* @since 4.3
*/
@Nullable
public String getEncoding() {
return this.encoding;
}
/**
* Set whether the configured {@link #setEncoding encoding} of this filter
* is supposed to override existing request and response encodings.
* <p>Default is "false", i.e. do not modify the encoding if
* {@link jakarta.servlet.http.HttpServletRequest#getCharacterEncoding()}
* returns a non-null value. Switch this to "true" to enforce the specified
* encoding in any case, applying it as default response encoding as well.
* <p>This is the equivalent to setting both {@link #setForceRequestEncoding(boolean)}
* and {@link #setForceResponseEncoding(boolean)}.
* @see #setForceRequestEncoding(boolean)
* @see #setForceResponseEncoding(boolean)
*/
public void setForceEncoding(boolean forceEncoding) {
this.forceRequestEncoding = forceEncoding;
this.forceResponseEncoding = forceEncoding;
}
/**
* Set whether the configured {@link #setEncoding encoding} of this filter
* is supposed to override existing request encodings.
* <p>Default is "false", i.e. do not modify the encoding if
* {@link jakarta.servlet.http.HttpServletRequest#getCharacterEncoding()}
* returns a non-null value. Switch this to "true" to enforce the specified
* encoding in any case.
* @since 4.3
*/
public void setForceRequestEncoding(boolean forceRequestEncoding) {
this.forceRequestEncoding = forceRequestEncoding;
}
/**
* Return whether the encoding should be forced on requests.
* @since 4.3
*/
public boolean isForceRequestEncoding() {
return this.forceRequestEncoding;
}
/**
* Set whether the configured {@link #setEncoding encoding} of this filter
* is supposed to override existing response encodings.
* <p>Default is "false", i.e. do not modify the encoding.
* Switch this to "true" to enforce the specified encoding
* for responses in any case.
* @since 4.3
*/
public void setForceResponseEncoding(boolean forceResponseEncoding) {
this.forceResponseEncoding = forceResponseEncoding;
}
/**
* Return whether the encoding should be forced on responses.
* @since 4.3
*/
public boolean isForceResponseEncoding() {
return this.forceResponseEncoding;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String encoding = getEncoding();
if (encoding != null) {
if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
if (isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
}
}
最核心的方法是:
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String encoding = getEncoding();
if (encoding != null) {
if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
if (isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
}
分析以上核心方法得知该过滤器对请求和响应都设置了字符编码方式。
- 当
强行使用请求字符编码方式为true
时,或者请求对象的字符编码方式为null
时,设置请求的字符编码方式。 - 当
强行使用响应字符编码方式为true
时,设置响应的字符编码方式。
根据以上代码,可以得出以下配置信息,在web.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>forceRequestEncoding</param-name>
<param-value>true</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>
我们再来测试,重启Tomcat10,看看乱码是否能够解决?
注意:针对于我们当前的Tomcat10的配置来说,它有默认的字符集ISO-8859-1,因此以下在web.xml文件中的配置是不能缺少的:
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
如果缺少它,仍然是会存在乱码问题的。自行测试一下!!!!
3.9 课堂笔记
12. 获取请求提交的数据
12.1 第一种方式使用原生的Servlet API
在处理器的方法参数上提供:HttpServletRequest
SpringMVC框架会自动将Tomcat服务器创建request对象传递给处理器方法。
我们直接在处理器方法中使用request对象即可。
当然,HttpServletResponse,HttpSession有需要的话,也可以采用这种方式注入。
12.2 第二种方式:使用SpringMVC框架提供的一个注解: @RequestParam(请求参数)
@RequestParam注解中的属性:
value属性:value属性可以使用name属性代替
name属性:name属性可以已使用value属性代替
required属性:用来设置该参数是否为必须的。默认值是true。默认情况下这个参数是必须要传递过来的。如果前端没有提交这个参数,报错:400错误。
这个属性有点类似于 @RequestMapping 注解中的 params 属性的作用。
@RequestMapping(value="/testParams", params={"username", "password"})
public String testParams(){
return "ok";
}
required属性可以设置为false,这样这个参数就不是必须的了。如果前端没有提供,则不会报400错误。但是由于前端没有提供这个数据,因此程序中的变量值为null
defaultValue属性:通过defaultValue属性可以给参数赋默认值。如果前端没有提供这样的参数,参数的默认值就起作用了。
12.3 第三种方式:依靠控制器方法上的形参名来接收
如果 请求参数名 和 控制器方法上的形参名 保持一致,那么 @RequestParam注解可以省略。
如果你使用的是Spring6+版本,则需要在pom.xml文件中添加如下编译配置:(Spring5以及之前的版本不需要。)
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
注意:如果 控制器方法上的形参名 和 请求参数名 没有对应上,那么控制器方法中的形参默认值是null。
12.4 第四种方式:使用POJO类/JavaBean接收请求参数(这是最常用的)
底层实现原理:反射机制。
不过,使用这种方式的前提是:POJO类的属性名必须和请求参数的参数名保持一致
实现原理是什么?
假设提交了一个请求,参数名是 username,那么要求POJO类当中必须有一个属性名也叫做:username
Class clazz = Class.forName("com.powernode.springmvc.pojo.User");
User user = (User)clazz.newInstance();
String fieldName = "username";
String setMethodName = "setUsername";
Method setMethod = clazz.getDeclaredMethod(setMethodName, ....);
setMethod.invoke(user, "zhaoliu");
重点:底层通过反射机制调用set方法给属性赋值的。所以set方法的方法名非常重要。
如果前端提交了参数是: username=zhangsan
那么必须保证POJO类当中有一个方法名叫做:setUsername
如果前端提交了参数是: email=zhangsan@powernode.com
那么必须保证POJO类当中有一个方法名叫做:setEmail
如果没有对应的set方法,将无法给对应的属性赋值。
13. 获取请求头信息?
使用 @RequestHeader 注解,它用来标注 形参。作用是:将 请求头信息 映射到 控制器方法的形参 上。
@PostMapping("/user/reg")
public String register(User user,
@RequestHeader(value = "Referer", required = false, defaultValue = "")
String referer){
System.out.println(user);
System.out.println(referer);
return "ok";
}
14. 获取客户端提交的Cookie?
使用 @CookieValue 注解标注控制器方法上的形参。
@RequestMapping("/user/reg")
public String register(User user,
@CookieValue(value = "id", required = false, defaultValue = "")
String id){
System.out.println("客户端提交过来的cookie,它的值是:" + id);
return "ok";
}
15. 关于javaweb项目中,get请求的乱码问题?
get请求,提交的数据是在浏览器的地址栏上回显。在请求行上提交数据,例如:/springmvc/login?username=张三&password=123
怎么解决get请求乱码问题呢?
对URI进行编码设置,在哪儿可以设置URI的编码方式呢?在Tomcat服务器的配置CATALINA_HOME/conf/server.xml文件中:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
maxParameterCount="1000"
URIEncoding="UTF-8"
/>
对于Tomcat10和Tomcat9来说:get请求没有乱码。也就是说Tomcat10或者Tomcat9已经自动对URI进行编码,并且默认的编码方式就是UTF-8
但是对于Tomcat8来说,URIEncoding的默认值是ISO-8859-1编码方式,所以在Tomcat8中,get请求是存在中文乱码问题的,怎么解决?如上所描述。
16. 关于javaweb项目中,post请求的乱码问题?
post请求乱码如何解决?
request.setCharacterEncoding("UTF-8");
但是有一个前提: request.setCharacterEncoding("UTF-8"); 这一行代码必须在 String username = request.getParameter("username"); 方法执行之前执行,才有效。
在Tomcat10当中,我们是不需要考虑post请求乱码问题,因为Tomcat10,已经自动帮助我们执行了: request.setCharacterEncoding("UTF-8");
在哪里可以看到呢?
在CATALINA_HOME/conf/web.xml文件中有这样的配置:
<request-character-encoding>UTF-8</request-character-encoding>
<response-character-encoding>UTF-8</response-character-encoding>
这个配置信息表示:请求体采用UTF-8的方式,另外响应的时候也采用UTF-8的方式,所以POST请求无乱码,响应也没有乱码。
注意了:对于Tomcat9以及之前的版本来说,没有以上的配置。POST请求乱码问题,响应的乱码问题都需要自行解决。
那么如果遇到Tomcat9- 版本,那么POST请求乱码应该怎么解决呢?对于SpringMVC来说,有什么好的办法吗?
在 request.getParameter() 方法执行之前,执行: request.setCharacterEncoding("UTF-8"); ,这样问题就能解决。
第一种方案:自己编写一个过滤器!!!!过滤器Filter在Servlet执行之前执行。
第二种方案:使用SpringMVC框架内置的字符编码过滤器即可:CharacterEncodingFilter。
第4章 三个域对象
4.1 Servlet中的三个域对象
请求域:request
会话域:session
应用域:application
三个域都有以下三个方法:
// 向域中存储数据
void setAttribute(String name, Object obj);
// 从域中读取数据
Object getAttribute(String name);
// 删除域中的数据
void removeAttribute(String name);
主要是通过:setAttribute + getAttribute方法来完成在域中数据的传递和共享。
4.1.1 request
接口名:HttpServletRequest
简称:request
request对象代表了一次请求。一次请求一个request。
使用请求域的业务场景:在A资源中通过转发的方式跳转到B资源,因为是转发,因此从A到B是一次请求,如果想让A资源和B资源共享同一个数据,可以将数据存储到request域中。
4.1.2 session
接口名:HttpSession
简称:session
session对象代表了一次会话。从打开浏览器开始访问,到最终浏览器关闭,这是一次完整的会话。每个会话session对象都对应一个JSESSIONID,而JSESSIONID生成后以cookie的方式存储在浏览器客户端。浏览器关闭,JSESSIONID失效,会话结束。
使用会话域的业务场景:
- 在A资源中通过重定向的方式跳转到B资源,因为是重定向,因此从A到B是两次请求,如果想让A资源和B资源共享同一个数据,可以将数据存储到session域中。
- 登录成功后保存用户的登录状态。
4.1.3 application
接口名:ServletContext
简称:application
application对象代表了整个web应用,服务器启动时创建,服务器关闭时销毁。对于一个web应用来说,application对象只有一个。
使用应用域的业务场景:记录网站的在线人数。
4.2 request域对象
在SpringMVC中,在request域中共享数据有以下几种方式:
- 使用原生Servlet API方式。
- 使用Model接口。
- 使用Map接口。
- 使用ModelMap类。
- 使用ModelAndView类。
4.2.1 使用原生Servlet API方式
在Controller的方法上使用HttpServletRequest:
package com.powernode.springmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: RequestScopeTestController
* Description:
* Datetime: 2024/3/18 15:20
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class RequestScopeTestController {
@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request){
// 将共享的数据存储到request域当中
request.setAttribute("testRequestScope", "在SpringMVC中使用原生Servlet API实现request域数据共享");
// 跳转视图,在视图页面将request域中的数据取出来,这样就完成了:Controller和View在同一个请求当中两个组件之间数据的共享。
// 这个跳转,默认情况下是:转发的方式。(转发forward是一次请求)
// 这个返回的是一个逻辑视图名称,经过视图解析器解析,变成物理视图名称。/WEB-INF/thymeleaf/view.html
return "view";
}
}
页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>view</title>
</head>
<body>
<div th:text="${testRequestScope}"></div>
</body>
</html>
超链接:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>Index Page</h1>
<a th:href="@{/testServletAPI}">在SpringMVC中使用原生Servlet API实现request域数据共享</a><br>
</body>
</html>
测试结果:
这种方式当然可以,用SpringMVC框架,不建议使用原生Servlet API。
4.2.2 使用Model接口
@RequestMapping("/testModel")
public String testModel(Model model){
// 向request域中存储数据
model.addAttribute("testRequestScope", "在SpringMVC中使用Model接口实现request域数据共享");
// 转发
return "view";
}
4.2.3 使用Map接口
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map){
// 向request域中存储数据
map.put("testRequestScope", "在SpringMVC中使用Map接口实现request域数据共享");
// 转发
return "view";
}
4.2.4 使用ModelMap类
@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap){
// 向request域中存储数据
modelMap.addAttribute("testRequestScope", "在SpringMVC中使用ModelMap实现request域数据共享");
// 转发
return "view";
}
4.2.4.1 Model、Map、ModelMap的关系
可以在以上Model、Map、ModelMap的测试程序中将其输出,看看输出什么:
看不出来什么区别,从输出结果上可以看到都是一样的。
可以将其运行时类名输出:
通过输出结果可以看出,无论是Model、Map还是ModelMap,底层实例化的对象都是:BindingAwareModelMap。
可以查看BindingAwareModelMap的继承结构:
通过继承结构可以看出:BindingAwareModelMap继承了ModelMap,而ModelMap又实现了Map接口。
另外,请看以下源码:
可以看出ModelMap又实现了Model接口。因此表面上是采用了不同方式,底层本质上是相同的。
SpringMVC之所以提供了这些方式,目的就是方便程序员的使用,提供了多样化的方式,可见它的重要性。
4.2.5 使用ModelAndView类
在SpringMVC框架中为了更好的体现MVC架构模式,提供了一个类:ModelAndView。这个类的实例封装了Model和View。也就是说这个类既封装业务处理之后的数据,也体现了跳转到哪个视图。使用它也可以完成request域数据共享。
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
// 创建 模型视图 对象
ModelAndView modelAndView = new ModelAndView();
// 给 模型视图对象 绑定数据
modelAndView.addObject("testRequestScope", "在SpringMVC中使用ModelAndView实现request域数据共享");
// 给 模型视图对象 绑定视图(绑定逻辑视图名称)
modelAndView.setViewName("view");
// 返回 模型视图对象
return modelAndView;
}
这种方式需要注意的是:
- 方法的返回值类型不是String,而是ModelAndView对象。
- ModelAndView不是出现在方法的参数位置,而是在方法体中new的。
- 需要调用addObject向域中存储数据。
- 需要调用setViewName设置视图的名字。
4.2.5.1 ModelAndView源码分析
以上我们通过了五种方式完成了request域数据共享,包括:原生Servlet API,Model、Map、ModelMap、ModelAndView
其中后四种:Model、Map、ModelMap、ModelAndView。这四种方式在底层DispatcherServlet调用我们的Controller之后,返回的对象都是ModelAndView,这个可以通过源码进行分析。
在以上四种方式中,拿Model举例,添加断点进行调试:
启动服务器,发送请求,走到断点:
查看VM Stack信息:
查看DispatcherServlet的1089行,源码如下:
可以看到这里,无论你使用哪种方式,最终都要返回一个ModelAndView对象。
DispatcherServlet 调用处理器方法之后,最终封装的对象都是ModelAndView对象。
提醒:大家可以通过以下断点调试方式,采用一级一级返回,最终可以看到都会返回ModelAndView对象。
4.3 session域对象
在SpringMVC中使用session域共享数据,实现方式有多种,其中比较常见的两种方式:
- 使用原生Servlet API
- 使用SessionAttributes注解
4.3.1 使用原生Servlet API
package com.powernode.springmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: SessionScopeTestController
* Description:
* Datetime: 2024/3/18 17:18
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class SessionScopeTestController {
@RequestMapping("/testSessionScope1")
public String testServletAPI(HttpSession session) {
// 向会话域中存储数据
session.setAttribute("testSessionScope1", "使用原生Servlet API实现session域共享数据");
// 返回逻辑视图名称(这是一个转发的行为)
return "view";
}
}
视图页面:
<div th:text="${session.testSessionScope1}"></div>
超链接:
<a th:href="@{/testSessionScope1}">在SpringMVC中使用原生Servlet API实现session域共享数据</a><br>
4.3.2 使用SessionAttributes注解
使用SessionAttributes注解标注Controller:
package com.powernode.springmvc.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
/**
* ClassName: SessionScopeTestController
* Description:
* Datetime: 2024/3/18 17:18
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
//@SessionAttributes(value = {"x", "y"})
@SessionAttributes({"x", "y"}) // 标注x和y都是存放到session域中,而不是request域。
public class SessionScopeTestController {
@RequestMapping("/testSessionScope2")
public String testSessionAttributes(ModelMap modelMap){
// 向session域中存储数据
modelMap.addAttribute("x", "我是埃克斯");
modelMap.addAttribute("y", "我是歪");
// 返回逻辑视图名称
return "view";
}
}
注意:SessionAttributes注解使用在Controller类上。标注了当key是 x 或者 y 时,数据将被存储到会话session中。如果没有 SessionAttributes注解,默认存储到request域中。
4.4 application域对象
在SpringMVC实现application域数据共享,最常见的方案就是直接使用Servlet API了:
package com.powernode.springmvc.controller;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* ClassName: ApplicationScopeTestController
* Description:
* Datetime: 2024/3/18 17:37
* Author: 老杜@动力节点
* Version: 1.0
*/
@Controller
public class ApplicationScopeTestController {
@RequestMapping("/testApplicationScope")
public String testApplicationScope(HttpServletRequest request){
// 将数据存储到application域当中
// 获取application对象,其实就是获取ServletContext对象
// 怎么获取ServletContext对象?通过request,通过session都可以获取。
ServletContext application = request.getServletContext();
// 向应用域中存储数据
application.setAttribute("applicationScope", "我是应用域当中的一条数据");
return "view";
}
}
视图页面:
<div th:text="${application.applicationScope}"></div>
超链接:
<a th:href="@{/testApplicationScope}">在SpringMVC中使用ServletAPI实现application域共享数据</a><br>
4.5 课堂笔记
17. request域:
第一种方式:在SpringMVC中使用原生的Servlet API可以完成request域数据共享:
在处理器方法上添加 HttpServletRequest参数即可。
第二种方式:在SpringMVC的处理器方法的参数上添加一个接口类型:Model
@RequestMapping("/testModel")
public String testModel(Model model){
// 向request域当中存储数据
model.addAttribute("name", value);
// 转发
return "ok";
}
MVC架构模式:
M: Model(模型,本质就是数据)
V: View
C: Controller
第三种方式:在SpringMVC的处理器方法的参数上添加一个接口类型:Map
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map){
// 向request域当中存储数据
map.put("testRequestScope", "在SpringMVC当中使用Map接口完成request域数据共享");
// 转发
return "ok";
}
第四种方式:在SpringMVC的处理器方法的参数上添加一个类型:ModelMap
@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap){
// 向request域当中存储数据
modelMap.addAttribute("testRequestScope", "在SpringMVC当中使用ModelMap类完成request域数据共享");
// 转发
return "ok";
}
研究一下:Model接口、Map接口、ModelMap类,三者之间的关系?
表面上使用的是不同的接口和不同的类。实际上底层都使用了同一个对象:org.springframework.validation.support.BindingAwareModelMap
BindingAwareModelMap继承了ExtendedModelMap类
ExtendedModelMap继承了ModelMap类
ExtendedModelMap实现了Model接口
ModelMap类继承了LinkedHashMap继承了HashMap实现了Map接口
第五种方式:使用Spring MVC框架提供的ModelAndView类完成request域数据共享。
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
// 创建 模型视图 对象
ModelAndView mav = new ModelAndView();
// 给 模型视图对象 绑定数据
mav.addObject("testRequestScope", "在SpringMVC当中使用ModelAndView类完成request域数据共享");
// 给 模型视图对象 绑定视图(绑定逻辑视图名称)
mav.setViewName("ok");
// 返回 模型视图对象
return mav;
}
我们聊一个真相:
对于处理器方法来说,不管你使用的参数是Model接口,还是Map接口,还是ModelMap类,还是ModelAndView类,最终处理器方法执行结束之后,
返回的都是ModelAndView对象。这个返回的ModelAndView对象给DispatcherServlet类了。
当请求路径不是JSP的时候,都会走前端控制器DispatcherServlet。
DispatcherServlet中有一个核心方法 doDispatch(),这个方法用来通过请求路径找到对应的 处理器方法
然后调用 处理器方法,处理器方法返回一个逻辑视图名称(可能也会直接返回一个ModelAndView对象),底层会
将逻辑视图名称转换为View对象,然后将View对象结合Model对象,封装一个ModelAndView对象,然后将该对象
返回给DispatcherServlet类了。
18. session域
第一种方式:使用原生的Servlet API实现。(在处理器方法的参数上添加一个 HttpSession 参数,SpringMVC会自动将session对象传递给这个参数。)
第二种方式:使用@SessionAttributes注解标注Controller
19. application域
这个域使用较少,如果使用的话,一般是采用ServletAPI的方式使用。
@RequestMapping("/testApplicationScope")
public String testApplicationScope(HttpServletRequest request){
ServletContext application = request.getServletContext();
application.setAttribute("testApplicationScope", "在SpringMVC中使用ServletAPI实现application域共享");
return "ok";
}
第5章 视图View
5.1 SpringMVC中视图的实现原理
5.1.1 Spring MVC视图支持可配置
在Spring MVC中,视图View是支持定制的,例如我们之前在 springmvc.xml 文件中进行了如下的配置:
<!--视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<!--作用于视图渲染的过程中,可以设置视图渲染后输出时采用的编码字符集-->
<property name="characterEncoding" value="UTF-8"/>
<!--如果配置多个视图解析器,它来决定优先使用哪个视图解析器,它的值越小优先级越高-->
<property name="order" value="1"/>
<!--当 ThymeleafViewResolver 渲染模板时,会使用该模板引擎来解析、编译和渲染模板-->
<property name="templateEngine">
<bean class="org.thymeleaf.spring6.SpringTemplateEngine">
<!--用于指定 Thymeleaf 模板引擎使用的模板解析器。模板解析器负责根据模板位置、模板资源名称、文件编码等信息,加载模板并对其进行解析-->
<property name="templateResolver">
<bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<!--设置模板文件的位置(前缀)-->
<property name="prefix" value="/WEB-INF/templates/"/>
<!--设置模板文件后缀(后缀),Thymeleaf文件扩展名不一定是html,也可以是其他,例如txt,大部分都是html-->
<property name="suffix" value=".html"/>
<!--设置模板类型,例如:HTML,TEXT,JAVASCRIPT,CSS等-->
<property name="templateMode" value="HTML"/>
<!--用于模板文件在读取和解析过程中采用的编码字符集-->
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
以上的配置表明当前SpringMVC框架使用的视图View是Thymeleaf的。
如果你需要换成其他的视图View,修改以上的配置即可。这样就可以非常轻松的完成视图View的扩展。
这种设计是完全符合OCP开闭原则的。视图View和框架是解耦合的,耦合度低扩展能力强。视图View可以通过配置文件进行灵活切换。
5.1.2 Spring MVC支持的常见视图
Spring MVC支持的常见视图包括:
- InternalResourceView:内部资源视图(Spring MVC框架内置的,专门为
JSP模板语法
准备的,也为转发forward准备的) - RedirectView:重定向视图(Spring MVC框架内置的,用来完成重定向效果)
- ThymeleafView:Thymeleaf视图(第三方的,为
Thymeleaf模板语法
准备的) - FreeMarkerView:FreeMarker视图(第三方的,为
FreeMarker模板语法
准备的) - VelocityView:Velocity视图(第三方的,为
Velocity模板语法
准备的) - PDFView:PDF视图(第三方的,专门用来生成pdf文件视图)
- ExcelView:Excel视图(第三方的,专门用来生成excel文件视图)
- …
5.1.3 实现视图机制的核心接口
实现视图的核心类与接口包括:
- DispatcherServlet类(前端控制器):
- 职责:在整个Spring MVC执行流程中,负责中央调度。
- 核心方法:doDispatch
- ViewResolver接口(视图解析器):
- 职责:负责将
逻辑视图名
转换为物理视图名
,最终创建View接口的实现类,即视图实现类对象。 - 核心方法:resolveViewName
- 职责:负责将
- View接口(视图):
- 职责:负责将模型数据Model渲染为视图格式(HTML代码),并最终将生成的视图(HTML代码)输出到客户端。(它负责将模板语言转换成HTML代码)
- 核心方法:render
- ViewResolverRegistry(视图解析器注册器):
- 负责在web容器(Tomcat)启动的时候,完成视图解析器的注册。如果有多个视图解析器,会将视图解析器对象按照order的配置放入List集合。
总结:
- 实现视图的核心类和接口包括:ViewResolverRegistry、DispatcherServlet、ViewResolver、View
- 如果你想定制自己的视图组件:
- 编写类实现ViewResolver接口,实现resolveViewName方法,在该方法中完成
逻辑视图名
转换为物理视图名
,并返回View对象。 - 编写类实现View接口,实现render方法,在该方法中将模板语言转换成HTML代码,并将HTML代码响应到浏览器。
- 编写类实现ViewResolver接口,实现resolveViewName方法,在该方法中完成
- 如果Spring MVC框架中使用Thymeleaf作为视图技术。那么相关的类包括:
- ThymeleafView
- ThymeleafViewResolver
视图机制源码跟踪:
public class DispatcherServlet extends FrameworkServlet {
// 前端控制器的核心方法,处理请求,返回视图,渲染视图,都是在这个方法中完成的。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 根据请求路径调用映射的处理器方法,处理器方法执行结束之后,返回逻辑视图名称
// 返回逻辑视图名称之后,DispatcherServlet会将 逻辑视图名称ViewName + Model,将其封装为ModelAndView对象。
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 这行代码的作用是处理视图
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
// 渲染页面(将模板字符串转换成html代码响应到浏览器)
render(mv, request, response);
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// 这个方法的作用是将 逻辑视图名称 转换成 物理视图名称 ,并且最终返回视图对象View
View view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
// 真正的将模板字符串转换成HTML代码,并且将HTML代码响应给浏览器。(真正的渲染。)
view.render(mv.getModelInternal(), request, response);
}
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
// 其实这一行代码才是真正起作用的:将 逻辑视图名称 转换成 物理视图名称 ,并且最终返回视图对象View
ViewResolver viewResolver; // 底层会创建一个ThymeleafViewResolver
// 如果使用的是Thymeleaf,那么返回的视图对象:ThymeleafView对象。
View view = viewResolver.resolveViewName(viewName, locale);
return view;
}
}
// 这是一个接口(负责视图解析的)
public interface ViewResolver { // 如果使用Thymeleaf,那么该接口的实现类就是:ThymeleafViewResolver
// 这个方法就是将:逻辑视图名称 转换成 物理视图名称 ,并且最终返回视图对象View
View resolveViewName(String viewName, Locale locale) throws Exception;
}
// 这是一个接口(负责将 模板字符串 转换成HTML代码,响应给浏览器)
public interface View {
void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception;
}
/*
核心类:DispatcherServlet
核心接口1:ViewResolver(如果你使用的是Thymeleaf,那么底层会创建ThymeleafViewResolver对象)
核心接口2:View(如果你使用的是Thymeleaf,那么底层会创建ThymeleafView对象)
结论:如果你自己想实现属于自己的视图。你至少需要编写两个类,
一个类实现ViewResolver接口,实现其中的resolveViewName方法。
另一个类实现View接口,实现其中的render方法。
*/
5.1.4 实现视图机制的原理描述
假设我们SpringMVC中使用了Thymeleaf作为视图。
第一步:浏览器发送请求给web服务器
第二步:Spring MVC中的DispatcherServlet接收到请求
第三步:DispatcherServlet根据请求路径分发到对应的Controller
第四步:DispatcherServlet调用Controller的方法
第五步:Controller的方法处理业务并返回一个逻辑视图名
(底层返回的是ModelAndView mv对象)给DispatcherServlet
第六步:DispatcherServlet调用ThymeleafViewResolver的resolveViewName方法,将逻辑视图名
转换为物理视图名
,并创建ThymeleafView对象返回给DispatcherServlet
第七步:DispatcherServlet再调用ThymeleafView的render方法,render方法将模板语言(模板字符串)转换为HTML代码,响应给浏览器,完成最终的渲染。
假设我们SpringMVC中使用了JSP作为视图。
第一步:浏览器发送请求给web服务器
第二步:Spring MVC中的DispatcherServlet接收到请求
第三步:DispatcherServlet根据请求路径分发到对应的Controller
第四步:DispatcherServlet调用Controller的方法
第五步:Controller的方法处理业务并返回一个逻辑视图名
(底层返回的是ModelAndView mv对象)给DispatcherServlet
第六步:DispatcherServlet调用InternalResourceViewResolver
的resolveViewName
方法,将逻辑视图名
转换为物理视图名
,并创建InternalResourceView
对象返回给DispatcherServlet
第七步:DispatcherServlet再调用InternalResourceView
的render
方法,render方法将模板语言转换为HTML代码,响应给浏览器,完成最终的渲染。
5.1.5 逻辑视图名到物理视图名的转换
逻辑视图名最终转换的物理视图名是什么,取决再springmvc.xml文件中视图解析器的配置:
假如视图解析器配置的是ThymeleafViewResolver,如下:
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="characterEncoding" value="UTF-8"/>
<property name="order" value="1"/>
<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>
以下程序返回逻辑视图名:index
@RequestMapping("/index")
public String toIndex(){
return "index";
}
最终逻辑视图名"index" 转换为物理视图名:/WEB-INF/templates/index.html
假如视图解析器配置的是InternalResourceViewResolver,如下:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/templates/"/>
<property name="suffix" value=".jsp"/>
</bean>
以下程序返回逻辑视图名:index
@RequestMapping("/index")
public String toIndex(){
return "index";
}
最终逻辑视图名"index" 转换为物理视图名:/WEB-INF/templates/index.jsp
5.2 Thymeleaf视图
我们在学习前面内容的时候,采用的都是Thymeleaf视图。我们再来测试一下,看看底层创建的视图对象是不是ThymeleafView
springmvc.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: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/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.springmvc.controller"/>
<!--视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="characterEncoding" value="UTF-8"/>
<property name="order" value="1"/>
<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/thymeleaf/"/>
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
</beans>
Controller代码如下:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/index")
public String toIndex(){
return "index";
}
}
视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>index page</title>
</head>
<body>
<h1>index page</h1>
</body>
</html>
添加断点:在DispatcherServlet的doDispatch方法的下图位置添加断点
启动Tomcat,在浏览器地址栏上发送请求:http://localhost:8080/springmvc/index
程序走到以上位置,这行代码是调用对应的Controller,并且Controller最终会返回ModelAndView对象:mv
按照我们之前所讲,返回mv之后,接下来就是视图处理与渲染,接着往下走,走到下图这一行:
这个方法的作用是处理分发结果,就是在这个方法当中进行了视图的处理与渲染,进入该方法:
进去之后走到上图位置:这个方法就是用来渲染页面的方法,再进入该方法:
走到上图位置就可以看到底层创建的是ThymeleafView对象。
上图根据不同的视图解析器,返回不同的视图
5.3 JSP视图(了解)
我们再来跟一下源码,看看JSP视图底层创建的是不是InternalResourceView对象。
我们前面说过 InternalResourceView是SpringMVC框架内置的,翻译为内部资源视图,SpringMVC把JSP看做是内部资源。可见JSP在之前的技术栈中有很高的地位。
不过,当下流行的开发中JSP使用较少,这里不再详细讲解。只是测试一下。
springmvc.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: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/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.springmvc.controller"/>
<!--视图解析器-->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
Controller代码如下:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/index")
public String toIndex(){
return "index";
}
}
视图页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>index jsp</title>
</head>
<body>
<h1>index jsp!</h1>
</body>
</html>
启动web容器,添加断点跟踪:
通过测试得知:对于JSP视图来说,底层创建的视图对象是InternalResourceView。
5.4 转发与重定向
5.4.1 回顾转发和重定向区别
- 转发是一次请求。因此浏览器地址栏上的地址不会发生变化。
- 重定向是两次请求。因此浏览器地址栏上的地址会发生变化。
- 转发的代码实现:request.getRequestDispatcher(“/index”).forward(request, response);
- 重定向的代码实现:response.sendRedirect(“/webapproot/index”);
- 转发是服务器内部资源跳转,由服务器来控制。不可实现跨域访问。
- 重定向可以完成内部资源的跳转,也可以完成跨域跳转。
- 转发的方式可以访问WEB-INF目录下受保护的资源。
- 重定向相当于浏览器重新发送了一次请求,在浏览器直接发送的请求是无法访问WEB-INF目录下受保护的资源的。
- 转发原理:
a. 假设发送了 /a 请求,执行了 AServlet
b. 在AServlet 中通过request.getRequestDispatcher("/b").forward(request,response);
转发到BServlet
c. 从AServlet跳转到BServlet是服务器内部来控制的。对于浏览器而言,浏览器只发送了一个 /a 请求。 - 重定向原理:
a. 假设发送了 /a 请求,执行了 AServlet
b. 在AServlet 中通过response.sendRedirect("/webapproot/b")
重定向到BServlet
c. 此时服务器会将请求路径/webapproot/b
响应给浏览器
d. 浏览器会自发的再次发送/webapproot/b
请求来访问BServlet
e. 因此对于重定向来说,发送了两次请求,一次是/webapproot/a
,另一次是/webapproot/b
。
以上所描述的是使用原生Servlet API来完成转发和重定向。在Spring MVC中是如何转发和重定向的呢?
5.4.2 forward
在Spring MVC中默认就是转发的方式,我们之前所写的程序,都是转发的方式。只不过都是转发到Thymeleaf的模板文件xxx.html上。
那么,在Spring MVC中如何转发到另一个Controller上呢?可以使用Spring MVC的forward
代码实现如下:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/a")
public String toA(){
// 返回的是一个逻辑视图名称
//return "a";
// 采用SpringMVC的转发方式跳转到 /b
// 转发的时候,格式有特殊要求: return "forward:下一个资源的路径";
// 这个就不是逻辑视图名称了。
return "forward:/b"; // 创建InternalResourceView对象。
}
@RequestMapping("/b")
public String toB(){
// 返回的是一个逻辑视图名称
return "b"; // 创建ThymeleafView对象。
}
}
视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>b</title>
</head>
<body>
<h1>Page B!!!</h1>
</body>
</html>
启动服务器,浏览器地址栏上输入:http://localhost:8080/springmvc/a
通过测试,可以顺利的完成转发,转发是一次请求,可以看到地址栏上的地址没有发生改变。
我们来跟踪一下源码,看看以上程序执行过程中,创建了几个视图对象,分别是什么?
通过源码的跟踪得知:整个请求处理过程中,一共创建了两个视图对象
- InternalResourceView
- ThymeleafView
这说明转发底层创建的视图对象是:InternalResourceView。
思考:既然会创建InternalResourceView,应该会对应一个视图解析器呀(InternalResourceViewResolver)?但是我在springmvc.xml文件中只配置了ThymeleafViewResolver,并没有配置InternalResourceViewResolver呀?这是为什么?
这是因为forward:
后面的不是逻辑视图名
,而是一个请求路径
。因此转发是不需要视图解析器的。
另外,转发使用的是InternalResourceView,也说明了转发是内部资源的跳转。(Internal是内部的意思,Resource是资源的意思。)
5.4.3 redirect
redirect是专门完成重定向效果的。和forward语法类似,只需要将之前的 return "forward:/b"
修改为 return "redirect:/b"
即可。
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping("/a")
public String toA(){
// 这个使用较多。
return "redirect:/b"; // 创建RedirectView
}
@RequestMapping("/b")
public String toB(){
return "b";
}
}
视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>b</title>
</head>
<body>
<h1>Page B!!!</h1>
</body>
</html>
启动服务器,浏览器地址栏上输入:http://localhost:8080/springmvc/a
可见,重定向是两次请求,地址栏上的地址发生了改变。
可以看一下源码,在重定向的时候,Spring MVC创建哪个视图对象?
通过断点调试可以看出,当重定向的时候,SpringMVC会创建一个重定向视图对象:RedirectView。这个视图对象也是SpringMVC框架内置的。
另外可以看出重定向之后的第二次请求创建的视图对象就是ThymeleafView了。
注意:从springmvc应用重定向到springmvc2应用(跨域),语法是:
@RequestMapping("/a")
public String a(){
return "redirect:http://localhost:8080/springmvc2/b";
}
可以自行测试一下!!!
5.5 <mvc:view-controller>
<mvc:view-controller>
配置用于将某个请求映射到特定的视图上,即指定某一个 URL 请求到一个视图资源的映射,使得这个视图资源可以被访问。它相当于是一个独立的处理程序,不需要编写任何 Controller,只需要指定 URL 和对应的视图名称就可以了。
一般情况下,<mvc:view-controller>
配置可以替代一些没有业务逻辑的 Controller,例如首页、错误页面等。当用户访问配置的 URL 时,框架将直接匹配到对应的视图,而无需再经过其他控制器的处理。
<mvc:view-controller>
配置的格式如下:
<mvc:view-controller path="/如何访问该页面" view-name="对应的逻辑视图名称" />
其中:
path
:被映射的 URL 路径。view-name
:对应的逻辑视图名称。
例如,配置首页的映射:
<mvc:view-controller path="/" view-name="index" />
上述配置将会匹配上访问应用程序的根路径,如:http://localhost:8080/。当用户在浏览器中访问该根路径时,就会直接渲染名为 index
的视图。
5.6 <mvc:annotation-driven/>
在SpringMVC中,如果在springmvc.xml文件中配置了 <mvc:view-controller>
,就需要同时在springmvc.xml文件中添加如下配置:
<mvc:annotation-driven/>
该配置的作用是:启用Spring MVC的注解。
如果没有以上的配置,Controller就无法访问到。访问之前的Controller会发生 404 问题。
5.7 访问静态资源
一个项目可能会包含大量的静态资源,比如:css、js、images等。
由于我们DispatcherServlet的url-pattern配置的是“/”,之前我们说过,这个"/"代表的是除jsp请求之外的所有请求,也就是说访问应用中的静态资源,也会走DispatcherServlet,这会导致404错误,无法访问静态资源,如何解决,两种方案:
- 使用默认 Servlet 处理静态资源
- 使用
mvc:resources
标签配置静态资源处理
这两种方式都可以。自行选择。
5.7.1 使用默认Servlet处理静态资源
首先需要在springmvc.xml文件中添加以下配置,开启 默认Servlet处理静态资源
功能:
<!-- 开启注解驱动 -->
<mvc:annotation-driven />
<!--开启默认Servlet处理-->
<mvc:default-servlet-handler>
然后在web.xml文件中指定什么样的路径走其他Servlet:
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
以上配置url-pattern使用的也是"/",和DispatcherServlet一样。表示的含义是:同一个请求路径,先走DispatcherServlet,如果找不到则走默认的Servlet。
默认的 Servlet 类中的代码已经由 Tomcat 服务器提供了实现,一般不需要开发者自己编写。在上面的示例中,我们指定了 org.apache.catalina.servlets.DefaultServlet
,则 Tomcat 服务器会自动将请求转发给该类处理。在处理时,该类会根据请求的 URL 去查询 Web 应用的静态资源(如 HTML、CSS、JavaScript 和图片等),并将其返回给用户。
告诉大家一个好消息,以上在web.xml文件中的配置我们也可以省略了,因为在Tomcat服务器中已经为我们提前配置好了,在CATALINA_HOME/conf/web.xml文件中,如下:
因此我们只需要在springmvc.xml文件中启用这个默认的Servlet即可:<mvc:default-servlet-handler>
5.7.2 使用 mvc:resources 标签配置静态资源
访问静态资源,也可以在springmvc.xml文件中添加如下的配置:
<!-- 开启注解驱动 -->
<mvc:annotation-driven />
<!-- 配置静态资源处理 -->
<mvc:resources mapping="/static/**" location="/static/" />
表示凡是请求路径是"/static/“开始的,都会去”/static/"目录下找该资源。
注意:要想使用 <mvc:resources>
配置,必须开启注解驱动 <mvc:annotation-driven />
5.8 课堂笔记
20. SpringMVC中常用的视图:
InternalResourceView:内部资源视图(是SpringMVC内置的,专门负责解析JSP模板语法的,另外也负责 转发forward 功能的实现)
RedirectView:重定向视图(是SpringMVC内置的,专门负责 重定向redirect 功能的实现)
ThymeleafView:Thymeleaf视图(是第三方的,专门负责解析Thymeleaf模板语法的)
......
21. 实现视图机制的核心类与核心接口
1. DispatcherServlet:前端控制器
负责接收前端的请求 (/login)
根据请求路径找到对应的处理器方法 (UserController#login())
执行处理器方法 (执行 UserController#login())
并且最终返回ModelAndView对象。
再往下就是处理视图。
2. ViewResolver接口:视图解析器接口 (ThymeleafViewResolver实现了ViewResolver接口、InternalResourceViewResolver也是实现了ViewResolver接口....)
这个接口做什么事儿?
这个接口作用是将 逻辑视图名称 转换为 物理视图名称。
并且最终返回一个View接口对象。
核心方法是什么?
View resolveViewName(String viewName, Locale locale) throws Exception;
3. View接口:视图接口 (ThymeleafView实现了View接口、InternalResourceView也实现了View接口.....)
这个接口做什么事儿?
这个接口负责将模板语法的字符串转换成html代码,并且将html代码响应给浏览器。(渲染。)
核心方法是什么?
void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
22. 在SpringMVC中是怎么通过代码完成转发的?怎么完成重定向的?
@RequestMapping("/a")
public String toA(){
// 返回的是一个逻辑视图名称
return "a";
}
注意:当 return "a"; 的时候,返回了一个逻辑视图名称。这种方式跳转到视图,默认采用的就是 forward 方式跳转过去的。只不过这个底层创建的视图对象:ThymeleafView
怎么转发?语法格式是什么呢?
return "forward:/b"; 转发到 /b,这是一次请求,底层创建的视图对象是:InternalResourceView对象。
"forward:/b" 这个已经不是逻辑视图名称了。是以转发的方式跳转,是一个资源的路径。不能随便写,以 forward: 开始。
怎么重定向?语法格式是什么呢?
return "redirect:/b"; 转发到 /b,这是两次请求,底层创建的视图对象是:RedirectView对象。
注意语法:必须以 redirect: 开始。
总结:
转发: return "forward:/b" 底层创建的是InternalResourceView对象
重定向: return "redirect:/b" 底层创建的是RedirectView对象
23. <mvc:view-controller>
这个配置信息,可以在springmvc.xml文件中进行配置,作用是什么?
如果一个Controller上的方法只是为了完成视图跳转,没有任何业务代码,那么这个Controller可以不写。
直接在 springmvc.xml 文件中添加 <mvc:view-controller> 即可。
<mvc:view-controller path="/test" view-name="test"/>
表示发送的请求路径是 /test,跳转的视图页面是: 前缀+test+后缀
24. <mvc:annotation-driven/>
这个配置信息叫做开启注解驱动。在springmvc.xml文件中配置。
当你使用了 <mvc:view-controller> 配置,会让你整个项目中所有的注解全部失效,你需要使用以上的配置来再次开启注解。
<mvc:annotation-driven/>
25. 关于静态资源处理:
假设我们在webapp目录下有static目录,static目录下有touxiang.jpeg图片。
我们可以在浏览器地址栏上直接访问:http://localhost:8080/springmvc/static/img/touxiang.jpeg 吗?不行。
因为会走DispatcherServlet,导致发生404错误。
怎么办?两种解决方案:
第一种解决方案:开启默认的Servlet处理
在Tomcat服务器中提供了一个默认的Servlet,叫做:org.apache.catalina.servlets.DefaultServlet
在CATALINA_HOME/conf/web.xml文件中,有这个默认的Servlet的相关配置。
不过,这个默认的Servlet默认情况下是不开启的。
你需要在springmvc.xml文件中使用以下配置进行开启:
<mvc:default-servlet-handler/>
<mvc:annotation-driven/>
开启之后的作用是,当你访问 http://localhost:8080/springmvc/static/img/touxiang.jpeg的时候,
默认先走 DispatcherServlet,如果发生404错误的话,会自动走DefaultServlet,然后DefaultServlet
帮你定位静态资源。
第二种解决方案:配置静态资源处理,在springmvc.xml文件中添加如下配置:
<mvc:resources mapping="/static/**" location="/static/" />
<mvc:annotation-driven/>
当请求路径符合 /static/** 的时候,去 /static/ 位置找资源。
第6章 RESTFul编程风格
6.1 RESTFul编程风格
6.1.1 RESTFul是什么
RESTFul是WEB服务接口
的一种设计风格。
RESTFul定义了一组约束条件和规范,可以让WEB服务接口
更加简洁、易于理解、易于扩展、安全可靠。
RESTFul对一个WEB服务接口
都规定了哪些东西?
- 对请求的URL格式有约束和规范
- 对HTTP的请求方式有约束和规范
- 对请求和响应的数据格式有约束和规范
- 对HTTP状态码有约束和规范
- 等 …
REST对请求方式的约束是这样的:
- 查询必须发送GET请求
- 新增必须发送POST请求
- 修改必须发送PUT请求
- 删除必须发送DELETE请求
REST对URL的约束是这样的:
-
传统的URL:get请求,/springmvc/getUserById?id=1
-
REST风格的URL:get请求,/springmvc/user/1
-
传统的URL:get请求,/springmvc/deleteUserById?id=1
-
REST风格的URL:delete请求, /springmvc/user/1
RESTFul对URL的约束和规范的核心是:通过采用 不同的请求方式
+ URL
来确定WEB服务中的资源。
RESTful 的英文全称是 Representational State Transfer(表述性状态转移)。简称REST。
表述性(Representational)是:URI + 请求方式。
状态(State)是:服务器端的数据。
转移(Transfer)是:变化。
表述性状态转移是指:通过 URI + 请求方式 来控制服务器端数据的变化。
6.1.2 RESTFul风格与传统方式对比
传统的 URL 与 RESTful URL 的区别是传统的 URL 是基于方法名进行资源访问和操作,而 RESTful URL 是基于资源的结构和状态进行操作的。下面是一张表格,展示两者之间的具体区别:
传统的 URL | RESTful URL |
---|---|
GET /getUserById?id=1 | GET /user/1 |
GET /getAllUser | GET /user |
POST /addUser | POST /user |
POST /modifyUser | PUT /user |
GET /deleteUserById?id=1 | DELETE /user/1 |
从上表中我们可以看出,传统的URL是基于动作的,而 RESTful URL 是基于资源和状态的,因此 RESTful URL 更加清晰和易于理解,这也是 REST 架构风格被广泛使用的主要原因之一。
6.1.3 RESTFul方式演示查询
RESTFul规范中规定,如果要查询数据,需要发送GET请求。
6.1.3.1 根据id查询(GET /api/user/1)
<?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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.springmvc.controller"/>
<!--视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="characterEncoding" value="UTF-8"/>
<property name="order" value="1"/>
<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/thymeleaf/"/>
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
<!--启用注解-->
<mvc:annotation-driven/>
<!--视图控制器映射-->
<mvc:view-controller path="/" view-name="index"/>
</beans>
首页index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>index page</h1>
<hr>
<!--根据id查询:GET /api/user/1 -->
<a th:href="@{/api/user/1}">根据id查询用户信息</a><br>
</body>
</html>
控制器Controller:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class UserController {
@RequestMapping(value = "/api/user/{id}", method = RequestMethod.GET)
public String getById(@PathVariable("id") Integer id){
System.out.println("根据用户id查询用户信息,用户id是" + id);
return "ok";
}
}
视图页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ok</title>
</head>
<body>
<h1>ok</h1>
</body>
</html>
启动服务器,测试:http://localhost:8080/springmvc
6.1.3.2 查询所有(GET /api/user)
<!--查询所有-->
<a th:href="@{/api/user}">查询所有</a><br>
@RequestMapping(value = "/api/user", method = RequestMethod.GET)
public String getAll(){
System.out.println("查询所有用户信息");
return "ok";
}
启动服务器测试:
6.1.4 RESTFul方式演示增加(POST /api/user)
RESTFul规范中规定,如果要进行保存操作,需要发送POST请求。
<!--保存用户-->
<form th:action="@{/api/user}" method="post">
<input type="submit" th:value="保存">
</form>
@RequestMapping(value = "/api/user", method = RequestMethod.POST)
public String save(){
System.out.println("保存用户信息");
return "ok";
}
启动服务器测试:
6.1.5 RESTFul方式演示修改
RESTFul规范中规定,如果要进行保存操作,需要发送PUT请求。
如何发送PUT请求?
第一步:首先你必须是一个POST请求。
第二步:在发送POST请求的时候,提交这样的数据: _method=PUT
第三步:在web.xml文件配置SpringMVC提供的过滤器:HiddenHttpMethodFilter
实践一下:
<!--修改用户-->
<hr>
<form th:action="@{/api/user}" method="post">
<!--隐藏域的方式提交 _method=put -->
<input type="hidden" name="_method" value="put">
用户名:<input type="text" name="username"><br>
<input type="submit" th:value="修改">
</form>
<!--隐藏的HTTP请求方式过滤器-->
<!--添加一个过滤器,这个过滤器是springmvc提前写好的,直接用就行了,这个过滤器可以帮助你将请求POST转换成PUT请求/DELETE请求-->
<!--一定要在字符编码过滤器后面配置。-->
<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>
@RequestMapping(value = "/api/user", method = RequestMethod.PUT)
public String update(String username){
System.out.println("修改用户信息,用户名:" + username);
return "ok";
}
注意pom.xml文件中添加如下配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>21</source>
<target>21</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
一定要重新build一下:
测试结果:
6.1.6 RESTFul方式演示删除
当删除
是个超链接时,超链接只能发送GET请求。可以通过给超链接添加鼠标单击事件onclick,执行一段JS代码提交form表单
通过form提交DELETE请求的前提是POST请求,并且需要通过隐藏域提交 _method=delete
需要注意阻止超链接的默认行为,否则默认会跳转走
<!--RESTful风格的:删除用户信息-->
<!--删除必须发送DELETE请求。和PUT请求实现方式相同。-->
<!--发送DELETE请求的前提是POST请求,并且需要通过隐藏域提交 _method=delete -->
<a th:href="@{/user/120}" onclick="del(event)">删除用户id=120的用户信息</a>
<form id="delForm" method="post">
<input type="hidden" name="_method" value="delete">
</form>
<script>
function del(event){
// 获取表单
let delForm = document.getElementById("delForm");
// 给form的action赋值
delForm.action = event.target.href;
// 发送POST请求提交表单
delForm.submit();
// 非常重要,你需要阻止超链接的默认行为。不然会跳转走
event.preventDefault();
}
</script>
@RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
public String del(@PathVariable("id") String id){
System.out.println("正在删除用户:" + id);
return "ok";
}
6.1.7 HiddenHttpMethodFilter
HiddenHttpMethodFilter是Spring MVC框架提供的,专门用于RESTFul编程风格。
实现原理可以通过源码查看:
通过源码可以看到,if语句中,首先判断是否为POST请求,如果是POST请求,调用request.getParameter(this.methodParam)
。可以看到this.methodParam
是_method
,这样就要求我们在提交请求方式的时候必须采用这个格式:_method=put
。获取到请求方式之后,调用了toUpperCase转换成大写了。因此前端页面中小写的put或者大写的PUT都是可以的。if语句中嵌套的if语句说的是,只有请求方式是 PUT,DELETE,PATCH的时候会创建HttpMethodRequestWrapper对象。而HttpMethodRequestWrapper对象的构造方法是这样的:
这样method就从POST变成了:PUT/DELETE/PATCH。
重点注意事项:CharacterEncodingFilter和HiddenHttpMethodFilter的顺序
细心的同学应该注意到了,在HiddenHttpMethodFilter源码中有这样一行代码:
大家是否还记得,字符编码过滤器执行之前不能调用 request.getParameter方法,如果提前调用了,乱码问题就无法解决了。因为request.setCharacterEncoding()方法的执行必须在所有request.getParameter()方法之前执行。因此这两个过滤器就有先后顺序的要求,在web.xml文件中,应该先配置CharacterEncodingFilter,然后再配置HiddenHttpMethodFilter。
6.2 使用RESTFul实现用户管理系统
6.2.1 静态页面准备
文件包括:user.css、user_index.html、user_list.html、user_add.html、user_edit.html。代码如下:
6.2.1.1 user.css
.header {
background-color: #f2f2f2;
padding: 20px;
text-align: center;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
overflow: hidden;
background-color: #333;
}
li {
float: left;
}
li a {
display: block;
color: white;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
li a:hover:not(.active) {
background-color: #111;
}
.active {
background-color: #4CAF50;
}
form {
width: 50%;
margin: 0 auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
label {
display: block;
margin-bottom: 8px;
}
input[type="text"], input[type="email"], select {
width: 100%;
padding: 6px 10px;
margin: 8px 0;
box-sizing: border-box;
border: 1px solid #555;
border-radius: 4px;
font-size: 16px;
}
button[type="submit"] {
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
button[type="submit"]:hover {
background-color: #3e8e41;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
.header {
background-color: #f2f2f2;
padding: 20px;
text-align: center;
}
a {
text-decoration: none;
color: #333;
}
.add-button {
margin-bottom: 20px;
padding: 10px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-button:hover {
background-color: #3e8e41;
}
6.2.1.2 user_index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户管理系统</title>
<link rel="stylesheet" href="user.css" type="text/css"></link>
</head>
<body>
<div class="header">
<h1>用户管理系统</h1>
</div>
<ul>
<li><a class="active" href="user_list.html">用户列表</a></li>
</ul>
</body>
</html>
6.2.1.3 user_list.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户列表</title>
<link rel="stylesheet" href="user.css" type="text/css"></link>
</head>
<body>
<div class="header">
<h1>用户列表</h1>
</div>
<div class="add-button-wrapper">
<a class="add-button" href="user_add.html">新增用户</a>
</div>
<table>
<thead>
<tr>
<th>编号</th>
<th>用户名</th>
<th>性别</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>张三</td>
<td>男</td>
<td>zhangsan@powernode.com</td>
<td>
修改
删除
</td>
</tr>
<tr>
<td>2</td>
<td>李四</td>
<td>女</td>
<td>lisi@powernode.com</td>
<td>
修改
删除
</td>
</tr>
</tbody>
</table>
</body>
</html>
6.2.1.4 user_add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>新增用户</title>
<link rel="stylesheet" href="user.css" type="text/css"></link>
</head>
<body>
<h1>新增用户</h1>
<form>
<label>用户名:</label>
<input type="text" name="username" required>
<label>性别:</label>
<select name="gender" required>
<option value="">-- 请选择 --</option>
<option value="1">男</option>
<option value="0">女</option>
</select>
<label>邮箱:</label>
<input type="email" name="email" required>
<button type="submit">保存</button>
</form>
</body>
</html>
6.2.1.5 user_edit.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>修改用户</title>
<link rel="stylesheet" href="user.css" type="text/css"></link>
</head>
<body>
<h1>修改用户</h1>
<form>
<label>用户名:</label>
<input type="text" name="username" value="张三" required>
<label>性别:</label>
<select name="gender" required>
<option value="">-- 请选择 --</option>
<option value="1" selected>男</option>
<option value="0">女</option>
</select>
<label>邮箱:</label>
<input type="email" name="email" value="zhangsan@powernode.com" required>
<button type="submit">修改</button>
</form>
</body>
</html>
6.2.2 SpringMVC环境搭建
6.2.2.1 创建module:usermgt
<?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.powernode</groupId>
<artifactId>usermgt</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!--springmvc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.5</version>
</dependency>
<!--servlet api-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
</dependency>
<!--logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.3</version>
</dependency>
<!--thymeleaf+spring6整合依赖-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
6.2.2.2 添加web支持
6.2.2.3 配置web.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<!--字符编码过滤器-->
<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>forceRequestEncoding</param-name>
<param-value>true</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>
<!--HTTP请求方式过滤器-->
<!--HiddenHttpMethodFilter-->
<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>
<!--前端控制器-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
注意两个过滤器Filter的配置顺序:
- 先配置 CharacterEncodingFilter
- 再配置 HiddenHttpMethodFilter
6.2.2.4 配置springmvc.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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.usermgt.controller,com.powernode.usermgt.dao"/>
<!--视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="characterEncoding" value="UTF-8"/>
<property name="order" value="1"/>
<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/thymeleaf/"/>
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
<!--视图控制器-->
<mvc:view-controller path="/" view-name="user_index"/>
<mvc:view-controller path="/toAdd" view-name="user_add"/>
<!--处理静态资源:开启默认的Servlet处理-->
<mvc:default-servlet-handler/>
<!--开启注解驱动-->
<mvc:annotation-driven/>
</beans>
在WEB-INF目录下新建:thymeleaf目录
创建package:
6.2.3 显示首页
在应用的根下新建目录:static,将user.css文件拷贝进去。
将user_index.html拷贝到WEB-INF/thymeleaf目录下:
代码有两处需要修改:
重要:在springmvc.xml文件中配置视图控制器映射:
<!--视图控制器映射-->
<mvc:view-controller path="/" view-name="user_index"/>
部署,启动服务器,测试:
6.2.4 实现用户列表
修改user_index.html中的超链接:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户管理系统</title>
<link rel="stylesheet" th:href="@{/static/user.css}" type="text/css"></link>
</head>
<body>
<div class="header">
<h1>用户管理系统</h1>
</div>
<ul>
<li><a class="active" th:href="@{/user}">用户列表</a></li>
</ul>
</body>
</html>
编写bean:User
package com.powernode.usermgt.bean;
public class User {
/**
* 用户编号
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 性别
* 1表示男
* 0表示女
*/
private Integer sex;
/**
* 邮箱
*/
private String email;
public User() {
}
public User(Long id, String username, Integer sex, String email) {
this.id = id;
this.username = username;
this.sex = sex;
this.email = email;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", sex=" + sex +
", email='" + email + '\'' +
'}';
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
编写UserDao,提供selectAll方法:
package com.powernode.usermgt.dao;
import com.powernode.usermgt.bean.User;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
@Repository
public class UserDao {
private static List<User> users = new ArrayList<>();
static {
User user1 = new User(10001L, "张三", "zhangsan@powernode.com", 1);
User user2 = new User(10002L, "李四", "lisi@powernode.com", 1);
User user3 = new User(10003L, "王五", "wangwu@powernode.com", 1);
User user4 = new User(10004L, "赵六", "zhaoliu@powernode.com", 0);
User user5 = new User(10005L, "钱七", "qianqi@powernode.com", 0);
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
users.add(user5);
}
public List<User> selectAll(){
return users;
}
}
编写控制器UserController:
package com.powernode.usermgt.controller;
import com.powernode.usermgt.bean.User;
import com.powernode.usermgt.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@Controller
public class UserController {
@Autowired
private UserDao userDao;
@GetMapping("/user")
public String list(Model model){
// 获取所有的用户
List<User> users = userDao.selectAll();
// 存储到request域
model.addAttribute("users", users);
// 跳转视图
return "user_list";
}
}
将user_list.html拷贝到thymeleaf目录下,并进行代码修改,显示用户列表:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户列表</title>
<link rel="stylesheet" th:href="@{/static/user.css}" type="text/css"></link>
</head>
<body>
<div class="header">
<h1>用户列表</h1>
</div>
<div class="add-button-wrapper">
<a class="add-button" href="user_add.html">新增用户</a>
</div>
<table>
<thead>
<tr>
<th>编号</th>
<th>用户名</th>
<th>性别</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.id}"></td>
<td th:text="${user.name}"></td>
<td th:text="${user.gender == 1 ? '男' : '女'}"></td>
<td th:text="${user.email}"></td>
<td>
<a href="">修改</a>
<a href="">删除</a>
</td>
</tr>
</tbody>
</table>
</body>
</html>
测试结果:
6.2.5 实现新增功能
6.2.5.1 跳转到新增页面
在用户列表页面,修改新增用户
的超链接:
将user_add.html拷贝到thymeleaf目录下,并进行代码修改如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http:www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>新增用户</title>
<link rel="stylesheet" th:href="@{/static/user.css}" type="text/css"></link>
</head>
<body>
<h1>新增用户</h1>
<form>
<label>用户名:</label>
<input type="text" name="username" required>
<label>性别:</label>
<select name="gender" required>
<option value="">-- 请选择 --</option>
<option value="1">男</option>
<option value="0">女</option>
</select>
<label>邮箱:</label>
<input type="email" name="email" required>
<button type="submit">保存</button>
</form>
</body>
</html>
在springmvc.xml文件中配置视图控制器映射
:
<mvc:view-controller path="/toSave" view-name="user_add"/>
启动服务器测试:
6.2.5.2 实现新增功能
前端页面发送POST请求,提交表单,user_add.html代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http:www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>新增用户</title>
<link rel="stylesheet" th:href="@{/static/user.css}" type="text/css"></link>
</head>
<body>
<h1>新增用户</h1>
<form th:action="@{/user}" method="post">
<label>用户名:</label>
<input type="text" name="name" required>
<label>性别:</label>
<select name="gender" required>
<option value="">-- 请选择 --</option>
<option value="1">男</option>
<option value="0">女</option>
</select>
<label>邮箱:</label>
<input type="email" name="email" required>
<button type="submit">保存</button>
</form>
</body>
</html>
编写控制器UserController:
@PostMapping("/user")
public String save(User user){
// 调用userDao保存用户信息
userDao.save(user);
// 重定向到用户列表页面(重新让浏览器发送一次全新的请求,去请求列表页面)
return "redirect:/user";
}
注意:保存成功后,采用重定向的方式跳转到用户列表。
编写UserDao:
public static Long generateId(){
// 使用Stream API
Long maxId = users.stream().map(user -> user.getId()).reduce((id1, id2) -> id1 > id2 ? id1 : id2).get();
return maxId + 1;
}
public void save(User user){
// 设置id
user.setId(generateId());
// 保存
users.add(user);
}
注意:单独写了一个方法生成id,内部使用了Stream API,不会这块内容的可以看老杜最新发布的2024版JavaSE。
启动服务器测试:
6.2.6 跳转到修改页面
修改user_list.html中修改
超链接:
<a th:href="@{'/user/' + ${user.id}}">修改</a>
编写Controller:
@GetMapping("/user/{id}")
public String toUpdate(@PathVariable("id") Long id, Model model){
// 根据id查询用户信息
User user = userDao.selectById(id);
// 将对象存储到request域
model.addAttribute("user", user);
// 转发到视图
return "user_edit";
}
编写UserDao:
public User selectById(Long id){
// Stream API
return users.stream().filter(user -> user.getId().equals(id)).findFirst().get();
}
将user_edit.html拷贝thymeleaf目录下,并修改代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>修改用户</title>
<link rel="stylesheet" th:href="@{/static/user.css}" type="text/css"></link>
</head>
<body>
<h1>修改用户</h1>
<form>
<label>用户名:</label>
<input type="text" name="username" th:value="${user.name}" required>
<label>性别:</label>
<select name="gender" required>
<option value="">-- 请选择 --</option>
<option value="1" th:field="${user.gender}">男</option>
<option value="0" th:field="${user.gender}">女</option>
</select>
<label>邮箱:</label>
<input type="email" name="email" th:value="${user.email}" required>
<button type="submit">修改</button>
</form>
</body>
</html>
启动服务器测试:
6.2.7 实现修改功能
将user_edit.html页面中的form表单修改一下,添加action,添加method,隐藏域的方式提交请求方式put,隐藏域的方式提交id:
<form th:action="@{/user}" method="post">
<!--隐藏域:设置请求方式为put请求-->
<input type="hidden" name="_method" value="put">
<!--隐藏域:提交id-->
<input type="hidden" name="id" th:value="${user.id}">
<label>用户名:</label>
<input type="text" name="name" th:value="${user.name}" required>
<label>性别:</label>
<select name="gender" required>
<option value="">-- 请选择 --</option>
<option value="1" th:field="${user.gender}">男</option>
<option value="0" th:field="${user.gender}">女</option>
</select>
<label>邮箱:</label>
<input type="email" name="email" th:value="${user.email}" required>
<button type="submit">修改</button>
</form>
编写Controller:
@PutMapping("/user")
public String modify(User user){
// 更新数据
userDao.update(user);
// 重定向到列表页面
return "redirect:/user";
}
编写UserDao:
public void update(User user){
for (int i = 0; i < users.size(); i++) {
if(user.getId().equals(users.get(i).getId())){
users.set(i, user);
break;
}
}
}
启动服务器测试:
6.2.8 实现删除功能
删除应该发送DELETE请求,要模拟DELETE请求,就需要使用表单方式提交。因此我们点击删除
超链接时需要采用表单方式提交。
在user_list.html页面添加form表单,并且点击超链接时应该提交表单,代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户列表</title>
<link rel="stylesheet" th:href="@{/static/user.css}" type="text/css"></link>
</head>
<body>
<div class="header">
<h1>用户列表</h1>
</div>
<div class="add-button-wrapper">
<a class="add-button" th:href="@{/toSave}">新增用户</a>
</div>
<table>
<thead>
<tr>
<th>编号</th>
<th>用户名</th>
<th>性别</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${users}">
<td th:text="${user.id}"></td>
<td th:text="${user.name}"></td>
<td th:text="${user.gender == 1 ? '男' : '女'}"></td>
<td th:text="${user.email}"></td>
<td>
<a th:href="@{'/user/' + ${user.id}}">修改</a>
<!--为删除提供一个鼠标单击事件-->
<a th:href="@{'/user/' + ${user.id}}" onclick="del(event)">删除</a>
</td>
</tr>
</tbody>
</table>
<!--为删除操作准备一个form表单,点击删除时提交form表单-->
<div style="display: none">
<form method="post" id="delForm">
<input type="hidden" name="_method" value="delete"/>
</form>
</div>
<script>
function del(event){
// 获取表单
let delForm = document.getElementById("delForm");
// 设置表单action
delForm.action = event.target.href;
if(window.confirm("您确定要删除吗?")){
// 提交表单
delForm.submit();
}
// 阻止超链接默认行为
event.preventDefault();
}
</script>
</body>
</html>
编写Controller:
@DeleteMapping("/user/{id}")
public String del(@PathVariable("id") Long id){
// 调用dao删除用户
userDao.deleteById(id);
// 重定向到列表
return "redirect:/user";
}
编写UserDao:
public void deleteById(Long id){
for (int i = 0; i < users.size(); i++) {
if(id.equals(users.get(i).getId())){
users.remove(i);
break;
}
}
}
启动服务器测试:
6.3 课堂笔记
26. 什么是RESTFul?
RESTful是对WEB服务接口的一种设计风格,提供了一套约束,可以让WEB服务接口更加简洁、易于理解。
REST对请求方式的约束是这样的:
查询get
新增POST
删除delete
修改put
REST对URL的约束是这样的:
GET /user/1 查一个
GET /user 查所有
POST /user 新增
PUT /user 修改
DELETE /user/1 删除
RESTful:是表述性状态转移。
本质上:通过 URL + 请求方式 来控制服务器端数据的变化。
27. RESTful编程风格中要求,修改的时候,必须提交PUT请求,怎么提交PUT请求呢?
第一步:要想发送PUT请求,首先你必须是一个POST请求。(POST请求是大前提)
第二步:在POST请求的表单中添加隐藏域:
<!--隐藏域-->
<input type="hidden" name="_method" value="put">
强调:name必须是 _method,value必须是put/PUT。
如果你要发送delete请求的话,value写delete即可。
第三步:添加一个过滤器
<!--添加一个过滤器,这个过滤器是springmvc提前写好的,直接用就行了,这个过滤器可以帮助你将请求POST转换成PUT请求/DELETE请求-->
<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>
第7章 HttpMessageConverte
7.1 HttpMessageConverter
HttpMessageConverter是Spring MVC中非常重要的一个接口。翻译为:HTTP消息转换器。该接口下提供了很多实现类,不同的实现类有不同的转换方式。
7.1.1 什么是HTTP消息
HTTP消息其实就是HTTP协议。HTTP协议包括请求协议和响应协议。
以下是一份HTTP POST请求协议:
POST /springmvc/user/login HTTP/1.1 --请求行
Content-Type: application/x-www-form-urlencoded --请求头
Content-Length: 32
Host: www.example.com
User-Agent: Mozilla/5.0
Connection: Keep-Alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
--空白行
username=admin&password=1234 --请求体
以下是一份HTTP GET请求协议:
GET /springmvc/user/del?id=1&name=zhangsan HTTP/1.1 --请求行
Host: www.example.com --请求头
User-Agent: Mozilla/5.0
Connection: Keep-Alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
以下是一份HTTP响应协议:
HTTP/1.1 200 OK --状态行
Date: Thu, 01 Jul 2021 06:35:45 GMT --响应头
Content-Type: text/plain; charset=utf-8
Content-Length: 12
Connection: keep-alive
Server: Apache/2.4.43 (Win64) OpenSSL/1.1.1g
--空白行
<!DOCTYPE html> --响应体
<html>
<head>
<title>hello</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
7.1.2 转换器转换的是什么
转换的是HTTP协议
与Java程序中的对象
之间的互相转换。请看下图:
上图是我们之前经常写的代码。请求体中的数据是如何转换成user对象的,底层实际上使用了HttpMessageConverter
接口的其中一个实现类FormHttpMessageConverter
。
通过上图可以看出FormHttpMessageConverter
是负责将请求协议
转换为Java对象
的。
再看下图:
上图的代码也是之前我们经常写的,Controller返回值看做逻辑视图名称,视图解析器将其转换成物理视图名称,生成视图对象,StringHttpMessageConverter
负责将视图对象中的HTML字符串写入到HTTP协议的响应体中。最终完成响应。
通过上图可以看出StringHttpMessageConverter
是负责将Java对象
转换为响应协议
的。
通过以上内容的学习,大家应该能够了解到HttpMessageConverter
接口是用来做什么的了:
如上图所示:HttpMessageConverter接口的可以将请求协议转换成Java对象,也可以把Java对象转换为响应协议。
HttpMessageConverter是接口,SpringMVC帮我们提供了非常多而丰富的实现类。每个实现类都有自己不同的转换风格。
对于我们程序员来说,Spring MVC已经帮助我们写好了,我们只需要在不同的业务场景下,选择合适的HTTP消息转换器即可。
怎么选择呢?当然是通过SpringMVC为我们提供的注解,我们通过使用不同的注解来启用不同的消息转换器。
在HTTP消息转换器这一小节,我们重点要掌握的是两个注解两个类:
- @ResponseBody
- @RequestBody
- ResponseEntity
- RequestEntity
7.2 Spring MVC中的AJAX请求
SpringMVC+Vue3+Thymeleaf+Axios发送一个简单的AJAX请求。
引入Vue和Axios的js文件:
<?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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.springmvc.controller"/>
<!--视图解析器-->
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="characterEncoding" value="UTF-8"/>
<property name="order" value="1"/>
<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/thymeleaf/"/>
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
</property>
</bean>
</property>
</bean>
<!--开启注解驱动-->
<mvc:annotation-driven/>
<!--视图控制器映射-->
<mvc:view-controller path="/" view-name="index"/>
<!--静态资源处理,开启默认的Servlet-->
<mvc:default-servlet-handler/>
</beans>
重点是静态资源处理、开启注解驱动、视图控制器映射等相关配置。
Vue3+Thymeleaf+Axios发送AJAX请求:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script th:src="@{/static/js/vue3.4.21.js}"></script>
<script th:src="@{/static/js/axios.min.js}"></script>
</head>
<body>
<h1>首页</h1>
<hr>
<div id="app">
<h1>{{message}}</h1>
<button @click="getMessage">获取消息</button>
</div>
<script th:inline="javascript">
Vue.createApp({
data(){
return {
message : "这里的信息将被刷新"
}
},
methods:{
// 异步方法(ajax请求多数情况下都是异步请求)
async getMessage(){
try {
// 发送ajax请求
const response = await axios.get([[@{/}]] + 'hello')
// 将返回的数据交给message
this.message = response.data
}catch (e) {
console.error(e)
}
}
}
}).mount("#app")
</script>
</body>
</html>
重点来了,Controller怎么写呢,之前我们都是传统的请求,Controller返回一个逻辑视图名
,然后交给视图解析器
解析。最后跳转页面。而AJAX请求是不需要跳转页面的,因为AJAX是页面局部刷新,以前我们在Servlet中使用response.getWriter().print("message")
的方式响应。在Spring MVC中怎么办呢?当然,我们在Spring MVC中也可以使用Servlet原生API来完成这个功能,代码如下:
package com.powernode.springmvc.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.io.IOException;
@Controller
public class HelloController {
//@RequestMapping(value = "/hello", method = RequestMethod.GET)
@GetMapping("/hello")
public String hello(HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
out.print("hello ajax, my name is Spring MVC!");
return null;
}
}
或者这样也行:不需要有返回值
package com.powernode.springmvc.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import java.io.IOException;
@Controller
public class HelloController {
//@RequestMapping(value = "/hello", method = RequestMethod.GET)
@GetMapping("/hello")
public void hello(HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
out.print("hello ajax, my name is Spring MVC!");
}
}
启动服务器测试:http://localhost:8080/springmvc/
注意:如果采用这种方式响应,则和 springmvc.xml 文件中配置的视图解析器没有关系,不走视图解析器了。
难道我们以后AJAX请求都要使用原生Servlet API吗?
- 不需要,我们可以使用SpringMVC中提供的HttpMessageConverter消息转换器。
我们要向前端响应一个字符串"hello",这个"hello"就是响应协议中的响应体。
我们可以使用 @ResponseBody 注解来启用对应的消息转换器。而这种消息转换器只负责将Controller返回的信息以响应体的形式写入响应协议。
7.3 @ResponseBody
7.3.1 StringHttpMessageConverter
上面的AJAX案例,Controller的代码可以修改为:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@RequestMapping(value = "/hello")
@ResponseBody
public String hello(){
// 由于你使用了 @ResponseBody 注解
// 以下的return语句返回的字符串则不再是“逻辑视图名”了
// 而是作为响应协议的响应体进行响应。
return "hello";
}
}
最核心需要理解的位置是:return “hello”;
这里的"hello"不是逻辑视图名了,而是作为响应体的内容进行响应。直接输出到浏览器客户端。
以上程序中使用的消息转换器是:StringHttpMessageConverter,为什么会启用这个消息转换器呢?因为你添加@ResponseBody
这个注解。
通常AJAX请求需要服务器给返回一段JSON格式的字符串,可以返回JSON格式的字符串吗?当然可以,代码如下:
package com.powernode.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@RequestMapping(value = "/hello")
@ResponseBody
public String hello(){
return "{\"username\":\"zhangsan\",\"password\":\"1234\"}";
}
}
测试:
这是完全可以的,此时底层使用的消息转换器还是:StringHttpMessageConverter
那如果在程序中是一个POJO对象,怎么将POJO对象以JSON格式的字符串响应给浏览器呢?两种方式:
- 第一种方式:自己写代码将POJO对象转换成JSON格式的字符串,用上面的方式直接return即可。
- 第二种方式:启用
MappingJackson2HttpMessageConverter
消息转换器。
7.3.2 MappingJackson2HttpMessageConverter
启用MappingJackson2HttpMessageConverter消息转换器的步骤如下:
第一步:引入jackson依赖,可以将java对象转换为json格式字符串
<!--专门负责将java对象转换成json格式字符串的组件,当然,它也可以将json格式的字符串转换成java对象。-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
第二步:开启注解驱动
这一步非常关键,开启注解驱动后,在HandlerAdapter中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter
<mvc:annotation-driven/>
第三步:准备一个POJO
package com.powernode.springmvc.bean;
public class User {
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
第四步:控制器方法使用 @ResponseBody 注解标注(非常重要),控制器方法返回这个POJO对象
package com.powernode.springmvc.controller;
import com.powernode.springmvc.bean.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@RequestMapping(value = "/hello")
@ResponseBody
public User hello(){
User user = new User("zhangsan", "22222");
return user;
}
}
测试:
以上代码底层启用的就是 MappingJackson2HttpMessageConverter 消息转换器。
他的功能很强大,可以将POJO对象转换成JSON格式的字符串,响应给前端。
其实这个消息转换器MappingJackson2HttpMessageConverter
本质上只是比StringHttpMessageConverter
多了一个json字符串的转换,其他的还是一样。
7.4 @RestController
因为我们现代的开发方式都是基于AJAX方式的,因此 @ResponseBody 注解非常重要,很常用。
为了方便,Spring MVC中提供了一个注解 @RestController。这一个注解代表了:@Controller + @ResponseBody。
@RestController 标注在类上即可。被它标注的Controller中所有的方法上都会自动标注 @ResponseBody
package com.powernode.springmvc.controller;
import com.powernode.springmvc.bean.User;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping(value = "/hello")
public User hello(){
User user = new User("zhangsan", "22222");
return user;
}
}
测试:
7.5 @RequestBody
7.5.1 FormHttpMessageConverter
这个注解的作用是直接将请求体传递给Java程序,在Java程序中可以直接使用一个String类型的变量接收这个请求体的内容。
在没有使用这个注解的时候:
@RequestMapping("/save")
public String save(User user){
// 执行保存的业务逻辑
userDao.save(user);
// 保存成功跳转到成功页面
return "success";
}
当请求体提交的数据是:
username=zhangsan&password=1234&email=zhangsan@powernode.com
那么Spring MVC会自动使用 FormHttpMessageConverter
消息转换器,将请求体转换成user对象。
当使用这个注解的时候:这个注解只能出现在方法的参数上。
@RequestMapping("/save")
public String save(@RequestBody String requestBodyStr){
System.out.println("请求体:" + requestBodyStr);
return "success";
}
Spring MVC仍然会使用 FormHttpMessageConverter
消息转换器,将请求体直接以字符串形式传递给 requestBodyStr 变量。
测试输出结果:
7.5.2 MappingJackson2HttpMessageConverter
另外,如果在请求体中提交的是一个JSON格式的字符串,这个JSON字符串传递给Spring MVC之后,能不能将JSON字符串转换成POJO对象呢?答案是可以的。
此时必须使用@RequestBody 注解来完成 。并且底层使用的消息转换器是:MappingJackson2HttpMessageConverter
。实现步骤如下:
- 第一步:引入jackson依赖
- 第二步:开启注解驱动
- 第三步:创建POJO类,将POJO类作为控制器方法的参数,并使用 @RequestBody 注解标注该参数
@RequestMapping("/send")
@ResponseBody
public String send(@RequestBody User user){
System.out.println(user);
System.out.println(user.getUsername());
System.out.println(user.getPassword());
// 不是逻辑视图名称,是普通字符串,因为前端发送的请求是AJAX请求。
return "success";
}
- 第四步:在请求体中提交json格式的数据
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script th:src="@{/static/js/vue3.4.21.js}"></script>
<script th:src="@{/static/js/axios.min.js}"></script>
</head>
<body>
<div id="app">
<button @click="sendJSON">通过POST请求发送JSON给服务器</button>
<h1>{{message}}</h1>
</div>
<script>
// 发送ajax post请求,并且在请求体当中提交json数据
let jsonObj = {"username":"zhangsan", "password":"1234"}
Vue.createApp({
data(){
return {
message:""
}
},
methods: {
async sendJSON(){
console.log("sendjson")
try{
const res = await axios.post([[@{/}]] + 'send', JSON.stringify(jsonObj), {
headers : {
"Content-Type" : "application/json"
}
})
// 将返回的数据交给message
this.message = res.data
}catch(e){
console.error(e)
}
}
}
}).mount("#app")
</script>
</body>
</html>
测试结果:
7.6 RequestEntity
RequestEntity不是一个注解,是一个普通的类。这个类的实例封装了整个请求协议:包括请求行、请求头、请求体所有信息。
出现在控制器方法的参数上:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
<script th:src="@{/static/js/vue3.4.21.js}"></script>
<script th:src="@{/static/js/axios.min.js}"></script>
</head>
<body>
<div id="app">
<button @click="sendJSON">通过POST请求发送JSON给服务器</button>
<h1>{{message}}</h1>
</div>
<script>
let jsonObj = {"username":"zhangsan", "password":"1234"}
Vue.createApp({
data(){
return {
message:""
}
},
methods: {
async sendJSON(){
console.log("sendjson")
try{
const res = await axios.post('/springmvc/send', JSON.stringify(jsonObj), {
headers : {
"Content-Type" : "application/json"
}
})
this.message = res.data
}catch(e){
console.error(e)
}
}
}
}).mount("#app")
</script>
</body>
</html>
@RequestMapping("/send")
@ResponseBody
public String send(RequestEntity<User> requestEntity){
// 获取请求方法
System.out.println("请求方式:" + requestEntity.getMethod());
// 获取请求URL
System.out.println("请求URL:" + requestEntity.getUrl());
// 获取请求头
HttpHeaders headers = requestEntity.getHeaders();
// 获取请求头中的内容类型
System.out.println("请求的内容类型:" + headers.getContentType());
System.out.println("请求头:" + headers);
// 获取请求体
User user = requestEntity.getBody();
System.out.println(user);
System.out.println(user.getUsername());
System.out.println(user.getPassword());
return "success";
}
测试结果:
在实际的开发中,如果你需要获取更详细的请求协议中的信息。可以使用RequestEntity
7.7 ResponseEntity
ResponseEntity不是注解,是一个类。用该类的实例可以封装响应协议,包括:状态行、响应头、响应体。也就是说:如果你想定制属于自己的响应协议,可以使用该类。
假如我要完成这样一个需求:前端提交一个id,后端根据id进行查询,如果返回null,请在前端显示404错误。如果返回不是null,则输出返回的user。
@Controller
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
} else {
return ResponseEntity.ok(user);
}
}
}
测试:当用户不存在时
测试:当用户存在时
7.8课堂笔记
28. 在SpringMVC中如何使用原生ServletAPI完成AJAX请求的响应?
@GetMapping("/ajax")
public void ajax(HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
out.print("hello ajax, my name is Spring MVC2!");
}
@GetMapping("/ajax")
public String ajax(HttpServletResponse response) throws IOException {
PrintWriter out = response.getWriter();
out.print("hello ajax, my name is Spring MVC!");
return null;
}
29. @ResponseBody 注解(非常重要,使用非常多,因为以后大部分的请求都是AJAX请求)
@GetMapping("/ajax")
@ResponseBody
public String ajax(){
return "hello ajax, my name is Spring MVC!";
}
重点:一旦处理器方法上添加了 @ResponseBody 注解,那么 return 返回语句,返回的将不是一个 “逻辑视图名称” 了。而是直接将返回结果采用字符串的形式响应给浏览器。
底层实现原理实际上代替的就是:
PrintWriter out = response.getWriter();
out.print("hello ajax, my name is Spring MVC!");
以上程序使用的HTTP消息转换器是:StringHttpMessageConverter。
@GetMapping("/ajax")
@ResponseBody
public User ajax() {
User user = new User(111222L, "zhangsan", "123");
return user;
}
当一个处理器方法上面有 @ResponseBody注解,并且返回的是一个java对象,例如user,那么springmvc框架,会自动将user对象转换成json格式的字符串,响应给浏览器。
当然,你必须要在pom.xml文件中引入一个可以处理json的依赖,例如jackson:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
以上程序中使用的消息转换器是:MappingJackson2HttpMessageConverter
30. 非常好用的注解:@RestController
它出现在类上。等于 @Controller + @ResponseBody
@RestController 它是一个复合注解。
当一个类上添加 @RestController 注解之后,表示该类上自动添加了 @Controller注解,并且该类中所有的方法上都会自动添加 @ResponseBody 注解。
31. 关于 @RequestBody 注解
* 该注解只能使用在处理器方法的形参上。
* 这个注解的作用是直接将请求体传递给Java程序,在Java程序中可以直接使用一个String类型的变量接收这个请求体的内容。
* 底层使用的HTTP消息转换器是:FormHttpMessageConverter
关于@RequestBody 注解的重要用法:如果前端请求体当中提交的数据是JSON格式,那么 @RequestBody 可以将提交的JSON格式的字符串转换成java对象。
注意:同样需要使用jackson的依赖。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
然后,要注意@RequestBody标注在处理器方法的形参上,也就是说形参只要准备一个user对象就行了,前端提交一个json字符串,直接将其转换成java对象user
以上前端请求体提交JSON格式的字符串,那么后端直接将json格式字符串转换成java对象,这里使用的消息转换器是:MappingJackson2HttpMessageConverter
32. RequestEntity
这个类的实例封装了整个请求协议。
SpringMVC自动创建好,传递给处理器方法的参数上。
你只需要在处理器方法的参数上加上: (RequestEntity requestEntity)即可,SpringMVC自动创建好该对象,传递到处理器方法的参数上。
通过它可以获取请求协议中任何信息,包括:请求方法、请求头、请求体。
33. ResponseEntity
ResponseEntity不是注解,是一个类。用该类的实例可以封装响应协议,包括:状态行、响应头、响应体。也就是说:如果你想定制属于自己的响应协议,可以使用该类。
注意:如果你要定制属于自己的响应协议,那么处理器方法的返回值类型必须是:ResponseEntity<User>,泛型为什么是User,因为响应体中的内容是user信息。