SpringMVC基础¶
约 4832 个字 426 行代码 5 张图片 预计阅读时间 21 分钟
MVC架构模式介绍¶
MVC(Model View Controller)是软件工程中的一种软件架构模式,它把软件系统分为模型(Model)、视图(View)和控制器(Controller)三个基本部分。用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑
- M:
Model
模型层:存放和数据库对象的实体类以及一些用于存储非数据库表完整相关的VO(valueObject)对象或者存放一些对数据进行逻辑运算操作的的一些业务处理代码 - V:
View
视图层:存放一些视图文件相关的代码,例如HTML、CSS、JS - C:
Controller
控制层:接收客户端请求,获得请求数据,最后将准备好的数据响应给客户端
MVC模式工作流程如下图所示:
MVC模式下项目中的常见包:
-
Model
模型层:- 实体类包(
pojo
/entity
/bean
)专门存放和数据库对应的实体类和一些VO对象 - 数据库访问包(
dao
/mapper
)专门存放对数据库不同表格CURD方法封装的一些类 - 服务包(
service
)专门存放对数据进行业务逻辑运算的一些类
- 实体类包(
-
Controller
控制层:控制层包(controller
) -
View
视图层:Web目录下的视图资源,例如HTML、CSS、JS、图片
SpringMVC介绍¶
SpringMVC,全称为Spring Web MVC,是基于Servlet API构建的原始Web框架,从一开始就包含在Spring框架中。它的正式名称Spring Web MVC来自其源模块的名称(Spring-webmvc
),在大部分情况下都会称为SpringMVC
关于Servlet
Servlet(server applet)是运行在服务端(例如Tomcat)的Java小程序,是SUN公司提供一套定义动态资源规范。从代码层面上来讲Servlet就是一个接口,其主要用来接收、处理客户端请求、响应给浏览器的动态资源。在整个Web应用中,Servlet主要负责接收处理请求、协同调度功能以及响应数据。因此可以把Servlet称为Web应用中的控制器
不是所有的Java类都能用于处理客户端请求,能处理客户端请求并做出响应的一套技术标准就是Servlet,因为其要处理客户端请求,所以Servlet必须在WEB项目中开发且在Tomcat这样的服务容器中运行
在Spring实现MVC时结合了自身项目的特点对MVC模式进行了一定的改变,如下图所示:
可以从图中看到最大的改变就是浏览器直接请求Controller
而不是通过视图发起请求
SpringMVC与SpringBoot¶
SpringMVC是Spring框架的一个Web模块,专注于MVC模式下的Web开发,而SpringBoot是Spring的扩展框架,通过自动配置和starters简化了Spring应用的搭建和部署,而SpringBoot是实现SpringMVC的其中一种方式,通过添加各种依赖和SpringMVC框架来实现Web功能
接下来的介绍会基于SpringBoot项目,在SpringBoot项目中同时介绍SpringMVC的相关知识。而本文档介绍SpringMVC基础内容时从下面三个方面进行:
- 建立连接
- 请求处理
- 响应处理
客户端与服务端建立连接¶
@RequestMapping
介绍¶
在SpringMVC中,要想做到浏览器可以请求到后端的动态资源可以使用@RequestMapping
注解实现URL路由映射。对应的,在具体的类上添加@RestController
例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 |
|
在@RequestMapping
注解中,第一个值为路径参数,例如上面例子中的"/sayHello"
,这个路径可以带/
,也可以不带/
,但是一般情况下推荐带上/
表示路径。接着,在浏览器通过地址http://localhost:8080/sayHello
中发起请求即可看到返回的字符串Hello World!
@RequestMapping
修饰位置¶
@RequestMapping
既可以修饰类,也可以修饰方法:
@RequestMapping
修饰类时,表示请求路径的初始信息@RequestMapping
修饰方法时,表示请求路径的具体信息
当修饰类和方法时,访问的地址就是类路径+方法路径,例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
此时的访问路径即为http://localhost:8080/hello/sayHello
,一般情况下建议在类上添加@RequestMapping
,这样可以避免方法路径的重复导致的服务器错误
@RequestMapping
多层路径¶
@RequestMapping
除了设置一层路径以外,还可以设置多层路径,例如:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
此时的访问路径即为http://localhost:8080/user/hello/v1/sayHello
@RequestMapping
与@GetMapping
和@PostMapping
¶
默认情况下,@RequestMapping
可以处理所有类型的HTTP请求,包括但不限于GET
和POST
,如果想要指定只处理GET
请求和POST
请求,可以设置@RequestMapping
的第二个属性值
例如,对于GET
请求:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
此时只有发送了GET
请求才可以执行hello()
方法
为了简化上面的写法,如果只想让方法处理GET
请求可以使用@GetMapping
,例如上面的代码可以修改为:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
同样,对于POST
请求可以通过@RequestMapping
的第二个属性值进行指定,例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
同样,如果只想让方法处理POST
请求可以使用@PostMapping
,例如上面的代码可以修改为:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
对于其他的请求方式也是同样的逻辑,此处不再演示
Note
需要注意的是,@GetMapping
和@PostMapping
不支持类上添加,只支持方法上添加,对于其他针对某一种请求方式的注解也是如此
请求处理¶
处理一个参数¶
当请求中只有一个参数,例如?name=zhangsan
时,接收方式很简单,只需要在处理方法中定义一个形参名字与请求参数key
的名字一样即可,例如下面的代码用于接收name
参数:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
此时发送请求为http://localhost:8080/hello/sayHello?name=zhangsan
即可收到下面的信息:
Text Only | |
---|---|
1 |
|
对于POST
请求来说,参数放在请求参数或者请求体都是可以正常接收的,接收代码与GET
请求类似:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
但是需要注意的是,如果方法参数是基本数据类型(不包括布尔类型,如果是布尔类型不传递参数时,默认值为false
)时,参数不可以不传递,否则就会报错,因为当不传递参数时,如果是引用数据类型,那么值为null
可以正常赋值给引用数据类型,但是如果参数是基本数据类型无法将null
转换为基本数据类型从而报错。所以在处理基本数据类型的参数时建议使用对应的包装类而非基本数据类型,例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
为什么前端URL参数和后端方法参数一致可以获取到参数
实际上这与Servlet有关,在Servlet开发中,客户端向服务器端发出请求,服务器端的软件Tomcat接收到用户请求后会将请求报文的信息转换为HttpServletRequest
对象,该对象中包含着请求中的所有信息,例如请求头、请求行。需要注意,这一过程中的HttpServletRequest
对象并不是由程序员手动创建的,而是由Tomcat自动创建,并且此时除了存在HttpServletRequest
对象以外,还有一个HttpServletResponse
对象,该对象用于存储响应报文信息。在Spring Web MVC中包括了Servlet,所以可以直接在方法参数中使用HttpServletRequest
对象,另外因为SpringBoot集成了Tomcat,所以该对象也会被正常设置。以获取参数String name
为例,实现下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 |
|
如果需要为形参名指定别名,可以在参数部分使用@RequestParam
注解,此时前端传递时应该匹配的时@RequestParam
中的名称而不是变量名:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
在上面的代码中,前端传递参数时key
应该为username
而不再是name
,例如:http://localhost:8080/hello/sayHello?username=zhangsan
但是,一旦指定了别名,如果不传递参数就会出现无法找到指定资源的情况,即不可以不传递参数。以上面的代码演示,使用Postman发送不指定参数的请求结果如下:
出现这个问题可以从@RequestParam
源码分析:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
在@RequestParam
注解源码中有一个字段required
,这个字段默认值为true
,表示一定要传递参数,所以可以通过设置该字段为false
使其在不传递参数时也可以正常请求资源,例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
此时发起请求http://localhost:8080/hello/sayHello
即可得到下面的结果:
Text Only | |
---|---|
1 |
|
处理多个参数¶
处理多个参数与处理一个参数比较类似,只需要在方法参数部分添加多个参数即可,例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
需要注意的是,URL中的参数顺序可以不与方法参数的顺序一致,但是参数名必须一致,即请求地址可以为http://localhost:8080/hello/sayHello?id=1&name=zhangsan
处理对象¶
上面介绍了处理多个参数,但是多个参数彼此并没有任何关系,有时前端传递的一个参数可能是属于某一个类的对象的字段值,此时就推荐使用对象而不是使用多个参数的方式,例如下面有一个User
类(注意提供getter
和setter
):
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
处理对象的方式与处理多个参数的方式类似,只需要在方法参数部分添加对象类型的参数即可,例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
前端传递方式与多个参数一致,例如:http://localhost:8080/hello/sayHello?name=zhangsan&age=18&gender=man
。此时可以看到下面的结果:
Text Only | |
---|---|
1 |
|
需要注意的是,前面在处理一个参数的时候提到过,如果是基本数据类型(除了布尔类型),则参数必须要传递,但是在处理对象时,对象的字段可以是基础数据类型,因为此时基础数据类型存在默认值,如果没有传递则直接使用默认值。此处不再演示,但是还是推荐使用包装类而不是基本数据类型
处理数组¶
处理数组也是类似,只需要将参数部分修改为数组类型即可:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
此时前端传递参数可以有两种方式:
key
和value
分开指定:http://localhost:8080/hello/sayHello?params=param1¶ms=param2¶ms=param3
key
与value
合并指定:http://localhost:8080/hello/sayHello?params=param1,param2,param3
这两种方式都可以正常得到结果:
Text Only | |
---|---|
1 |
|
但是现在有一个问题,如果是合并指定,那么数组中存储的是三个单独的元素还是一个整体的元素。为了验证这一点,可以在方法中打印数组的大小:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
以合并指定的方式发送请求可以得到如下结果:
Text Only | |
---|---|
1 |
|
通过结果可以看到数组中存储的是三个单独元素而并非一个整体的元素
处理集合¶
集合与数组比较类似,但是需要注意的是,默认情况下,请求中参数名相同的多个值是封装到数组而并非集合,直接传递给参数的集合会出现错误。为了避免这个问题,可以使用@RequestParam
进行参数绑定,确保数组可以转换为集合,以List
为例:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
请求方式与数组一致,此处不再演示
处理JSON数据¶
对于JSON字符串来说,需要在参数部分使用@RequestBody
进行绑定,而JSON字符串中保存的是一个JSON对象,所以考虑使用一个类对象进行接收(此处依旧使用User
类),例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
传递JSON数据时建议在请求体中传递,例如:
JSON | |
---|---|
1 2 3 4 5 |
|
此时可以得到结果:
Text Only | |
---|---|
1 |
|
获取到URL中的资源路径¶
在前面@RequestParam
中使用的都是静态的路径,如果路径中存在动态变化的部分,就需要在@RequestParam
中使用参数的形式(或者具体请求方式的注解,例如@GetMapping
),对应的方法要获取到这个动态变化的参数就需要使用@PathVariable
注解,例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
需要注意的是,此时必须保证路径参数和方法参数一致,即{id}
与Integer id
一致,此时请求路径为:http://localhost:8080/hello/sayHello/1
,得到的结果为:
Text Only | |
---|---|
1 |
|
也可以通过@PathVariable
指定别名,此时{user_id}
需要与@PathVariable("user_id")
一致,例如:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
此时请求路径为:http://localhost:8080/hello/sayHello/1
,得到的结果为:
Text Only | |
---|---|
1 |
|
但是需要注意的是,虽然@PathVariable
注解也存在required
字段,但是不论是修改为true
还是false
都必须指定路径参数
上传文件¶
上传文件时使用的参数类型是MultipartFile
,例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
如果想参数指定别名,可以使用@RequestPart
注解:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 |
|
获取Cookie和Session¶
关于Cookie和Session的介绍,此处不再提及,具体见HTTP中的Cookie和Session
对于获取Cookie来说,有两种方式:
- 使用Servlet的
HttpServletRequest
对象:可以获取到Cookie中所有的键值对,更常用 - 使用注解
@CookieValue
注解:根据注解指定的key
(或者方法形参名,此时不需要单独在注解中指定key
名称)获取到具体的某一个Cookie值
例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Java | |
---|---|
1 2 3 4 |
|
对于Session来说,在获取之前要先使用HttpServletRequest
对象进行创建,该类中有一个getSession
方法,该方法有两个版本:
- 无参版本
getSession()
:获取已有的HttpSession
对象,如果不存在该对象则创建新的HttpSession
对象并返回 - 有参版本
getSession(boolean create)
:如果create
为true
,则获取已有的HttpSession
对象,如果不存在该对象则创建新的HttpSession
对象并返回(与无参版本效果相同);如果create
为false
,则获取已有的HttpSession
对象,如果不存在该对象则返回null
例如下面的设置方式:
Java | |
---|---|
1 2 3 4 5 6 7 8 |
|
Note
需要注意的是,不需要单独调用HttpServletResponse
对象将对应的JSESSIONID
设置到Cookie中,这一步已经由getSession
方法实现
设置完成后即可获取Session,获取方式有三种:
- 使用
HttpServletRequest
对象获取:获取方式详细且可以根据具体的键获取到对应的值 - 使用
HttpSession
对象获取:与第一种方式类似,但是不用显式调用getSession
方法,默认情况下调用的是无参getSession
- 使用
@SessionAttribute
注解获取:根据注解指定的key
(或者方法形参名,此时不需要单独在注解中指定key
名称)获取到具体的值,但是必须确保Session已经创建
例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 8 |
|
Java | |
---|---|
1 2 3 4 5 6 |
|
Java | |
---|---|
1 2 3 4 5 |
|
获取Header¶
获取Header的方式有两种:
- 使用
HttpServletRequest
对象:可以获取到所有的Header键值对,更常用 - 使用
@RequestHeader
注解:根据注解指定的key
(或者方法形参名,此时不需要单独在注解中指定key
名称)获取到具体的某一个Header值
例如下面的代码:
Java | |
---|---|
1 2 3 4 5 6 |
|
Java | |
---|---|
1 2 3 4 5 |
|
响应处理¶
返回静态页面¶
返回静态页面的方式比较直接,在项目目录下的resource
目录中的static
创建一个test.html
,内容任意,接着写下下面的代码:
Java | |
---|---|
1 2 3 4 5 6 7 |
|
接着,使用浏览器向服务器发送请求:http://localhost:8080/static
,会发现页面显示的内容并不是test.html
中的内容,而是将test.html
作为纯文本渲染到页面上
出现这个问题的原因就在@RestController
注解上,查看这个注解的源码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
对于前三行注解分别表示的含义如下:
@Target
:指定注解可以应用的目标元素类型,ElementType.TYPE
表示该注解能用于类、接口、枚举等类型声明上,类似的还有ElementType.METHOD
表示该注解能用于方法上@Retention
:指定注解的保留策略,即注解在什么时候还有效。RetentionPolicy.RUNTIME
表示注解在运行时仍然有效,可以通过反射获取。类似的还有SOURCE
和CLASS
Documented
:标记注解是否包含在JavaDoc文档中
重点看后两个注解:
在早期的SpringMVC中,前后端并没有分离,所以有些页面是通过后端返回的,而现在大部分都是前后端分离模式,即后端只需要告诉前端需要的数据,前端根据数据进行页面处理。而@Controller
注解出现的时期就是前后端没有分离的早期,所以默认情况下返回的是页面而非数据,而如果要返回数据而非页面就需要使用@ResponseBody
注解,此时返回的数据就被当作text/html
类型进行处理,在进入了前后端分离时,大部分情况下都是返回数据,所以为了简化开发,将@Controller
和@ResponseBody
进行合并,得到了@RestController
注解
基于上面的原因,所以要返回静态页面需要使用@Controller
注解而非@RestController
注解:
Java | |
---|---|
1 2 3 4 5 6 7 |
|
再次发送请求即可看到test.html
中的内容
返回数据¶
上面已经介绍了@ResponseBody
注解的基本作用,下面详细介绍该注解:
@ResponseBody
既可以用在类上,也可以用在方法上。当注解用在类上,表示这个类所有的方法返回的都是纯text/html
数据,而不返回页面;当注解用在方法上,表示当前方法返回的是数据而不是页面,其他方法不受影响
以用在类上为例:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
可以发现两个方法都是将返回的数据直接渲染到页面上,特别是response2
方法,并不会返回test.html
文件中的内容
返回JSON数据¶
后端需要向前端返回JSON数据时只需要返回一个对象即可,例如以前面的User
类为例,返回一个User
对象:
Java | |
---|---|
1 2 3 4 5 6 7 8 |
|
前端得到的数据如下:
Text Only | |
---|---|
1 |
|
设置状态码¶
设置状态码通过HttpServletResponse
对象进行设置即可:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
设置Header¶
一些常见的头部字段可以通过@RequestMapping
注解实现,先看@RequestMapping
的源码:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
其中:
value
:指定映射的URLmethod
:指定请求类型,如GET
,POST
等consumes
:指定处理请求的提交内容类型(Content-Type
),例如application/json
,text/html
produces
:指定返回的内容类型,还可以同时设置返回值的字符编码Params
:要求请求中必须包含某些参数值时,才让该方法处理headers
:要求请求中必须包含某些指定的header值,才能让该方法处理请求
自定义Header可以通过HttpServletResponse
对象进行设置:
Java | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|