一. HelloWorld运行流程
1)客户端点击链接会发送链接http://localhost:8080/springmvc/hello请求;
2)来到Tomcat服务器;
3)SpringMVC的前端控制器收到所有请求;
4)请求的地址和@RequestMapping标注的哪个匹配,来找到到底使用哪个类的哪个方法来处理;
5)前端控制器找到了目标处理器类和目标方法,直接利用反射执行目标方法;
6)方法执行完成后,会有一个返回值,SpringMVC认为这个返回值就是要去的页面地址;
7)拿到方法返回值以后,用视图解析器进行拼串得到完整的页面地址;
8)拿到页面地址,前端控制器转发页面。
二.url-pattern
在之前的HelloWorld实验中,了解到:
- /会拦截所有请求,但不会拦截*.jsp请求,能保证jsp访问正常。
- /*范围更大,还会拦截*.jsp这些请求,一旦拦截jsp页面就不能显示了。
为什么/会拦截.html请求,而不会拦截*.jsp请求呢?首先我们要知道的是处理*jsp是Tomcat的事。所有项目下的小web.xml都是继承Tomcat的大web.xml。
1)服务器的大web.xml中有一个DefaultServlet的url-pattern=/
2) 前端控制器的url-pattern=/
DefaultServlet是Tomcat中处理静态资源的一个Servlet。除了JSP和Servlet外剩下的都是静态资源,index.html也是静态资源。现在前端控制器的/禁用了Tomcat的DefaultServlet。 静态资源会来到前端控制器看那个方法的RequestMapping是这个index.html。
JSP页面能运行是我们没有覆盖JspServlet的配置。
使用/不仅仅是因为能保证jsp访问正常,也是为了迎合Rest风格URL。
三.@RequestMapping映射
SpringMVC使用@RequestMapping注解为控制器指定可以处理哪些URL请求。在控制器的类定义以及方法定义域处都可以标注@RequestMapping。
- 标记在类上:提供初步的映射信息。相当于WEB应用的根目录。
- 标记在方法上:提供进一步的细分映射信息。相当于标记在类上的URL。
若类上未标注@RequestMapping,则方法标记的URL相当于WEB应用的根目录。
作用:DispatcherServlet截获请求后,就通过控制器上@RequestMapping提供的映射信息确定请求所对应的处理方法。
源码如下:
package org.springframework.web.bind.annotation;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
String[] value() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
标准的HTTP请求报头:
3.1 属性
3.1.1 method:规定请求方式
定义控制器方法:
@RequestMapping("/haha")
@Controller
public class RequestMappingTestController {
/*
* RequestMapping的其他属性:
* method: 限定请求方式
* Http协议中的所有请求方式: GET PSOT PUT PATCH DELETE OPTIONS
*
* method=RequestMethod.POST:只接受这种类型的请求,默认是什么都可以
* 不是规定的方式报错,4xx都是客户端错误
*/
@RequestMapping(value="/handle02",method=RequestMethod.POST)
public String handle02(){
return "success";
}
}
以get方式请求:
<a href="haha/handle02">GET方式请求</a>
发生请求错误:
以POST方式请求成功:
<form action="haha/handle02" method="post">
<input type="submit"/>
</form>
3.1.2 params:规定请求参数
params参数支持简单表达式:
- param1: 表示请求必须包含名为 param1 的请求参数。 eg:params={"username"} 发送请求的时候必须带上一个名为username的参数,如果没带404
- !param1: 表示请求不能包含名为 param1 的请求参数。eg:params={"!username"} 发送请求的时候不携带一个名为username的参数,带了404
- param1 != value1: 表示请求包含名为 param1 的请求参数,但其值不能为 value1
- {“param1=value1”, “param2”}: 请求必须包含名为 param1 和param2 的两个请求参数,且 param1 参数的值必须为 value1
@RequestMapping(value="/handle03",params={"username=123","pwd","!age"})
public String handle03(){
return "success";
}
3.1.3 headers:规定请求头
headers和params一样支持上述的简单表达式,也可以配置浏览器信息。
/*
* header:规定请求头,和params一样支持简单的表达式
* User-Agent:浏览器信息
* 实验 让IE能访问,让谷歌不能访问
* 谷歌 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
* IE:User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko
*/
@RequestMapping(value="/handle04",headers={"User-Agent=Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"})
public String handle04(){
return "success";
}
3.2 Ant风格的资源路径
Ant 风格资源地址支持 3 种匹配符:
* *:能替代任意多个字符和一层路径
* **:能替代多层路径
@RequestMapping支持Ant风格的URL:
package com.test.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/*
* 模糊匹配功能
*
*
* URL地址可以写模糊的通配符
*
* ?:能替代任意一个字符
* *:能替代任意多个字符和一层路径
* **:能替代多层路径
*/
@Controller
public class RequestMappingTest {
@RequestMapping("/antTest01")
public String antTest01(){
System.out.println("antTest01");
return "success";
}
/*
* ?匹配一个字符,0个多个都不行;
* 模糊和精确多个匹配的情况下,精确优先
*/
@RequestMapping("/antTest0?")
public String antTest02(){
System.out.println("antTest02");
return "success";
}
/*
* 匹配任意多个字符
*/
@RequestMapping("/antTest0*")
public String antTest03(){
System.out.println("antTest03");
return "success";
}
/*
* 匹配一层路径
*/
@RequestMapping("/*/antTest0*")
public String antTest04(){
System.out.println("antTest04");
return "success";
}
/*
* 匹配多层路径
*/
@RequestMapping("/**/antTest0*")
public String antTest05(){
System.out.println("antTest05");
return "success";
}
}
四.@PathVariable映射URL绑定的占位符
带占位符的URL是Spring3.0新增的功能,该功能在SpringMVC向REST目标挺进发展过程中具有里程碑的意义。
通过@PathVariable可以将URL中占位符参数绑定到控制处理方法的入参中。
/*
* 路径上有占位符:占位符 语法就是在任意路径的地方写一个{变量名}
* 路径上的占位符只能占一层路径
*/
@RequestMapping("/user/{id}")
public String pathVariableTest(@PathVariable("id")String id){
System.out.println("路径上的占位符"+id);
return "success";
}
五.Rest
5.1 简介
REST:即 Representational State Transfer。(资源)表现层状态转化。是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用
- 资源(Resources):网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的存在。可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的 URI 。要获取这个资源,访问它的URI就可以,因此 URI 即为每一个资源的独一无二的识别符。
- 表现层(Representation):把资源具体呈现出来的形式,叫做它的表现层(Representation)。比如,文本可以用 txt 格式表现,也可以用 HTML 格式、XML 格式、JSON 格式表现,甚至可以采用二进制格式。
- 状态转化(State Transfer):每发出一个请求,就代表了客户端和服务器的一次交互过程。HTTP协议,是一个无状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是 “表现层状态转化”。具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。
Rest:系统希望以非常简洁的URL地址来发请求,怎样表示对一个资源的增删改查用请求方式来区分。简洁的URL提交请求,以请求方式区分对资源的操作。
我们之前的URL一般这样编写:
getOrder?id=1 GET
deleteOrder?id=1 GET
updateOrder POST
saveOrder POST
Rest推荐URL地址这样起名:/资源名/资源标识符:
/order/1 HTTP GET :得到 id = 1 的 order
/order/1 HTTP DELETE:删除 id = 1的 order
/order/1 HTTP PUT:更新id = 1的 order
/order HTTP POST:新增 order
5.2 Rest风格增删改查环境搭建
和之前创建SpringMVC项目一样,创建动态Web项目,配置web.xml和springmvc-servlet.xml。前面创建步骤基本一致。
创建index.jsp页面,在这个页面发出Rest风格的URL请求:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<!-- 发起图书的增删改查请求,使用Rest风格的URL地址:
请求URL 请求方式 表示含义
/book/1 GET: 查询1号图书
/book/1 DELETE: 删除图书
/book/1 OUT: 更新1号图书
/book POST: 添加1号图书
-->
<a href="book/1">查询图书</a><br/>
<form action="book" method="post">
<input type="submit" value="添加一号图书"/>
</form>
<a href="book/1">删除图书</a><br/>
<a href="book/1">更新图书</a><br/>
</body>
</html>
处理器:
package com.test.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class BookController {
@RequestMapping(value="/book",method=RequestMethod.POST)
public String addBook(){
System.out.println("添加了新的图书");
return "success";
}
/*
* 删除图书
*/
@RequestMapping(value="/book/{bid}",method=RequestMethod.DELETE)
public String deleteBook(@PathVariable("bid")Integer id){
System.out.println("删除了"+id+"号图书");
return "success";
}
/*
* 图书更新
*/
@RequestMapping(value="/book/{bid}",method=RequestMethod.PUT)
public String updateBook(@PathVariable("bid")Integer id){
System.out.println("更新了"+id+"号图书");
return "success";
}
/*
* 处理查询图书
*/
@RequestMapping(value="/book/{bid}",method=RequestMethod.GET)
public String getBook(@PathVariable("bid")Integer id){
System.out.println("查询到了"+id+"号图书");
return "success";
}
}
测试执行后,会发现删除和更新的请求并没有到达相对应的方法,而是去了GET对应的处理方法。这是因为:浏览器 form 表单只支持 GET 与 POST 请求,而DELETE、PUT 等 method 并不支持。
5.3 支持REST风格的过滤器:可以将POST请求转换为PUT或DELETE请求
这个步骤如下:
1)SpringMVC中有一个Filter,可以把普通的请求转化为规定形式的请求:在web.xml中配置filter(HiddenHttpMethodFilter)
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2)如何发起其他形式请求
(1)创建一个post类型的表单
(2)表单项中携带一个_method的参数
(3)这个_method的值就是DELETE或PUT
<!-- 发送DELETE请求 -->
<form action="book/1" method="post">
<input name="_method" value="delete">
<input type="submit" value="删除一号图书"/>
</form><br>
<!-- 发送PUT请求 -->
<form action="book/1" method="post">
<input name="_method" value="put">
<input type="submit" value="删除一号图书"/>
</form><br>
5.3.1 HiddenHttpMethodFilter源码解析
为什么请求隐含参数名称必须叫做”_method”?form表单的值又是如何转换成隐含参数_method的值的?源码解析如下:
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = DEFAULT_METHOD_PARAM;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//获取表单_method带来的值
String paramValue = request.getParameter(this.methodParam);
//如果表单是一个post,并且_method有值
if ("POST".equals(request.getMethod()) && StringUtils.hasLength(paramValue)) {
//转为大写PUT,DELETE
String method = paramValue.toUpperCase(Locale.ENGLISH);
//重写了request.getMethod();
HttpServletRequest wrapper = new HttpMethodRequestWrapper(request, method);
//wrapper.getMethod==PUT,DELETE
filterChain.doFilter(wrapper, response);
}
else {
//直接放行
filterChain.doFilter(request, response);
}
}
...
}