SpringMVC架构解析:从入门到精通(1)

发布于:2025-09-15 ⋅ 阅读:(24) ⋅ 点赞:(0)

1. MVC架构

MVC是一种软件架构模式,它将应用分为三块:

  • M:Model(模型)

  • V:View(视图)

  • C:Controller(控制器)

应用为什么要被分为三块,优点是什么?

① 低耦合,扩展能力增强;  ②代码复用性增强;  ③代码可维护性增强;

④高内聚,让程序员更加专注业务的开发

MVC将应用分为三块,每一块各司其职,都有自己专注的事情要做,它们属于分工协作,互相配合:

  • Model:负责业务处理及数据的收集。

  • View:负责数据的展示

  • Controller:负责调度。它是一个调度中心,它来决定什么时候调用Model来处理业务,什么时候调用View视图来展示数据。

MVC架构模式的描述:前端浏览器发送请求给web服务器,web服务器中的Controller接收到用户的请求,Controller负责将前端提交的数据进行封装,然后Controller调用Model来处理业务,当Model处理完业务后会返回处理之后的数据给Controller,Controller再调用View来完成数据的展示,最终将结果响应给浏览器,浏览器进行渲染展示页面。

2. SpringMVC

SpringMVC是一个实现了MVC架构模式的Web框架,底层基于Servlet实现。SpringMVC已经将MVC架构模式实现了,因此只要基于SpringMVC框架写代码,编写的程序就是符合MVC架构模式的。

SpringMVC框架做了什么,与纯粹的Servlet开发有什么区别?

  1. 入口控制:SpringMVC框架通过DispatcherServlet作为入口控制器,负责接收请求和分发请求。而在Servlet开发中,需要自己编写Servlet程序,并在web.xml中进行配置,才能接受和处理请求。

  2. 在SpringMVC中,表单提交时可以自动将表单数据绑定到相应的JavaBean对象中,只需要在控制器方法的参数列表中声明该JavaBean对象即可,无需手动获取和赋值表单数据。而在纯粹的Servlet开发中,这些都是需要自己手动完成的。

  3. IoC容器:SpringMVC框架通过IoC容器管理对象,只需要在配置文件中进行相应的配置即可获取实例对象,而在Servlet开发中需要手动创建对象实例。

  4. 统一处理请求:SpringMVC框架提供了拦截器、异常处理器等统一处理请求的机制,并且可以灵活地配置这些处理器。而在Servlet开发中,需要自行编写过滤器、异常处理器等,增加了代码的复杂度和开发难度。

  5. 视图解析:SpringMVC框架提供了多种视图模板,如JSP、Freemarker、Velocity等,并且支持国际化、主题等特性。而在Servlet开发中需要手动处理视图层,增加了代码的复杂度。

3. SpringMVC程序开发

3.1 配置web.xml文件

<!--前端控制器-->
<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>0</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <!--所有前端发送的请求都会先经过 DispatcherServlet 处理,除了jsp-->
    <url-pattern>/</url-pattern>
</servlet-mapping>

3.1.1 DispatchServlet类

DispatcherServlet是Web应用程序的主要入口点之一,它的职责包括:

  1. 接收客户端的HTTP请求:DispatcherServlet监听来自Web浏览器的HTTP请求,然后根据请求的URL将请求数据解析为Request对象。

  2. 处理请求的URL:DispatcherServlet将请求的URL(Uniform Resource Locator)与处理程序进行匹配,确定要调用哪个控制器(Controller)来处理此请求。

  3. 调用相应的控制器:DispatcherServlet将请求发送给找到的控制器处理,控制器将执行业务逻辑,然后返回一个模型对象(Model)。

  4. 渲染视图:DispatcherServlet将调用视图引擎,将模型对象呈现为用户可以查看的HTML页面。

  5. 返回响应给客户端:DispatcherServlet将为用户生成的响应发送回浏览器,响应可以包括表单、JSON、XML、HTML以及其它类型的数据。

3.2 配置springmvc-servlet.xml文件

SpringMVC框架有它自己的配置文件,该配置文件的名字默认为:<servlet-name>-servlet.xml,默认存放的位置是WEB-INF 目录下:

<!--组件扫描-->
<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>

视图解析器(View Resolver)的作用主要是将Controller方法返回的逻辑视图名称解析成实际的视图对象。视图解析器将解析出的视图对象返回给DispatcherServlet,并最终由DispatcherServlet将该视图对象转化为响应结果,呈现给用户。

如果采用了其它视图,请配置对应的视图解析器,例如:

  • JSP的视图解析器:InternalResourceViewResolver

  • FreeMarker视图解析器:FreeMarkerViewResolver

  • Velocity视图解析器:VelocityViewResolver

3.3 视图

在WEB-INF目录下新建templates目录,在templates目录中新建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>
<!-- th: 表示后面的代码可以编写Thymeleaf语法,可以被Thymeleaf语法解析 -->
<!-- Thymeleaf检测到以 / 开始,表示绝对路径,自动会将视图解析器中webapp的上下文路径加上去 -->
<!-- 最终的效果是:href="/other" -->
<a th:href="@{/other}">other请求</a>
</body>
</html>

3.4 Controller类

@RequestMapping("/")
public String toIndex(){
    // 返回逻辑视图名称
    return "index";
}

// 请求映射
// 这个方法是一个实例方法
// 这个方法目前返回一个String字符串
// 返回值代表的是一个逻辑视图名称
//@RequestMapping(value="/test")
@RequestMapping("/test")
public String hehe(){
    // 返回一个逻辑视图名称
    return "first";
}

3.5 执行流程总结

  1. 浏览器发送请求:http://localhost:8080/haha

  2. SpringMVC的前端控制器DispatcherServlet接收到请求

  3. DispatcherServlet根据请求路径 /haha 映射到 FirstController#名字随意(),调用该方法

  4. FirstController#名字随意() 处理请求

  5. FirstController#名字随意() 返回逻辑视图名称 first 给视图解析器

  6. 视图解析器找到 /WEB-INF/templates/first.html 文件,并进行解析,生成视图解析对象返回给前端控制器DispatcherServlet

  7. 前端控制器DispatcherServlet响应结果到浏览器。

3.6 IndexController

@Controller
public class IndexController {
    @RequestMapping("/")
    public String toIndex(){
        return "index";
    }
}

表示请求路径如果是:http://localhost:8080/ ,则进入 /WEB-INF/templates/index.html 页面。

4. RequestMapping

@RequestMapping 注解是 Spring MVC 框架中的一个控制器映射注解,用于将请求映射到相应的处理方法上。具体来说,它可以将指定 URL 的请求绑定到一个特定的方法或类上,从而实现对请求的处理和响应。

在同一个webapp中,RequestMapping必须具有唯一性。在类上和方法上都使用RequestMapping注解来进行路径的映射。假设在类上映射的路径是"/a",在方法上映射的路径是"/b",那么整体表示映射的路径就是:"/a/b"。

@Controller
@RequestMapping("/product")
public class ProductController {
	// @RequestMapping("/product/detail")
	@RequestMapping("/detail")
	public String toDetail(){
		return "product_detail";
	}
}

首页中添加两个超链接:

<!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>

4.1 Value属性

value属性是该注解最核心的属性,value属性填写的是请求路径,也就是说通过该请求路径与对应的控制器的方法绑定在一起。value属性是一个字符串数组:

@Controller
public class IndexController {
	@RequestMapping("/")
	public String toDetail(){
		return "index";
	}

	// 对于注解来说:如果是一个数组,数组中只有一个元素,大括号是可以省略的
	//@RequestMapping(value = "/testVal1")
	// 对于注解来说,如果只使用了一个value属性,那么value也是可以省略的。
	// @RequestMapping("/testVal2")
//	@RequestMapping(value = {"/testValue1", "/testValue2"})
	@RequestMapping(path = {"/testValue1", "/testValue2"})
	public String testValue(){
		return "testValue";
	}
}
<!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>

<!--测试RequestMapping的value属性-->
<a th:href="@{/testValue1}">testValue1</a><br>
<a th:href="@{/testValue2}">testValue2</a><br>

</body>
</html>

4.1.1 Ant风格的value

value是可以用来匹配路径的,路径支持模糊匹配,把这种模糊匹配称之为Ant风格。

关于路径中的通配符包括:

  • ?,代表任意一个字符

  • *,代表0到N个任意字符

  • **,代表0到N个任意字符,并且路径中可以出现路径分隔符 /

注意:** 通配符在使用时,左右不能出现字符,只能是 /;

在Spring6当中,** 通配符只能作为路径的末尾出现。

//@RequestMapping(value = "/x?z/testAntValue")	 // x和z中间任意一个字符,除了/和?和空着
//@RequestMapping(value = "/x*z/testAntValue") 	 // x和z中间任意一个字符,除了/和?
//@RequestMapping(value = "/x**z/testAntValue")  // x和z中间任意一个字符,除了?,和上面这个语句没有区别
//@RequestMapping(value = "/**/testAntValue") // 报错了,Spring6报错。Spring5不会报错。
@RequestMapping(value = "/testAntValue/**")
public String testRequestMappingAntValue(){
	return "ok";
}

4.1.2 value中的占位符

普通的请求路径: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}

@RequestMapping(value = "/login/{a}/{b}")
public String testRESTFulURL(
	  @PathVariable(value = "a")
	  String username,
	  @PathVariable("b")
	  String password){
	System.out.println("用户名:" + username + ",密码:" + password);
	return "ok";
}
<!--测试RequestMapping注解的value属性支持占位符-->
<a th:href="@{/login/zhangsan/123456}">测试value属性使用占位符</a>

4.2 method属性

在Servlet当中,如果后端要求前端必须发送一个post请求,后端可以通过重写doPost方法来实现。后端要求前端必须发送一个get请求,后端可以通过重写doGet方法来实现。当重写的方法是doPost时,前端就必须发送post请求,当重写doGet方法时,前端就必须发送get请求。如果前端发送请求的方式和后端的处理方式不一致时,会出现405错误。

HTTP状态码405,这种机制的作用是:限制客户端的请求方式,以保证服务器中数据的安全。

假设后端程序要处理的请求是一个登录请求,为了保证登录时的用户名和密码不被显示到浏览器的地址栏上,后端程序有义务要求前端必须发送一个post请求,如果前端发送get请求,则应该拒绝。

// 当前端发送的请求路径是 /user/login ,并且发送的请求方式是以POST方式请求的。则可以正常映射。
// 当前端发送的请求路径不是 /user/login,请求方式是POST,不会映射到这个方法上。
// 当前端发送的请求路径是 /user/login,请求方式不是POST,也不会映射到这个方法上。
//@RequestMapping(value = "/user/login", method = RequestMethod.POST)
@PostMapping("/user/login")
public String userLogin(){
	System.out.println("处理登录的业务......");
	return "ok";
}

在SpringMVC中不仅提供了 PostMaping注解,像这样的注解还有四个,包括:

  • GetMapping:要求前端必须发送get请求

  • PutMapping:要求前端必须发送put请求

  • DeleteMapping:要求前端必须发送delete请求

  • PatchMapping:要求前端必须发送patch请求

<!-- 前端  发送POST请求-->
<form th:action="@{/user/login}" method="post">
	用户名:<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,则都是无法建立映射的。

4.3 params属性

对于RequestMapping注解来说:

  • value属性是一个数组,只要满足数组中的任意一个路径,就能映射成功

  • method属性也是一个数组,只要满足数组中任意一个请求方式,就能映射成功。

  • params属性也是一个数组,不过要求请求参数必须和params数组中要求的所有参数完全一致后,才能映射成功。

4.3.1 params属性的四种用法

@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的原因:请求参数格式不正确而导致的。

<!--测试RequestMapping注解的params属性-->
<!--发送请求 /testParams,并且携带参数 username password-->
<a th:href="@{/testParams?username=admin&password=1234}">测试RequestMapping注解的params属性</a><br>
<a th:href="@{/testParams(username='admin', password='1234')}">测试RequestMapping注解的params属性</a><br>
// 当请求路径是 /testParams,并且提交的参数包括 username 和 password时,才能映射成功。
@RequestMapping(value = "/testParams", params = {"username", "password"})
//@RequestMapping(value = "/testParams", params = {"username=zhangsan", "password"})
//@RequestMapping(value = "/testParams", params = {"username!=zhangsan", "password"})
//@RequestMapping(value = "/testParams", params = {"!username", "password"})
public String testParams(){
	return "ok";
}

4.4 headers属性

@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错误!!!

<!--测试RequestMapping注解的headers属性-->
<a th:href="@{/testHeaders}">测试RequestMapping注解的headers属性</a>
// 只有当请求路径是 /testHeaders,并且请求头当中有 Referer 和 Host,这样才能映射成功。
//@RequestMapping(value = "/testHeaders", headers = {"Referer", "Host"})
//@RequestMapping(value = "/testHeaders", headers = {"!Referer", "Host"})
@RequestMapping(value = "/testHeaders", headers = {"Referer=http://localhost:8888/", "Host"})
//@RequestMapping(value = "/testHeaders", headers = {"Referer!=http://localhost:8080/", "Host"})
public String testHeaders(){
	return "ok";
}

4.5 跳转的ok页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>ok</title>
</head>
<body>
<h1>Test OK!</h1>
</body>
</html>

5. 获取请求数据

5.1 注册表单

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<title>用户注册</title>
</head>

<body>
<!--注册页面-->
<form th:action="@{/user/reg}" 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="interest" value="smoke">
	喝酒<input type="checkbox" name="interest" value="drink">
	烫头<input type="checkbox" name="interest" value="tt">
	<br>
	简介:
	<textarea cols="60" rows="10" name="intro"></textarea><br>
	年龄:
	<input type="text" name="age"><br>
	<input type="submit" value="注册">
</form>

</body>
</html>

5.1.1 成功页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ok</title>
</head>
<body>
<h1>Test OK!</h1>
</body>
</html>

5.2 原生API获取数据

原生的Servlet API指的是:HttpServletRequest在SpringMVC当中,一个Controller类中的方法参数上如果有HttpServletRequest,SpringMVC会自动将  当前请求对象 传递给这个参数,因此可以通过这个参数来获取请求提交的数据。

//@RequestMapping(value = "/user/reg", method = RequestMethod.POST)
@PostMapping("/user/reg")
// SpringMVC检测到request对象会自动将Tomcat服务器创建的request对象传进来
public String register(HttpServletRequest request, HttpServletResponse response, HttpSession session){
  // HttpServletRequest、HttpServletResponse、HttpSession都属于原生Servlet API。
  // HttpServletRequest(请求对象)代表 客户端(如浏览器)向服务器发送的一次 HTTP 请求。
  // HttpServletResponse(响应对象)代表 服务器对客户端请求的响应。
  // HttpSession(会话对象)代表一次用户会话(Session),即用户从打开浏览器到关闭浏览器期间与服务器的交互过程。
  System.out.println(request);
  System.out.println(response);
  System.out.println(session);

  // 获取请求提交的数据
  String username = request.getParameter("username");
  String password = request.getParameter("password");
  String sex = request.getParameter("sex");
  String[] interest = request.getParameterValues("interest"); // interest=smoke&interest=drink&interest=tt
  String intro = request.getParameter("intro");

  System.out.println(username);
  System.out.println(password);
  System.out.println(sex);
  System.out.println(Arrays.toString(interest));
  System.out.println(intro);

  return "ok";
}

5.3 使用RequestParam注解标注

对于@RequestParam注解来说,属性有value和name,这两个属性的作用相同,都是用来指定提交数据的name。

value都是注册表单中各个标签的id.

发送请求时提交的数据是:name1=value1&name2=value2,则这个注解应该这样写:@RequestParam(value="name1")、@RequestParam(value="name2")

@PostMapping("/user/reg")
public String register(
	/* @RequestParam(value = "username") // value可以,name也可以*/
	/* @RequestParam(name = "username") // username 不能随便写,最好是复制过来的。*/
	@RequestParam(name = "username") /* 如果前端没有提供 uname 参数,则报错:400*/
	String a, /* 变量名随意*/
	@RequestParam(value = "password")
	String password,
	@RequestParam("sex")
	Integer sex, /* SpringMVC也可以自动帮助我们做类型转换,从前端提交的是'0'/'1'字符串,可以自动转换成Integer类型。*/
	@RequestParam("interest")
	String[] interest,
	@RequestParam("intro")
	String intro,
	@RequestParam(value = "age", required = false, defaultValue = "20")
	Integer age){

  System.out.println(a);
  System.out.println(password);
  System.out.println(sex);
  System.out.println(Arrays.toString(interest));
  System.out.println(intro);
  System.out.println("年龄:" + age);
  return "ok";
}

5.3.1 RequestParam注解的required属性

required属性用来设置该方法参数是否为必须的。默认情况下,这个参数为 true,表示方法参数是必需的。如果请求中缺少对应的参数,则会抛出异常。可以将其设置为false,false表示不是必须的,如果请求中缺少对应的参数,则方法的参数为null。

1. required属性可以设置为false,这样这个参数就不是必须的了。如果前端没有提供,则不会报400错误。但是由于前端没有提供这个数据,因此程序中的变量值为null.

2. defaultValue属性:通过defaultValue属性可以给参数赋默认值。如果前端没有提供这样的参数,参数的默认值就起作用了。

5.4 依靠控制器方法上的形参名接收

如果 请求参数名(表单中的标签名)控制器方法上的形参名 保持一致,那么 @RequestParam注解可以省略。

// 依靠控制器方法上的形参名来接收
@PostMapping("/user/reg")
public String register(String uname, String password, Integer sex, String[] interest, String intro, Integer age){
  System.out.println(uname); // null(因为前端没有提交这个数据),前端的是username
  System.out.println(password);
  System.out.println(sex);
  System.out.println(Arrays.toString(interest));
  System.out.println(intro);
  System.out.println(age);
  return "ok";
}

但是,如果采用的是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>

5.5 使用POJO类接收请求参数

// 通过POJO类接收请求参数
@PostMapping("/user/reg")
public String register(User user){
	System.out.println(user);
  return "ok";
}

底层实现原理:反射机制。

使用这种方式的前提是: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方法,将无法给对应的属性赋值。所以set方法正确,属性名其实可以随意定义

5.6 RequestHeader注解

该注解的作用是:将请求头信息映射到方法的形参上。和RequestParam注解功能相似,RequestParam注解的作用:将请求参数映射到方法的形参上。当然,对于RequestHeader注解来说,也有三个属性:value、required、defaultValue,和RequestParam一样。

// 获取请求头信息
// 将`请求头信息`映射到`方法的形参上`。
//@PostMapping("/user/reg")
@RequestMapping("/user/reg")
public String register(User user,
			   @RequestHeader(value = "Referer", required = false, defaultValue = "")
			   String referer,
			   @RequestHeader(value = "Host", required = false, defaultValue = "")
			   String host,
			   

               @CookieValue(value = "id", required = false, defaultValue = "")
			   String id){
  System.out.println(user);
  System.out.println(referer);
  System.out.println(host);
  System.out.println("客户端提交过来的cookie,它的值是:" + id);
  return "ok";
}

5.7 CookieValue注解

该注解的作用:将请求提交的Cookie数据映射到方法形参上同样是有三个属性:value、required、defaultValue。

<!--发送Cookie给服务器-->
<script type="text/javascript">
	function sendCookie(){
		/*id=123456789:创建名为 id的 Cookie,值为 123456789
		expires=...:设置过期时间为 2025 年 12 月 18 日 UTC 时间 12:00
		path=/:使 Cookie 在整个网站根路径下有效*/
		document.cookie = "id=123456789; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/";
		/*
		* 将浏览器重定向到路径 /springmvc/user/reg
		* 用户访问新页面,常用于身份验证后跳转
		*/
		document.location = "/user/reg";
	}
</script>
<button onclick="sendCookie()">向服务器端发送Cookie</button>

5.8 请求乱码问题

5.8.1 get请求

在SpringMVC中如何解决请求体的中文乱码问题呢?当然,还是使用

request.setCharacterEncoding("UTF-8")使用它有一个前提条件,要想解决请求体乱码问题,以上代码必须在 request.getParameter("username")执行之前执行才有效。

@RequestMapping("/user/reg")
public String register(User user, HttpServletRequest request) throws UnsupportedEncodingException {
	// 设置请求体的字符编码方式,解决POST请求乱码问题
	//request.setCharacterEncoding("UTF-8");

	System.out.println(user);
	return "ok";
}

也就是说以上代码如果放在Controller的相关方法中执行是无效的,因为Controller的方法在执行之前 DispatcherServlet已经调用了 request.getParameter("username")方法。因此在Controller方法中使用request.setCharacterEncoding("UTF-8");无效

5.8.2 post请求

如果遇到Tomcat9- 版本,那么POST请求乱码应该怎么解决呢?

在 request.getParameter() 方法执行之前,执行: request.setCharacterEncoding("UTF-8"); 


第一种方案:自己编写一个过滤器!!!!过滤器Filter在Servlet执行之前执行。

<!--配置字符编码过滤器,在上面DispatcherServlet执行之前先执行-->
<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>com.hnlg.springmvc.filter.CharacterEncodingFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

第二种方案:使用SpringMVC框架内置的字符编码过滤器即可:CharacterEncodingFilter。

<!--使用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>

6. 三个域对象

请求域:request会话域:session应用域:application三个域都有以下三个方法:

// 向域中存储数据
void setAttribute(String name, Object obj);
​
// 从域中读取数据
Object getAttribute(String name);
​
// 删除域中的数据
void removeAttribute(String name);

主要是通过:setAttribute + getAttribute方法来完成在域中数据的传递和共享。

对比项 转发(Forward) 重定向(Redirect)
请求次数 1次(服务器内部完成) 2次(客户端再次发起)
浏览器地址栏 不变(仍显示原地址 /login 改变(变为新地址 /welcome.jsp
能否共享 request 域数据 ✅ 可以(request.setAttribute() 有效) ❌ 不行(request 已结束)
效率 高(服务器内部跳转) 稍低(多一次 HTTP 请求)
应用场景 表单验证失败、内部处理跳转 登录成功跳转、防止重复提交
API 调用 request.getRequestDispatcher().forward() response.sendRedirect()
场景 推荐方式 原因
表单提交后跳转到结果页 ✅ 重定向 防止用户刷新页面重复提交(F5 不会重复登录)
表单验证失败返回原页 ✅ 转发 可以保留错误信息和用户已填数据
传递敏感数据 ✅ 转发 不暴露在 URL 中
跳转到外部网站 ✅ 重定向 只能用 sendRedirect("https://baidu.com")

6.1 request域

接口名:HttpServletRequest

request对象代表了一次请求。一次请求一个request。

使用请求域的业务场景:在A资源中通过转发的方式跳转到B资源,因为是转发,因此从A到B是一次请求,如果想让A资源和B资源共享同一个数据,可以将数据存储到request域中。

6.1.1 共享数据方式

原生Servlet API方式
<a th:href="@{/testServletAPI}">测试在SpringMVC当中使用原生Servlet API完成request域数据共享</a>
@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/ok.html
		return "ok";
	}
}

无论是Model、Map还是ModelMap,底层实例化的对象都是:BindingAwareModelMap。

BindingAwareModelMap继承了ModelMap,而ModelMap又实现了Map接口。 因此表面上是采用了不同方式,底层本质上是相同的。

Model接口
<a th:href="@{/testModel}">测试在SpringMVC当中使用Model接口完成request域数据共享</a>
@RequestMapping("/testModel")
public String testModel(Model model){
	// 向request域当中绑定数据
	model.addAttribute("testRequestScope", "在SpringMVC当中使用Model接口完成request域数据共享");
	System.out.println(model);
	System.out.println(model.getClass().getName());
	// 转发
	return "ok";
}
Map接口
<a th:href="@{/testMap}">测试在SpringMVC当中使用Map接口完成request域数据共享</a>
@RequestMapping("/testMap")
public String testMap(Map<String, Object> map){
	// 向request域当中存储数据
	map.put("testRequestScope", "在SpringMVC当中使用Map接口完成request域数据共享");
	System.out.println(map);
	System.out.println(map.getClass().getName());
	// 转发
	return "ok";
}
Model Map类
<a th:href="@{/testModelMap}">测试在SpringMVC当中使用ModelMap类完成request域数据共享</a>
@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap){
	// 向request域当中存储数据
	modelMap.addAttribute("testRequestScope", "在SpringMVC当中使用ModelMap类完成request域数据共享");
	System.out.println(modelMap);
	System.out.println(modelMap.getClass().getName());
	// 转发
	return "ok";
}

ModelAndView类

在SpringMVC框架中为了更好的体现MVC架构模式,提供了一个类:ModelAndView。这个类的实例封装了Model和View。也就是说这个类既封装业务处理之后的数据,也体现了跳转到哪个视图。使用它也可以完成request域数据共享。

<a th:href="@{/testModelAndView}">测试在SpringMVC当中使用ModelAndView类完成request域数据共享</a>
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
	// 创建 模型视图 对象
	ModelAndView mav = new ModelAndView();
	// 给 模型视图对象 绑定数据
	mav.addObject("testRequestScope", "在SpringMVC当中使用ModelAndView类完成request域数据共享");
	// 给 模型视图对象 绑定视图(绑定逻辑视图名称)
	mav.setViewName("ok");
	// 返回 模型视图对象
	return mav;
}
  1. 方法的返回值类型不是String,而是ModelAndView对象。

  2. ModelAndView不是出现在方法的参数位置,而是在方法体中new的。

  3. 需要调用addObject向域中存储数据。

  4. 需要调用setViewName设置视图的名字。

当请求路径不是JSP的时候,都会走前端控制器DispatcherServlet。
DispatcherServlet中有一个核心方法 doDispatch(),这个方法用来通过请求路径找到对应的 处理器方法;

然后调用 处理器方法,处理器方法返回一个逻辑视图名称(可能也会直接返回一个ModelAndView对象),底层会将逻辑视图名称转换为View对象,然后将View对象结合Model对象,封装一个ModelAndView对象,然后将该对象返回给DispatcherServlet类了。

6.1.2 显示结果

<div th:text="${testRequestScope}"></div>

6.2 session域

接口名:HttpSession

session对象代表了一次会话。从打开浏览器开始访问,到最终浏览器关闭,这是一次完整的会话。每个会话session对象都对应一个JSESSIONID,而JSESSIONID生成后以cookie的方式存储在浏览器客户端。浏览器关闭,JSESSIONID失效,会话结束。

使用会话域的业务场景:

  1. 在A资源中通过重定向的方式跳转到B资源,因为是重定向,因此从A到B是两次请求,如果想让A资源和B资源共享同一个数据,可以将数据存储到session域中。

  2. 登录成功后保存用户的登录状态。

6.2.1 原生Servlet API

<a th:href="@{/testSessionServletAPI}">测试在SpringMVC当中使用原生Servlet API完成session域数据共享</a>
@Controller
public class SessionScopeTestController {

	@RequestMapping("/testSessionServletAPI")
	public String testServletAPI(HttpSession session){
		// 处理核心业务....
		// 将数据存储到session中
		session.setAttribute("testSessionScope", "在SpringMVC当中使用原生Servlet API完成session域数据共享");
		// 返回逻辑视图名称(这是一个转发的行为)
		return "ok";
	}
}
<div th:text="${session.testSessionScope}"></div>

6.2.2 SessionAttributes注解

<a th:href="@{/testSessionAttributes}">测试在SpringMVC当中使用@SessionAttributes注解完成session域数据共享</a>
@Controller
//@SessionAttributes(value = {"x", "y"})
@SessionAttributes({"x", "y"}) // 标注x和y都是存放到session域中,而不是request域。
public class SessionScopeTestController {

	@RequestMapping("/testSessionAttributes")
	/*ModelMap指的是request域,但是在类上加了注解就存储到session域,如果没有注解就默认存储到request域*/
	public String testSessionAttributes(ModelMap modelMap){
		// 处理业务
		// 将数据存储到session域当中
		modelMap.addAttribute("x", "我是埃克斯");
		modelMap.addAttribute("y", "我是歪");

		// 返回逻辑视图名称
		return "ok";
	}
}
<div th:text="${session.x}"></div>
<div th:text="${session.y}"></div>

6.3 application

接口名:ServletContext

application对象代表了整个web应用,服务器启动时创建,服务器关闭时销毁。对于一个web应用来说,application对象只有一个。

使用应用域的业务场景:记录网站的在线人数。

<a th:href="@{/testApplicationScope}">测试在SpringMVC当中使用Servlet API实现application域数据共享</a>
@Controller
public class ApplicationScopeTestController {

	@RequestMapping("/testApplicationScope")
	public String testApplicationScope(HttpServletRequest request){
		// 将数据存储到application域当中
		// 获取application对象,其实就是获取ServletContext对象
		// 怎么获取ServletContext对象?通过request,通过session都可以获取。
		ServletContext application = request.getServletContext();
		application.setAttribute("testApplicationScope", "在SpringMVC中使用ServletAPI实现application域共享");
		return "ok";
	}
}
<div th:text="${application.testApplicationScope}"></div>

7. SpringMVC的视图实现原理

Spring MVC支持的常见视图包括:

1. InternalResourceView:内部资源视图(Spring MVC框架内置的,专门为JSP模板语法准备的,也是为转发准备的)

2. RedirectView:重定向视图(Spring MVC框架内置的,用来完成重定向效果)

3. ThymeleafView:Thymeleaf视图(第三方的,为Thymeleaf模板语法准备的)

4. FreeMarkerView:FreeMarker视图(第三方的,为FreeMarker模板语法准备的)

5. VelocityView:Velocity视图(第三方的,为Velocity模板语法准备的)

6. PDFView:PDF视图(第三方的,专门用来生成pdf文件视图)

7. ExcelView:Excel视图(第三方的,专门用来生成excel文件视图)


7.1 实现视图机制的核心接口

7.1.1 DispatcherServlet类(前端控制器)

职责:在整个Spring MVC执行流程中,负责中央调度。

核心方法:doDispatch

7.1.2 ViewResolver接口(视图解析器)

职责:负责将逻辑视图名转换为物理视图名,最终创建View接口的实现类,即视图实现类对象。

核心方法:resolveViewName

7.1.3 View接口(视图)

职责:负责将模型数据Model渲染为视图格式(HTML代码),并最终将生成的视图(HTML代码)输出到客户端。(它负责将模板语言转换成HTML代码)

核心方法:render

7.1.4 ViewResolverRegistry(视图解析器注册器)

负责在web容器(Tomcat)启动的时候,完成视图解析器的注册。

如果有多个视图解析器,会将视图解析器对象按照order的配置放入List集合。

如果想定制自己的视图组件:

  • 编写类实现ViewResolver接口,实现resolveViewName方法,在该方法中完成**逻辑视图名**转换为**物理视图名**,并返回View对象。

  • 编写类实现View接口,实现render方法,在该方法中将模板语言转换成HTML代码,并将HTML代码响应到浏览器。

如果Spring MVC框架中使用Thymeleaf作为视图技术。那么相关的类包括:

  • ThymeleafView

  • ThymeleafViewResolver


第一步:浏览器发送请求给web服务器

第二步:Spring MVC中的DispatcherServlet接收到请求

第三步:DispatcherServlet根据请求路径分发到对应的Controller

第四步:DispatcherServlet调用Controller的方法

第五步:Controller的方法处理业务并返回一个逻辑视图名给DispatcherServlet

第六步:DispatcherServlet调用ThymeleafViewResolver的resolveViewName方法,将逻辑视图名转换为物理视图名,并创建ThymeleafView对象返回给DispatcherServlet

第七步:DispatcherServlet再调用ThymeleafView的render方法,render方法将模板语言转换为HTML代码,响应给浏览器,完成最终的渲染。


7.2 Thymeleaf视图解析器源码

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);
	}
    
    // 将 逻辑视图名称 转换成 物理视图名称,并且最终返回视图对象View
	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方法。
*/

8. 转发与重定向

8.1 转发forward与重定向redirect创建对象

@Controller
public class ForwardController {
	@RequestMapping("/a")
	public String toA(){
		// 返回的是一个逻辑视图名称
		// 默认采用的也是转发
		//return "a";

		// 采用SpringMVC的转发方式跳转到 /b
		// 转发的时候,格式有特殊要求: return "forward:下一个资源的路径";
		// 这个就不是逻辑视图名称了。
		//return "forward:/b"; // 创建InternalResourceView对象。

		// 这个使用较多。
		return "redirect:/b"; // 创建RedirectView
	}

	@RequestMapping("/b")
	public String toB(){
		// 返回的是一个逻辑视图名称
		return "b"; // 创建ThymeleafView对象。
	}
}

8.2 mvc:view-controller

<mvc:view-controller> 配置用于将某个请求映射到特定的视图上,即指定某一个 URL 请求到一个视图资源的映射,使得这个视图资源可以被访问。它相当于是一个独立的处理程序,不需要编写任何 Controller,只需要指定 URL 和对应的视图名称就可以了。

一般情况下,<mvc:view-controller> 配置可以替代一些没有业务逻辑的 Controller,例如首页、错误页面等。当用户访问配置的 URL 时,框架将直接匹配到对应的视图,而无需再经过其他控制器的处理。

<mvc:view-controller path="/如何访问该页面" view-name="对应的逻辑视图名称" />
<!--配置视图控制器-->
<!--只用这个组件单独配置之后,所有的注解就失效了-->

path="/test"表示处理所有访问 /test的请求

  • 支持 Ant 风格路径模式(如 /admin/*/docs/**)

  • 可配置多个路径(逗号分隔:path="/test,/demo"

8.3 mvc:annotation-driven

<!--开启注解驱动-->
<mvc:annotation-driven/>

8.4 访问静态资源

由于DispatcherServlet的url-pattern配置的是“/”,之前说过,这个"/"代表的是除jsp请求之外的所有请求,也就是说访问应用中的静态资源,也会走DispatcherServlet,这会导致404错误,无法访问静态资源,如何解决,两种方案:

  • 使用默认 Servlet 处理静态资源

  • 使用 mvc:resources 标签配置静态资源处理

8.4.1 默认Servlet处理静态资源

<!--开启注解驱动-->
<mvc:annotation-driven/>

<!--开启默认的Servlet处理-->
<mvc:default-servlet-handler/>

8.4.2 mvc:resources 标签配置静态资源处理

<!--开启注解驱动-->
<mvc:annotation-driven/>

<!--当请求路径符合 /static/** 的时候,去 /static/ 位置找资源-->
<mvc:resources mapping="/static/**" location="/static/" />

表示凡是请求路径是"/static/"开始的,都会去"/static/"目录下找该资源。

9. RESTFul编程风格

9.1 RESTFul是什么

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

9.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

9.3 RESTFul方式演示查询

<!--RESTful风格的:查看用户列表-->
<a th:href="@{/user}">查看用户列表</a><br>

<!--RESTful风格的:根据id查询用户信息-->
<a th:href="@{/user/110}">查询id=110的这个用户信息</a><br>
@Controller
public class UserController {

	@RequestMapping(value = "/user", method = RequestMethod.GET)
	public String getAll(){
		System.out.println("正在查询所有用户信息....");
		return "ok";
	}

	@RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
	public String getById(@PathVariable("id") String id){
		System.out.println("正在根据用户id查询用户信息...,用户id是" + id);
		return "ok";
	}
}

9.4 RESTFul方式演示增加

<!--RESTful风格的:新增用户信息。新增必须发送POST请求,需要使用form表单-->
<form th:action="@{/user}" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    年龄:<input type="text" name="age"><br>
    <input type="submit" value="保存">
</form>
@RequestMapping(value = "/user", method = RequestMethod.POST)
public String save(User user){
	System.out.println("正在保存用户信息...");
	System.out.println(user);
	return "ok";
}

9.5 RESTFul方式演示修改

如何发送PUT请求?

第一步:首先你必须是一个POST请求。

第二步:在发送POST请求的时候,提交这样的数据:**_method=PUT**

第三步:在web.xml文件配置SpringMVC提供的过滤器:HiddenHttpMethodFilter

<!--RESTful风格的:修改用户信息。修改必须发送PUT请求,要发送PUT请求,
首先你必须是一个POST请求-->
<form th:action="@{/user}" method="post">
    <!--隐藏域-->
    <!--强调:name必须是 _method,value必须是put/PUT。
        如果你要发送delete请求的话,value写delete即可。-->
    <input type="hidden" name="_method" value="put">

    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    年龄:<input type="text" name="age"><br>
    <input type="submit" value="修改">
</form>
<!--添加一个过滤器,这个过滤器是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 = "/user", method = RequestMethod.PUT)
public String modify(User user){
	System.out.println("正在修改用户信息:" + user);
	return "ok";
}

9.6 RESTFul方式演示删除

操作 实现方式 是否需要 JS 说明
修改(PUT) 直接用 <form method="post"> + _method=put ❌ 不需要 JS 表单自然提交
删除(DELETE) 用 <a> 触发 JS,动态设置表单 action 并提交 ✅ 需要 JS 因为没有“提交按钮”

💡 为什么删除要用 JS?因为删除通常是一个链接或按钮,不是表单提交行为。

<!--RESTful风格的:删除用户信息-->
<!--删除必须发送DELETE请求。和PUT请求实现方式相同。-->
<!--发送DELETE请求的前提是POST请求,并且需要通过隐藏域提交 _method=delete -->
<!--发送一个 DELETE 请求到 /user/120-->
<!--HTML 无法直接发送 DELETE-->
<!--用 JS 控制一个隐藏的 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";
}

Optional<Long> 是一个可能包含 Long 值的“盒子”,它明确告诉你:

  • ✅ 有值(比如 Optional[1003]
  • ❌ 没有值(空的,Optional.empty()

为什么 stream() 的某些操作返回 Optional

因为 有些操作的结果可能不存在

常见返回 Optional 的操作:

方法 什么时候有值? 什么时候是 Optional.empty()
.findFirst() 找到了第一个元素 列表为空或没匹配项
.findAny() 找到了任意一个 没找到
.reduce(...) 聚合出一个结果 列表为空
.max() / .min() 找到了最大/最小值 列表为空

为什么不是所有 Stream 操作都返回 Optional

因为有些操作一定有结果,不需要包装:

操作 返回类型 说明
.count() long 空集合也是 0,不会为空
.collect() List<T> 返回集合,空集合是 [],不是 null
.forEach() void 直接执行,不返回值