SpringMVC架构解析:从入门到精通(1)
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开发有什么区别?
-
入口控制:SpringMVC框架通过DispatcherServlet作为入口控制器,负责接收请求和分发请求。而在Servlet开发中,需要自己编写Servlet程序,并在web.xml中进行配置,才能接受和处理请求。
-
在SpringMVC中,表单提交时可以自动将表单数据绑定到相应的JavaBean对象中,只需要在控制器方法的参数列表中声明该JavaBean对象即可,无需手动获取和赋值表单数据。而在纯粹的Servlet开发中,这些都是需要自己手动完成的。
-
IoC容器:SpringMVC框架通过IoC容器管理对象,只需要在配置文件中进行相应的配置即可获取实例对象,而在Servlet开发中需要手动创建对象实例。
-
统一处理请求:SpringMVC框架提供了拦截器、异常处理器等统一处理请求的机制,并且可以灵活地配置这些处理器。而在Servlet开发中,需要自行编写过滤器、异常处理器等,增加了代码的复杂度和开发难度。
-
视图解析: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应用程序的主要入口点之一,它的职责包括:
-
接收客户端的HTTP请求:DispatcherServlet监听来自Web浏览器的HTTP请求,然后根据请求的URL将请求数据解析为Request对象。
-
处理请求的URL:DispatcherServlet将请求的URL(Uniform Resource Locator)与处理程序进行匹配,确定要调用哪个控制器(Controller)来处理此请求。
-
调用相应的控制器:DispatcherServlet将请求发送给找到的控制器处理,控制器将执行业务逻辑,然后返回一个模型对象(Model)。
-
渲染视图:DispatcherServlet将调用视图引擎,将模型对象呈现为用户可以查看的HTML页面。
-
返回响应给客户端: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 执行流程总结
-
浏览器发送请求:http://localhost:8080/haha
-
SpringMVC的前端控制器DispatcherServlet接收到请求
-
DispatcherServlet根据请求路径 /haha 映射到 FirstController#名字随意(),调用该方法
-
FirstController#名字随意() 处理请求
-
FirstController#名字随意() 返回逻辑视图名称 first 给视图解析器
-
视图解析器找到 /WEB-INF/templates/first.html 文件,并进行解析,生成视图解析对象返回给前端控制器DispatcherServlet
-
前端控制器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=ttString 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(因为前端没有提交这个数据),前端的是usernameSystem.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,值为 123456789expires=...:设置过期时间为 2025 年 12 月 18 日 UTC 时间 12:00path=/:使 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.htmlreturn "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;
}
-
方法的返回值类型不是String,而是ModelAndView对象。
-
ModelAndView不是出现在方法的参数位置,而是在方法体中new的。
-
需要调用addObject向域中存储数据。
-
需要调用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失效,会话结束。
使用会话域的业务场景:
-
在A资源中通过重定向的方式跳转到B资源,因为是重定向,因此从A到B是两次请求,如果想让A资源和B资源共享同一个数据,可以将数据存储到session域中。
-
登录成功后保存用户的登录状态。
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 {// 这个方法的作用是将 逻辑视图名称 转换成 物理视图名称 ,并且最终返回视图对象ViewView view = resolveViewName(viewName, mv.getModelInternal(), locale, request);// 真正的将模板字符串转换成HTML代码,并且将HTML代码响应给浏览器。(真正的渲染)view.render(mv.getModelInternal(), request, response);}// 将 逻辑视图名称 转换成 物理视图名称,并且最终返回视图对象Viewprotected View resolveViewName(String viewName, @Nullable Map<String, Object> model,Locale locale, HttpServletRequest request) throws Exception {// 其实这一行代码才是真正起作用的:将 逻辑视图名称 转换成 物理视图名称 ,并且最终返回视图对象ViewViewResolver viewResolver; // 底层会创建一个ThymeleafViewResolver// 如果使用的是Thymeleaf,那么返回的视图对象:ThymeleafView对象。View view = viewResolver.resolveViewName(viewName, locale); return view;}}// 这是一个接口(负责视图解析的)
public interface ViewResolver { // 如果使用Thymeleaf,那么该接口的实现类就是:ThymeleafViewResolver// 这个方法就是将:逻辑视图名称 转换成 物理视图名称 ,并且最终返回视图对象 ViewView 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 | 直接执行,不返回值 |