10.2 分组校验
由于校验规则都是定义在实体类上面的,但是,在不同的数据提交环境下,校验规则可能不一样。例如,用户的 id 是自增长的,添加的时候,可以不用传递用户 id,但是修改的时候则必须传递用户 id,这种情况下,就需要使用分组校验。
分组校验,首先需要定义校验组,所谓的校验组,其实就是空接口:
public interface ValidationGroup1 { } public interface ValidationGroup2 { } 复制代码
然后,在实体类中,指定每一个校验规则所属的组:
public class Student { @NotNull(message = "{student.id.notnull}",groups = ValidationGroup1.class) private Integer id; @NotNull(message = "{student.name.notnull}",groups = {ValidationGroup1.class, ValidationGroup2.class}) @Size(min = 2,max = 10,message = "{student.name.length}",groups = {ValidationGroup1.class, ValidationGroup2.class}) private String name; @Email(message = "{student.email.error}",groups = {ValidationGroup1.class, ValidationGroup2.class}) private String email; @Max(value = 150,message = "{student.age.error}",groups = {ValidationGroup2.class}) private Integer age; public String getEmail() { return email; } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", email='" + email + '\'' + ", age=" + age + '}'; } public void setEmail(String email) { this.email = email; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } 复制代码
在 group 中指定每一个校验规则所属的组,一个规则可以属于一个组,也可以属于多个组。
最后,在接收参数的地方,指定校验组:
@Controller public class StudentController { @RequestMapping("/addstudent") @ResponseBody public void addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) { if (result != null) { //校验未通过,获取所有的异常信息并展示出来 List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage()); } } } } 复制代码
配置完成后,属于 ValidationGroup2 这个组的校验规则,才会生效。
10.3 校验注解
校验注解,主要有如下几种:
- @Null 被注解的元素必须为 null
- @NotNull 被注解的元素必须不为 null
- @AssertTrue 被注解的元素必须为 true
- @AssertFalse 被注解的元素必须为 false
- @Min(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
- @Max(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
- @DecimalMin(value) 被注解的元素必须是一个数字,其值必须大于等于指定的最小值
- @DecimalMax(value) 被注解的元素必须是一个数字,其值必须小于等于指定的最大值
- @Size(max=, min=) 被注解的元素的大小必须在指定的范围内
- @Digits (integer, fraction) 被注解的元素必须是一个数字,其值必须在可接受的范围内
- @Past 被注解的元素必须是一个过去的日期
- @Future 被注解的元素必须是一个将来的日期
- @Pattern(regex=,flag=) 被注解的元素必须符合指定的正则表达式
- @NotBlank(message =) 验证字符串非 null,且长度必须大于0
- @Email 被注解的元素必须是电子邮箱地址
- @Length(min=,max=) 被注解的字符串的大小必须在指定的范围内
- @NotEmpty 被注解的字符串的必须非空
- @Range(min=,max=,message=) 被注解的元素必须在合适的范围内
11.1 数据回显基本用法
数据回显就是当用户数据提交失败时,自动填充好已经输入的数据。一般来说,如果使用 Ajax 来做数据提交,基本上是没有数据回显这个需求的,但是如果是通过表单做数据提交,那么数据回显就非常有必要了。
11.1.1 简单数据类型
简单数据类型,实际上框架在这里没有提供任何形式的支持,就是我们自己手动配置。我们继续在第 10 小节的例子上演示 Demo。加入提交的 Student 数据不符合要求,那么重新回到添加 Student 页面,并且预设之前已经填好的数据。
首先我们先来改造一下 student.jsp 页面:
<form action="/addstudent" method="post"> <table> <tr> <td>学生编号:</td> <td><input type="text" name="id" value="${id}"></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="name" value="${name}"></td> </tr> <tr> <td>学生邮箱:</td> <td><input type="text" name="email" value="${email}"></td> </tr> <tr> <td>学生年龄:</td> <td><input type="text" name="age" value="${age}"></td> </tr> <tr> <td colspan="2"> <input type="submit" value="提交"> </td> </tr> </table> </form> 复制代码
在接收数据时,使用简单数据类型去接收:
@RequestMapping("/addstudent") public String addStudent2(Integer id, String name, String email, Integer age, Model model) { model.addAttribute("id", id); model.addAttribute("name", name); model.addAttribute("email", email); model.addAttribute("age", age); return "student"; } 复制代码
这种方式,相当于框架没有做任何工作,就是我们手动做数据回显的。此时访问页面,服务端会再次定位到该页面,而且数据已经预填好。
11.1.2 实体类
上面这种简单数据类型的回显,实际上非常麻烦,因为需要开发者在服务端一个一个手动设置。如果使用对象的话,就没有这么麻烦了,因为 SpringMVC 在页面跳转时,会自动将对象填充进返回的数据中。
此时,首先修改一下 student.jsp 页面:
<form action="/addstudent" method="post"> <table> <tr> <td>学生编号:</td> <td><input type="text" name="id" value="${student.id}"></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="name" value="${student.name}"></td> </tr> <tr> <td>学生邮箱:</td> <td><input type="text" name="email" value="${student.email}"></td> </tr> <tr> <td>学生年龄:</td> <td><input type="text" name="age" value="${student.age}"></td> </tr> <tr> <td colspan="2"> <input type="submit" value="提交"> </td> </tr> </table> </form> 复制代码
注意,在预填数据中,多了一个 student. 前缀。这 student 就是服务端接收数据的变量名,服务端的变量名和这里的 student 要保持一直。服务端定义如下
@RequestMapping("/addstudent") public String addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) { if (result != null) { //校验未通过,获取所有的异常信息并展示出来 List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage()); } return "student"; } return "hello"; } 复制代码
注意,服务端什么都不用做,就说要返回的页面就行了,student 这个变量会被自动填充到返回的 Model 中。变量名就是填充时候的 key。如果想自定义这个 key,可以在参数中写出来 Model,然后手动加入 Student 对象,就像简单数据类型回显那样。
另一种定义回显变量别名的方式,就是使用 @ModelAttribute 注解。
11.2 @ModelAttribute
@ModelAttribute 这个注解,主要有两方面的功能:
- 在数据回显时,给变量定义别名
- 定义全局数据
11.2.1 定义别名
在数据回显时,给变量定义别名,非常容易,直接加这个注解即可:
@RequestMapping("/addstudent") public String addStudent(@ModelAttribute("s") @Validated(ValidationGroup2.class) Student student, BindingResult result) { if (result != null) { //校验未通过,获取所有的异常信息并展示出来 List<ObjectError> allErrors = result.getAllErrors(); for (ObjectError allError : allErrors) { System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage()); } return "student"; } return "hello"; } 复制代码
这样定义完成后,在前端再次访问回显的变量时,变量名称就不是 student 了,而是 s:
<form action="/addstudent" method="post"> <table> <tr> <td>学生编号:</td> <td><input type="text" name="id" value="${s.id}"></td> </tr> <tr> <td>学生姓名:</td> <td><input type="text" name="name" value="${s.name}"></td> </tr> <tr> <td>学生邮箱:</td> <td><input type="text" name="email" value="${s.email}"></td> </tr> <tr> <td>学生年龄:</td> <td><input type="text" name="age" value="${s.age}"></td> </tr> <tr> <td colspan="2"> <input type="submit" value="提交"> </td> </tr> </table> </form> 复制代码
11.2.2 定义全局数据
假设有一个 Controller 中有很多方法,每个方法都会返回数据给前端,但是每个方法返回给前端的数据又不太一样,虽然不太一样,但是没有方法的返回值又有一些公共的部分。可以将这些公共的部分提取出来单独封装成一个方法,用 @ModelAttribute 注解来标记。
例如在一个 Controller 中 ,添加如下代码:
@ModelAttribute("info") public Map<String,Object> info() { Map<String, Object> map = new HashMap<>(); map.put("username", "javaboy"); map.put("address", "www.javaboy.org"); return map; } 复制代码
当用户访问当前 Controller 中的任意一个方法,在返回数据时,都会将添加了 @ModelAttribute 注解的方法的返回值,一起返回给前端。@ModelAttribute 注解中的 info 表示返回数据的 key。
12.1 返回 JSON
目前主流的 JSON 处理工具主要有三种:
- jackson
- gson
- fastjson
在 SpringMVC 中,对 jackson 和 gson 都提供了相应的支持,就是如果使用这两个作为 JSON 转换器,只需要添加对应的依赖就可以了,返回的对象和返回的集合、Map 等都会自动转为 JSON,但是,如果使用 fastjson,除了添加相应的依赖之外,还需要自己手动配置 HttpMessageConverter 转换器。其实前两个也是使用 HttpMessageConverter 转换器,但是是 SpringMVC 自动提供的,SpringMVC 没有给 fastjson 提供相应的转换器。
12.1.1 jackson
jackson 是一个使用比较多,时间也比较长的 JSON 处理工具,在 SpringMVC 中使用 jackson ,只需要添加 jackson 的依赖即可:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.10.1</version> </dependency> 复制代码
依赖添加成功后,凡是在接口中直接返回的对象,集合等等,都会自动转为 JSON。如下:
public class Book { private Integer id; private String name; private String author; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } } @RequestMapping("/book") @ResponseBody public Book getBookById() { Book book = new Book(); book.setId(1); book.setName("三国演义"); book.setAuthor("罗贯中"); return book; } 复制代码
这里返回一个对象,但是在前端接收到的则是一个 JSON 字符串,这个对象会通过 HttpMessageConverter 自动转为 JSON 字符串。
如果想返回一个 JSON 数组,写法如下:
@RequestMapping("/books") @ResponseBody public List<Book> getAllBooks() { List<Book> list = new ArrayList<Book>(); for (int i = 0; i < 10; i++) { Book book = new Book(); book.setId(i); book.setName("三国演义:" + i); book.setAuthor("罗贯中:" + i); list.add(book); } return list; } 复制代码
添加了 jackson ,就能够自动返回 JSON,这个依赖于一个名为 HttpMessageConverter 的类,这本身是一个接口,从名字上就可以看出,它的作用是 Http 消息转换器,既然是消息转换器,它提供了两方面的功能:
- 将返回的对象转为 JSON
- 将前端提交上来的 JSON 转为对象
但是,HttpMessageConverter 只是一个接口,由各个 JSON 工具提供相应的实现,在 jackson 中,实现的名字叫做 MappingJackson2HttpMessageConverter,而这个东西的初始化,则由 SpringMVC 来完成。除非自己有一些自定义配置的需求,否则一般来说不需要自己提供 MappingJackson2HttpMessageConverter。
举一个简单的应用场景,例如每一本书,都有一个出版日期,修改 Book 类如下:
public class Book { private Integer id; private String name; private String author; private Date publish; public Date getPublish() { return publish; } public void setPublish(Date publish) { this.publish = publish; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } } 复制代码
然后在构造 Book 时添加日期属性:
@RequestMapping("/book") @ResponseBody public Book getBookById() { Book book = new Book(); book.setId(1); book.setName("三国演义"); book.setAuthor("罗贯中"); book.setPublish(new Date()); return book; } 复制代码
访问 /book 接口,返回的 json 格式如下:
如果我们想自己定制返回日期的格式,简单的办法,可以通过添加注解来实现:
public class Book { private Integer id; private String name; private String author; @JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai") private Date publish; 复制代码
注意这里一定要设置时区。
这样,就可以定制返回的日期格式了。
但是,这种方式有一个弊端,这个注解可以加在属性上,也可以加在类上,也就说,最大可以作用到一个类中的所有日期属性上。如果项目中有很多实体类都需要做日期格式化,使用这种方式就比较麻烦了,这个时候,我们可以自己提供一个 jackson 的 HttpMesageConverter 实例,在这个实例中,自己去配置相关属性,这里的配置将是一个全局配置。
在 SpringMVC 配置文件中,添加如下配置:
<mvc:annotation-driven> <mvc:message-converters> <ref bean="httpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" id="httpMessageConverter"> <property name="objectMapper"> <bean class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="dateFormat"> <bean class="java.text.SimpleDateFormat"> <constructor-arg name="pattern" value="yyyy-MM-dd HH:mm:ss"/> </bean> </property> <property name="timeZone" value="Asia/Shanghai"/> </bean> </property> </bean> 复制代码
添加完成后,去掉 Book 实体类中日期格式化的注解,再进行测试,结果如下:
12.1.2 gson
gson 是 Google 推出的一个 JSON 解析器,主要在 Android 开发中使用较多,不过,Web 开发中也是支持这个的,而且 SpringMVC 还针对 Gson 提供了相关的自动化配置,以致我们在项目中只要添加 gson 依赖,就可以直接使用 gson 来做 JSON 解析了。
<dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.6</version> </dependency> 复制代码
如果项目中,同时存在 jackson 和 gson 的话,那么默认使用的是 jackson,为社么呢?在 org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter 类的构造方法中,加载顺序就是先加载 jackson 的 HttpMessageConverter,后加载 gson 的 HttpMessageConverter。
加完依赖之后,就可以直接返回 JSON 字符串了。使用 Gson 时,如果想做自定义配置,则需要自定义 HttpMessageConverter。
<mvc:annotation-driven> <mvc:message-converters> <ref bean="httpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven> <bean class="org.springframework.http.converter.json.GsonHttpMessageConverter" id="httpMessageConverter"> <property name="gson"> <bean class="com.google.gson.Gson" factory-bean="gsonBuilder" factory-method="create"/> </property> </bean> <bean class="com.google.gson.GsonBuilder" id="gsonBuilder"> <property name="dateFormat" value="yyyy-MM-dd"/> </bean> 复制代码
12.1.3 fastjson
fastjson 号称最快的 JSON 解析器,但是也是这三个中 BUG 最多的一个。在 SpringMVC 并没针对 fastjson 提供相应的 HttpMessageConverter,所以,fastjson 在使用时,一定要自己手动配置 HttpMessageConverter(前面两个如果没有特殊需要,直接添加依赖就可以了)。
使用 fastjson,我们首先添加 fastjson 依赖:
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.60</version> </dependency> 复制代码
然后在 SpringMVC 的配置文件中配置 HttpMessageConverter:
<mvc:annotation-driven> <mvc:message-converters> <ref bean="httpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven> <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter"> <property name="fastJsonConfig"> <bean class="com.alibaba.fastjson.support.config.FastJsonConfig"> <property name="dateFormat" value="yyyy-MM-dd"/> </bean> </property> </bean> 复制代码
fastjson 默认中文乱码,添加如下配置解决:
<mvc:annotation-driven> <mvc:message-converters> <ref bean="httpMessageConverter"/> </mvc:message-converters> </mvc:annotation-driven> <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter"> <property name="fastJsonConfig"> <bean class="com.alibaba.fastjson.support.config.FastJsonConfig"> <property name="dateFormat" value="yyyy-MM-dd"/> </bean> </property> <property name="supportedMediaTypes"> <list> <value>application/json;charset=utf-8</value> </list> </property> </bean> 复制代码
12.2 接收 JSON
浏览器传来的参数,可以是 key/value 形式的,也可以是一个 JSON 字符串。在 Jsp/Servlet 中,我们接收 key/value 形式的参数,一般是通过 getParameter 方法。如果客户端商户惨的是 JSON 数据,我们可以通过如下格式进行解析:
@RequestMapping("/addbook2") @ResponseBody public void addBook2(HttpServletRequest req) throws IOException { ObjectMapper om = new ObjectMapper(); Book book = om.readValue(req.getInputStream(), Book.class); System.out.println(book); } 复制代码
但是这种解析方式有点麻烦,在 SpringMVC 中,我们可以通过一个注解来快速的将一个 JSON 字符串转为一个对象:
@RequestMapping("/addbook3") @ResponseBody public void addBook3(@RequestBody Book book) { System.out.println(book); } 复制代码
这样就可以直接收到前端传来的 JSON 字符串了。这也是 HttpMessageConverter 提供的第二个功能。
13. RESTful
本小节选自外部博客,原文链接:www.ruanyifeng.com/blog/2011/0…
越来越多的人开始意识到,网站即软件,而且是一种新型的软件。这种"互联网软件"采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency)、高并发等特点。网站开发,完全可以采用软件开发的模式。但是传统上,软件和网络是两个不同的领域,很少有交集;软件开发主要针对单机环境,网络则主要研究系统之间的通信。互联网的兴起,使得这两个领域开始融合,现在我们必须考虑,如何开发在互联网环境中使用的软件。
RESTful 架构,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。
但是,到底什么是 RESTful 架构,并不是一个容易说清楚的问题。下面,我就谈谈我理解的 RESTful 架构。、
RESTful 它不是一个具体的架构,不是一个软件,不是一个框架,而是一种规范。在移动互联网兴起之前,我们都很少提及 RESTful,主要是因为用的少,移动互联网兴起后,RESTful 得到了非常广泛的应用,因为在移动互联网兴起之后,我们再开发后端应用,就不仅仅只是开发一个网站了,还对应了多个前端(Android、iOS、HTML5 等等),这个时候,我们在设计后端接口是,就需要考虑接口的形式,格式,参数的传递等等诸多问题了。
13.1 起源
REST 这个词,是 Roy Thomas Fielding 在他 2000 年的博士论文中提出的。
Fielding 是一个非常重要的人,他是 HTTP 协议(1.0版和1.1版)的主要设计者、Apache 服务器软件的作者之一、Apache 基金会的第一任主席。所以,他的这篇论文一经发表,就引起了关注,并且立即对互联网开发产生了深远的影响。
他这样介绍论文的写作目的:
"本文研究计算机科学两大前沿----软件和网络----的交叉点。长期以来,软件研究主要关注软件设计的分类、设计方法的演化,很少客观地评估不同的设计选择对系统行为的影响。而相反地,网络研究主要关注系统之间通信行为的细节、如何改进特定通信机制的表现,常常忽视了一个事实,那就是改变应用程序的互动风格比改变互动协议,对整体表现有更大的影响。我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。"
13.2 名称
Fielding 将他对互联网软件的架构原则,定名为REST,即 Representational State Transfer 的缩写。我对这个词组的翻译是"表现层状态转化"。
如果一个架构符合 REST 原则,就称它为 RESTful 架构。
要理解 RESTful 架构,最好的方法就是去理解 Representational State Transfer 这个词组到底是什么意思,它的每一个词代表了什么涵义。如果你把这个名称搞懂了,也就不难体会 REST 是一种什么样的设计。
13.3 资源(Resources)
REST 的名称"表现层状态转化"中,省略了主语。"表现层"其实指的是"资源"(Resources)的"表现层"。
所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个 URI (统一资源定位符)指向它,每种资源对应一个特定的 URI。要获取这个资源,访问它的 URI 就可以,因此 URI 就成了每一个资源的地址或独一无二的识别符。
所谓"上网",就是与互联网上一系列的"资源"互动,调用它的 URI。
在 RESTful 风格的应用中,每一个 URI 都代表了一个资源。
13.4 表现层(Representation)
"资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层"(Representation)。
比如,文本可以用 txt 格式表现,也可以用 HTML 格式、XML 格式、JSON 格式表现,甚至可以采用二进制格式;图片可以用 JPG 格式表现,也可以用 PNG 格式表现。
URI 只代表资源的实体,不代表它的形式。严格地说,有些网址最后的 ".html" 后缀名是不必要的,因为这个后缀名表示格式,属于 "表现层" 范畴,而 URI 应该只代表"资源"的位置。它的具体表现形式,应该在 HTTP 请求的头信息中用 Accept 和 Content-Type 字段指定,这两个字段才是对"表现层"的描述。
13.5 状态转化(State Transfer)
访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。
互联网通信协议 HTTP 协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。而这种转化是建立在表现层之上的,所以就是"表现层状态转化"。
客户端用到的手段,只能是 HTTP 协议。具体来说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:
- GET 用来获取资源
- POST 用来新建资源(也可以用于更新资源)
- PUT 用来更新资源
- DELETE 用来删除资源
13.6 综述
综合上面的解释,我们总结一下什么是 RESTful 架构:
- 每一个 URI 代表一种资源;
- 客户端和服务器之间,传递这种资源的某种表现层;
- 客户端通过四个 HTTP 动词,对服务器端资源进行操作,实现"表现层状态转化"。
13.7 误区
RESTful 架构有一些典型的设计误区。
最常见的一种设计错误,就是 URI 包含动词。因为"资源"表示一种实体,所以应该是名词,URI 不应该有动词,动词应该放在 HTTP 协议中。
举例来说,某个 URI 是 /posts/show/1,其中 show 是动词,这个 URI 就设计错了,正确的写法应该是 /posts/1,然后用 GET 方法表示 show。
如果某些动作是HTTP动词表示不了的,你就应该把动作做成一种资源。比如网上汇款,从账户 1 向账户 2 汇款 500 元,错误的 URI 是:
- POST /accounts/1/transfer/500/to/2
正确的写法是把动词 transfer 改成名词 transaction,资源不能是动词,但是可以是一种服务:
POST /transaction HTTP/1.1 Host: 127.0.0.1 from=1&to=2&amount=500.00 复制代码
另一个设计误区,就是在URI中加入版本号:
因为不同的版本,可以理解成同一种资源的不同表现形式,所以应该采用同一个 URI。版本号可以在 HTTP 请求头信息的 Accept 字段中进行区分(参见 Versioning REST Services):
Accept: vnd.example-com.foo+json; version=1.0 Accept: vnd.example-com.foo+json; version=1.1 Accept: vnd.example-com.foo+json; version=2.0 复制代码
13.8 SpringMVC 的支持
SpringMVC 对 RESTful 提供了非常全面的支持,主要有如下几个注解:
- @RestController
这个注解是一个组合注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController { /** * The value may indicate a suggestion for a logical component name, * to be turned into a Spring bean in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise) * @since 4.0.1 */ @AliasFor(annotation = Controller.class) String value() default ""; } 复制代码
一般,直接用 @RestController 来标记 Controller,可以不使用 @Controller。
请求方法中,提供了常见的请求方法:
- @PostMapping
- @GetMapping
- @PutMapping
- @DeleteMapping
另外还有一个提取请求地址中的参数的注解 @PathVariable:
@GetMapping("/book/{id}")//http://localhost:8080/book/2 public Book getBookById(@PathVariable Integer id) { Book book = new Book(); book.setId(id); return book; } 复制代码
参数 2 将被传递到 id 这个变量上。
14. 静态资源访问
在 SpringMVC 中,静态资源,默认都是被拦截的,例如 html、js、css、jpg、png、txt、pdf 等等,都是无法直接访问的。因为所有请求都被拦截了,所以,针对静态资源,我们要做额外处理,处理方式很简单,直接在 SpringMVC 的配置文件中,添加如下内容:
<mvc:resources mapping="/static/html/**" location="/static/html/"/> 复制代码
mapping 表示映射规则,也是拦截规则,就是说,如果请求地址是 /static/html 这样的格式的话,那么对应的资源就去 /static/html/ 这个目录下查找。
在映射路径的定义中,最后是两个 *,这是一种 Ant 风格的路径匹配符号,一共有三个通配符:
一个比较原始的配置方式可能如下:
<mvc:resources mapping="/static/html/**" location="/static/html/"/> <mvc:resources mapping="/static/js/**" location="/static/js/"/> <mvc:resources mapping="/static/css/**" location="/static/css/"/> 复制代码
但是,由于 ** 可以表示多级路径,所以,以上配置,我们可以进行简化:
<mvc:resources mapping="/**" location="/"/> 复制代码
15. 拦截器
SpringMVC 中的拦截器,相当于 Jsp/Servlet 中的过滤器,只不过拦截器的功能更为强大。
拦截器的定义非常容易:
@Component public class MyInterceptor1 implements HandlerInterceptor { /** * 这个是请求预处理的方法,只有当这个方法返回值为 true 的时候,后面的方法才会执行 * @param request * @param response * @param handler * @return * @throws Exception */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("MyInterceptor1:preHandle"); return true; } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("MyInterceptor1:postHandle"); } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("MyInterceptor1:afterCompletion"); } } @Component public class MyInterceptor2 implements HandlerInterceptor { /** * 这个是请求预处理的方法,只有当这个方法返回值为 true 的时候,后面的方法才会执行 * @param request * @param response * @param handler * @return * @throws Exception */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("MyInterceptor2:preHandle"); return true; } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("MyInterceptor2:postHandle"); } public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("MyInterceptor2:afterCompletion"); } } 复制代码
拦截器定义好之后,需要在 SpringMVC 的配置文件中进行配置:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <ref bean="myInterceptor1"/> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/**"/> <ref bean="myInterceptor2"/> </mvc:interceptor> </mvc:interceptors> 复制代码
如果存在多个拦截器,拦截规则如下:
- preHandle 按拦截器定义顺序调用
- postHandler 按拦截器定义逆序调用
- afterCompletion 按拦截器定义逆序调用
- postHandler 在拦截器链内所有拦截器返成功调用
- afterCompletion 只有 preHandle 返回 true 才调用