一. 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; @Overrideprotected 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,DELETEString method = paramValue.toUpperCase(Locale.ENGLISH);//重写了request.getMethod(); HttpServletRequest wrapper = new HttpMethodRequestWrapper(request, method);//wrapper.getMethod==PUT,DELETEfilterChain.doFilter(wrapper, response);}else {//直接放行filterChain.doFilter(request, response);}}
...
}