笔记来源:【尚硅谷】SpringMVC教程丨一套快速上手spring mvc
从注解名称上我们可以看到,@RequestMapping
注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系
SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求
控制器中有多个方法对应同一个请求的情况
这是一种特殊情况,我们定义至少两个控制类,其中定义的控制器方法上@ReqeustMapping
指定同一请求路径
@Controller
public class HelloController {
@RequestMapping("/")
public String index() {
return "index";
}
}
@Controller
public class RequestMappingController {
@RequestMapping("/")
public String index() {
return "target";
}
}
如果存在两个或多个控制器,其控制器方法的@RequestMapping
一致,即多个控制器方法试图都想要处理同一个请求时,这时启动 Tomcat 时会抛出BeanCreationException
异常,并指明There is already 'helloController' bean method
即 helloController 中存在处理同意请求的控制器方法
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'requestMappingController' method
com.vectorx.springmvc.s00_helloworld.RequestMappingController#index()
to { [/]}: There is already 'helloController' bean method
com.vectorx.springmvc.s00_helloworld.HelloController#index() mapped.
但是这显然是不合理的,当一个系统方法很多,很难完全避免两个或多个同名方法的存在,那该怎么办呢?
查看@RequestMapping
注解的源码,可以清楚地看到@Target
中有TYPE
和METHOD
两个属性值,表示其可以作用在类或方法上
也就是说@RequestMapping
不仅可以在控制器方法上进行使用,也可以在控制器类上进行使用。那两种方式有什么区别呢?
@RequestMapping
标识一个类:设置映射请求的请求路径的初始信息@RequestMapping
标识一个方法:设置映射请求的请求路径的具体信息@Controller
@RequestMapping("/requestMappingController")
public class RequestMappingController {
@RequestMapping("/success")
public String success() {
return "success";
}
}
前台创建超链接,超链接跳转路径 = 控制器上@RequestMapping
映射请求的初始路径 + 控制器方法上@RequestMapping
映射请求的具体路径,即/requestMappingController/success
,再将其使用Tymeleaf
的@{}
语法包裹起来,这样Tymeleaf
会为我们自动加上上下文路径
<a th:href="@{/requestMappingController/success}">访问指定页面success.htmla>
测试结果
这样就可以为不同的控制器方法,在设置映射请求的请求路径时,指定不同的初始信息,从而避免控制器中有多个方法对应同一个请求的情况,这也就解决了之前的问题
@RequestMapping
注解的value
属性有哪些用途呢?
查看@RequestMapping
注解的源码,会发现其value
属性返回值为 String 类型的数组,这也说明了之所以@RequestMapping
注解的value
属性可以匹配多个请求的原因。通过为value
属性指定多个值的方式,就可以对个多个请求建立请求映射
在控制器方法上的@RequestMapping
中新增一个/test
请求映射
@RequestMapping(value = {"/success", "/test"})
public String success() {
return "success";
}
前台创建一个/test
的请求路径的超链接,以便进行测试
<a th:href="@{/requestMappingController/test}">>测试RequestMapping注解的value属性-->/testa>
测试结果
这样,同一个控制器方法就可以实现对多个请求进行统一处理
@RequestMapping
注解的method
属性有哪些用途呢?
GET
或POST
)匹配请求映射1、常用的请求方式有 4 种:GET、POST、PUT、DELETE
- GET 用于查询数据
- POST 用于添加数据
- PUT 用于更新数据
- DELETE 用户删除数据
但是很多情况下,习惯上会让 POST 承担更多的职责,即通过 POST 进行增、删、改的操作,可以说是“一个人揽了三个人的活”
2、还有 4 种不常用的请求:HEAD、PATCH、OPTIONS、TRACE
- HEAD 获取响应的报头,检查超链接的有效性、网页是否被修改,多用于自动搜索机器人获取网页的标志信息,获取 rss 种子信息,或者传递安全认证信息等
- PATCH 用于更新数据
- OPTIONS 用于测试服务器,解决同源策略和跨域请求问题
- TRACE 用于测试或诊断
有的资料还会介绍 CONNECT 请求,好像用于 HTTP 代理的,很少人听过,当然用的更少(我也是刚知道,有懂的求科普)
RequestMethod
类型的数组,表示该请求映射能够匹配多种请求方式的请求源码为证:
RequestMethod[] method() default {};
同时注意到method
属性默认值为空数组,是否说明控制器方法不添加method
属性时,不同的请求方法都能够匹配呢?
通过测试验证猜想
<a th:href="@{/requestMappingController/test}">测试RequestMapping注解的method属性-->GETa>
<br/>
<form th:action="@{/requestMappingController/test}" method="post">
<input type="submit" value="测试RequestMapping注解的method属性-->POST">
form>
测试结果:事实证明,控制器方法不添加method
属性时,可以接收GET
和POST
的请求,那么应该是默认不对请求方式限制了
本着严谨的态度,再测试下是否在不添加method
属性时,默认也支持PUT
和DELETE
请求
不过,PUT
和DELETE
请求比较特殊,需要使用到隐藏域,且method
固定为POST
<form th:action="@{/requestMappingController/test}" method="post">
<input type="hidden" name="_method" value="put"/>
<input type="submit" value="测试RequestMapping注解的method属性-->PUT">
form>
<br/>
<form th:action="@{/requestMappingController/test}" method="post">
<input type="hidden" name="_method" value="delete"/>
<input type="submit" value="测试RequestMapping注解的method属性-->DELETE">
form>
同时在web.xml
中需要添加隐藏域请求方式的过滤器配置
<filter>
<filter-name>HiddenHttpMethodFilterfilter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilterfilter-class>
filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilterfilter-name>
<url-pattern>/url-pattern>
filter-mapping>
测试结果也是成功的
存疑点:本来也想测试下
HEAD
、PTACH
、OPTIONS
和TRACE
这几种不常用的请求方式的,但是发现 form 表单好像不支持这些请求方式。即使使用隐藏域,也会变成GET
的请求方式。很疑惑这些请求方式要怎么模拟,有懂的求科普。这里记录下,留个印象,以待后续考古o(╯□╰)o
值得注意的是
若当前请求的请求地址满足请求映射的value
属性,但是请求方式不满足method
属性,则浏览器报错
HTTP Status 405 - Request method 'POST' not supported
代码验证
@RequestMapping(value = {"/success", "/test"}, method = RequestMethod.GET)
public String success() {
return "success";
}
验证结果:确实报了 405。同理可以将method
属性值中GET
改成POST
、PUT
和DELETE
对应进行验证即可,这里就不一一验证了
虽然无关紧要,但就是好奇为嘛我的是中文提示(灬°ω°灬)
发现这个版本 tomcat 的 lib 包中有 i18n 相关 jar 包,原来是做了国际化,“不一定有用”的知识又增加了~
对于处理指定请求方式的控制器方法,SpringMVC 中提供了@RequestMapping
的派生注解
GET
请求的映射->@GetMapping
POST
请求的映射->@PostMapping
PUT
请求的映射->@PutMapping
DELETE
请求的映射->@DeleteMapping
这里对于派生注解很容易理解,用数学上的等价解释就是
@ G e t M a p p i n g ( v a l u e = " . . . " ) < = = > @ R e q u e s t M a p p i n g ( v a l u e = " . . . " , m e t h o d = R e q u e s t M e t h o d . G E T ) @GetMapping(value="...") <==> @RequestMapping(value="...", method=RequestMethod.GET) @GetMapping(value="...")<==>@RequestMapping(value="...",method=RequestMethod.GET)
@ P o s t M a p p i n g ( v a l u e = " . . . " ) < = = > @ R e q u e s t M a p p i n g ( v a l u e = " . . . " , m e t h o d = R e q u e s t M e t h o d . P O S T ) @PostMapping(value="...") <==> @RequestMapping(value="...", method=RequestMethod.POST) @PostMapping(value="...")<==>@RequestMapping(value="...",method=RequestMethod.POST)
@ P u t M a p p i n g ( v a l u e = " . . . " ) < = = > @ R e q u e s t M a p p i n g ( v a l u e = " . . . " , m e t h o d = R e q u e s t M e t h o d . P U T ) @PutMapping(value="...") <==> @RequestMapping(value="...", method=RequestMethod.PUT) @PutMapping(value="...")<==>@RequestMapping(value="...",method=RequestMethod.PUT)
@ D e l e t e M a p p i n g ( v a l u e = " . . . " ) < = = > @ R e q u e s t M a p p i n g ( v a l u e = " . . . " , m e t h o d = R e q u e s t M e t h o d . D E L E T E ) @DeleteMapping(value="...") <==> @RequestMapping(value="...", method=RequestMethod.DELETE) @DeleteMapping(value="...")<==>@RequestMapping(value="...",method=RequestMethod.DELETE)
通过代码进行测试
后台代码
@Controller
@RequestMapping("/requestMappingController")
public class RequestMappingController {
@GetMapping("/success")
public String successGet() {
return "successget";
}
@PostMapping("/success")
public String successPost() {
return "successpost";
}
@PutMapping("/success")
public String successPut() {
return "successput";
}
@DeleteMapping("/success")
public String successDelete() {
return "successdelete";
}
}
前台代码
<a th:href="@{/requestMappingController/success}">测试GetMappinga>
<br/><br/>
<form th:action="@{/requestMappingController/success}" method="post">
<input type="submit" value="测试PostMapping">
form>
<br/>
<form th:action="@{/requestMappingController/success}" method="post">
<input type="hidden" name="_method" value="put"/>
<input type="submit" value="测试PutMapping">
form>
<br/>
<form th:action="@{/requestMappingController/success}" method="post">
<input type="hidden" name="_method" value="delete"/>
<input type="submit" value="测试DeleteMapping">
form>
别忘了,新增四个测试页面successget.html
、successpost.html
、successput.html
和successdelete.html
,在这个四个不同页面中标注不同的内容以示区分
验证结果:可以看到,GET
/POST
/PUT
/DELETE
等请求方式,均能够被正常接收和处理
GET
,POST
,PUT
,DELETE
,但是目前浏览器只支持GET
和POST
(OS:刚才还有点疑惑的,这里好像“水落石出了”)method
设置了其他请求方式的字符串(PUT
或DELETE
),则按照默认的GET
请求方式处理PUT
和DELETE
请求,则需要通过 Spring 提供的过滤器HiddenHttpMethodFilter
,在 restful 部分会讲到(OS:我上面刚自己研究了下,没想到老师这里会讲^_^
)如果去除过滤器HiddenHttpMethodFilter
的配置,同时注释掉隐藏域的代码,并将method
为post
值改成put
和delete
<form th:action="@{/requestMappingController/success}" method="put">
<input type="submit" value="测试PutMapping">
form>
<br/>
<form th:action="@{/requestMappingController/success}" method="delete">
<input type="submit" value="测试DeleteMapping">
form>
按照上述的说法,会按照默认的GET
请求方式处理,这里进行验证
可以发现,本应走PUT
和DELETE
方式的请求,都被GET
请求方式的控制器方法处理了。如果这里控制器中连相应GET
请求方式都没有定义的话,肯定是报 405 了。这里注释掉@GetMapping
的请求方法
//@GetMapping("/success")
//public String successGet() {
// return "successget";
//}
验证结果:显而易见
@RequestMapping
注解的params
属性通过请求的请求参数匹配请求映射
它是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系
param
:要求请求映射所匹配的请求必须携带param
请求参数!param
:要求请求映射所匹配的请求必须不能携带param
请求参数param=value
:要求请求映射所匹配的请求必须携带param
请求参数且param=value
param!=value
:要求请求映射所匹配的请求必须携带param
请求参数但是param!=value
若当前请求满足@RequestMapping
注解的value
和method
属性,但是不满足params
属性,此时页面显示400
错误,即请求不匹配
这里指定params
的值指定为username
,这就要求请求中必须携带username
的请求参数
@RequestMapping(
value = {"/testParams"},
params = {"username"}
)
public String testParams() {
return "success";
}
前台测试代码:分别不加请求参数和加上请求参数,进行测试
<a th:href="@{/requestMappingController/testParams}">测试RequestMapping注解的params属性==>testParamsa><br/>
<a th:href="@{/requestMappingController/testParams?username=admin}">测试RequestMapping注解的params属性==>testParams?username=admina>
测试结果
可以发现,当配置了params
属性并指定相应的请求参数时,请求中必须要携带相应的请求参数信息,否则前台就会报抛出400
的错误信息,符合预期
HTTP Status 400:Parameter conditions "username" not met for actual request parameters
不过在Tymeleaf
中使用问号的方式会有错误提示,虽然不影响功能,但不想要错误提示的话,最好通过(...,...)
的方式进行包裹,多个参数间通过,
隔开
<a th:href="@{/requestMappingController/testParams(username='admin', password=123456)}">测试RequestMapping注解的params属性==>testParams(username='admin', password=123456)a><br/>
测试验证
可以发现,通过括号包裹的方式,Tymeleaf
最终会帮我们将其解析成?username=admin&password=123456
的格式
存疑点:实测发现,``testParams(username=‘admin’, password=123456)
改成
testParams(username=admin, password=123456),即
admin`不加单引号也是可以的,这与课堂上所讲的并不一致,此点存疑
这里将params = {"username"}
中username
前加上!
即可,即params = {"!username"}
,这就要求请求中的请求参数中不能携带username
请求参数
@RequestMapping(
value = {"/testParams"},
params = {"!username"}
)
public String testParams() {
return "success";
}
测试结果
可以发现,没有携带username
请求参数的请求变得能够正常访问,而携带了username
请求参数的请求反而出现了400
的异常信息,符合预期
HTTP Status 400:Parameter conditions "!username" not met for actual request parameters: username={admin}, password={123456}
这里params
的值指定为username=admin
的形式,即要求请求中不仅要携带username
的请求参数,且值为admin
@RequestMapping(
value = {"/testParams"},
params = {"username=admin"}
)
public String testParams() {
return "success";
}
测试结果
可以发现,不携带username
请求参数的请求和携带username
请求参数但不为admin
的请求,均提示400
的请求错误,符合预期
这里将params
的值指定为username!=admin
,即要求请求中不仅要携带username
的请求参数,且值不能为admin
@RequestMapping(
value = {"/testParams"},
params = {"username!=admin"}
)
public String testParams() {
return "success";
}
测试结果
实际测试结果发现:不携带username
请求参数的请求和携带username
请求参数但值不为admin
的请求,可以正常访问;而携带username
请求参数但值为admin
的请求,不能正常访问,不完全符合预期
存疑点:不携带
username
请求参数的请求能够正常访问,这一点不符合课程中讲解的内容,此点存疑
@RequestMapping
注解的headers
属性通过请求的请求头信息匹配请求映射
它是一个字符串类型的数组,可以通过四种表达式设置请求头信息和请求映射的匹配关系
header
:要求请求映射所匹配的请求必须携带header
请求头信息header
:要求请求映射所匹配的请求必须不能携带header
请求头信息header=value
:要求请求映射所匹配的请求必须携带header
请求头信息且header=value
header!=value
:要求请求映射所匹配的请求必须携带header
请求头信息且header!=value
若当前请求满足@RequestMapping
注解的value
和method
属性,但是不满足headers
属性,此时页面显示404
错误,即资源未找到
测试代码
@RequestMapping(
value = {"/testHeaders"},
headers = {"Host=localhost:8081"}
)
public String testHeaders() {
return "success";
}
测试结果
因为我本地tomcat
启动端口是8080
,所以是匹配不成功的,此时显示404
错误,符合预期
再将端口号修改为8080
@RequestMapping(
value = {"/testHeaders"},
headers = {"Host=localhost:8080"}
)
public String testHeaders() {
return "success";
}
测试结果
这一次,因为端口号一致,所以成功跳转,符合预期
?
:表示任意的单个字符*
:表示任意的0个或多个字符**
:表示任意的一层或多层目录。注意:在使用**
时,只能使用/**/xxx
的方式探子来报:
**
经实测,0 层目录也可以,这里严谨来说,应该是“表示任意层目录”
后台测试代码
//ant风格路径
@RequestMapping("/a?a/testAnt")
public String testAnt() {
return "success";
}
前台测试代码
Ant风格路径——?:<br/>
<a th:href="@{/requestMappingController/testAnt}">测试ant风格路径_/a?a/testAnt==>/testAnta><br/>
<a th:href="@{/requestMappingController/a1a/testAnt}">测试ant风格路径_/a?a/testAnt==>/a1a/testAnta><br/>
<a th:href="@{/requestMappingController/aaa/testAnt}">测试ant风格路径_/a?a/testAnt==>/aaa/testAnta><br/>
<a th:href="@{/requestMappingController/aaaa/testAnt}">测试ant风格路径_/a?a/testAnt==>/aaaa/testAnta><br/>
<a th:href="@{/requestMappingController/a/a/testAnt}">测试ant风格路径_/a?a/testAnt==>/a/a/testAnta><br/>
<a th:href="@{/requestMappingController/a?a/testAnt}">测试ant风格路径_/a?a/testAnt==>/a?a/testAnta><br/>
测试结果
可以发现,/a?a/testAnt
能够匹配的路径有
/a1a/testAnt
/aaa/testAnt
不能匹配的路径有
/testAnt
/aaaa/testAnt
/a/a/testAnt
/a?a/testAnt
即证明,?
修饰的路径,有且必须有一个字符代替?
的位置,即只能匹配单个字符,且不能为/
和?
这两种特殊字符(因为/
和?
在 url 路径中比较特殊,除此之外其他单个字符均可),符合预期
后台测试代码
//ant风格路径
@RequestMapping("/a*a/testAnt")
public String testAnt() {
return "success";
}
前台测试代码
Ant风格路径——*:<br/>
<a th:href="@{/requestMappingController/aa/testAnt}">测试ant风格路径_/a*a/testAnt==>/aa/testAnta><br/>
<a th:href="@{/requestMappingController/a1a/testAnt}">测试ant风格路径_/a*a/testAnt==>/a1a/testAnta><br/>
<a th:href="@{/requestMappingController/aaaaa/testAnt}">测试ant风格路径_/a*a/testAnt==>/aaaaa/testAnta><br/>
测试结果
可以发现,/a*a/testAnt
能够匹配的路径有
/aa/testAnt
/a1a/testAnt
/aaaaa/testAnt
即证明,*
修饰的路径,允许 0 个或多个字符代替*
的位置,符合预期
上面说到,在使用**
时,只能使用/**/xxx
的方式,这里对其进行验证
后台测试代码
//ant风格路径
@RequestMapping("/a**a/testAnt")
public String testAnt() {
return "success";
}
前台测试代码
Ant风格路径——**:<br/>
<a th:href="@{/requestMappingController/aa/testAnt}">测试ant风格路径_/a**a/testAnt==>/aa/testAnta><br/>
<a th:href="@{/requestMappingController/a1a/testAnt}">测试ant风格路径_/a**a/testAnt==>/a1a/testAnta><br/>
<a th:href="@{/requestMappingController/a1a/testAnt}">测试ant风格路径_/a**a/testAnt==>/a11a/testAnta><br/>
<a th:href="@{/requestMappingController/a**a/testAnt}">测试ant风格路径_/a**a/testAnt==>/a**a/testAnta><br/>
<a th:href="@{/requestMappingController/aaaaa/testAnt}">测试ant风格路径_/a**a/testAnt==>/aaaaa/testAnta><br/>
<a th:href="@{/requestMappingController/a/a/testAnt}">测试ant风格路径_/a**a/testAnt==>/a/a/testAnta><br/>
<a th:href="@{/requestMappingController/a/d/e/a/testAnt}">测试ant风格路径_/a**a/testAnt==>/a/d/e/a/testAnta><br/>
测试结果
可以发现,/a**a/testAnt
能够匹配的路径有
/aa/testAnt
/a1a/testAnt
/a11a/testAnt
/a**a/testAnt
/aaaaa/testAnt
不能匹配的路径有
/a/a/testAnt
/a/d/e/a/testAnt
不符合预期
存疑点:这里
/a**a/
多层路径不能匹配,而 0 个或多个字符能够匹配,这与课程中的“两颗星真的就是两颗星”不符,其匹配规则与/a*a/
一致,即/a**a/ <==> /a*a/
,两颗星与一颗星作用相同,此点存疑
上述只是对**
的错误用法时的匹配规则,下面才是真正对**
的正确用法验证,请看
后台测试代码
//ant风格路径
@RequestMapping("/**/testAnt")
public String testAnt() {
return "success";
}
前台测试代码
Ant风格路径——**:<br/>
<a th:href="@{/requestMappingController/testAnt}">测试ant风格路径_/a**a/testAnt==>/testAnta><br/>
<a th:href="@{/requestMappingController/a/testAnt}">测试ant风格路径_/a**a/testAnt==>/a/testAnta><br/>
<a th:href="@{/requestMappingController/a/a/a/a/testAnt}">测试ant风格路径_/a**a/testAnt==>/a/a/a/a/testAnta><br/>
测试结果
可以发现,不管中间添加多少层路径都是能够匹配成功的,符合预期
/deleteUser?id=1
/deleteuser/11
SpringMVC 路径中的占位符常用于 restful 风格中,当请求路径中将某些数据通过路径的方式传输到服务器中,就可以在相应的@RequestMapping
注解的value
属性中通过占位符{xxx}
表示传输的数据,再通过@PathVariable
注解,将占位符所表示的数据赋值给控制器方法的形参
{xxx}
占位符而不使用@PathVariable
注解;②形参名称与请求中的占位符名称同名后台测试代码
@RequestMapping("/testRest/{id}/{username}")
public String testRest(String id, String username) {
System.out.println("id=" + id + ", username=" + username);
return "success";
}
前台测试代码
路径中的占位符:<br/>
<a th:href="@{/requestMappingController/testRest/1/admin}">测试路径中的占位符==>/testRest/1/admina><br/>
测试结果
后台日志
id=null, username=null
可以发现,请求能够匹配成功,但是同名形参无法接收到占位符的值
查看PathVariable
注解源码
可以看到,它只能作用在方法参数上,那么怎么用就一目了然了
后台测试代码
@RequestMapping("/testRest/{id}/{username}")
public String testRest(@PathVariable("id") String id, @PathVariable("username") String username) {
System.out.println("id=" + id + ", username=" + username);
return "success";
}
测试结果
后台日志
id=1, username=admin
可以发现,请求能够匹配成功,形参通过@PathVariable
注解接收到了占位符的值
<a th:href="@{/requestMappingController/testRest}">测试路径中的占位符==>/testRest</a><br/>
测试结果
可以看到,没有占位符时,直接显示了404
错误,即表示路径中存在占位符的控制器方法不能匹配未设置占位符的请求
也就是说,路径中存在占位符的控制器方法,只能接收带了对应占位符的请求
<a th:href="@{/requestMappingController/testRest///}">测试路径中的占位符_空值==>/testRest///a><br/>
<a th:href="@{/requestMappingController/testRest/ / /}">测试路径中的占位符_空格==>/testRest/ / /a><br/>
测试结果
同时占位符为空格的情况是,后台打印了日志:id= , username=
可以看到,
404
错误URL
编码,即%20
由以上情况测试结果可以得出
@PathVariable
注解获取(就目前所学知识而言)@RequestMapping
注解
value
属性:可以匹配多个请求路径,匹配失败报404
method
属性:支持GET
、POST
、PUT
、DELETE
,默认不限制,匹配失败报405
params
属性:四种方式,param
、!param
、param==value
、param!=value
,匹配失败报400
headers
属性:四种方式,header
、!header
、header==value
、header!=value
,匹配失败报400
?
(单个字符)、*
(0 或多个字符)和**
(0 或多层路径){xxx}
占位符、@PathVariable
赋值形参以下导图仅供参考