一、Restful请求格式
1. 介绍
Rest(Representational State Transfer:表现层状态转移)是一种软件架构风格,其核心是面向资源的一种设计。何为面向资源,意思是网络上的所有事物都可以抽象为资源,而每个资源都有唯一的资源标识,对资源的操作不应该改变这些标识。
通俗讲就是每个资源都有一个url地址,而不是不同的操作有不同的url地址,比如我们对用户信息的增删改查,用户就是资源,增删改查是操作,以前我们是一个操作一个url地址,现在按照Restful的说法,url地址只能有一个。
Restful的出现同时也解决了客户端的种类多种多样造成请求的格式比较混乱的问题,Restful提供了一种统一的前后端交互的接口规范,可以更好的实现数据的交互。
2. 正常使用
以前我们来实现对用户的增删该查的时候是以操作为基础来声明URL地址的:
新增用户: http://localhost:8080/userAdd?uid=1&uname=zhangsan&age=12
修改用户: http://localhost:8080/userUpdate?uid=1&uname=zhangsan
删除用户: http://localhost:8080/userDelete?uid=1
查询用户:http://localhost:8080/userSel?uid=1
而按照Restful的格式对用户的操作应当只有一个url地址:
操作用户: http://localhost:8080/user
Restful要求在当前的url地址中直接嵌套请求数据。
新增用户: http://localhost:8080/user/1/zhangsan/12
修改用户: http://localhost:8080/user/1/zhangsan/28
删除用户: http://localhost:8080/user/1
查询用户: http://localhost:8080/user/1
但请求数据被嵌套在了请求地址中如何获取呢?不能在像以前直接在单元方法上声明形参来接收了,需要结合@PathVariable注解来获取。
/*** @RequestMapping注解可以接收任意请求方式的请求* @GetMapping("地址"):接收GET请求,一般用在查询方法上* @DeleteMapping("地址"):接收DELETE请求,一般用在删除方法上* @PostMapping("地址"):接收POST请求,一般用户在新增上* @PutMapping("地址"):接收PUT请求,一般用在修改上*/
//查询用户信息
@GetMapping("/user/{id}")
public String selUser(@PathVariable Integer id){System.out.println("用户ID为:"+id);return "success.jsp";
}
//删除用户信息
@DeleteMapping("/user/{id}")
public String delUser(@PathVariable Integer id){System.out.println("用户ID为:"+id);return "success.jsp";
}
//新增用户信息
@PostMapping("/user/{id}/{name}/{age}")
public String addUser(@PathVariable Integer id,@PathVariable String name,@PathVariable Integer age){System.out.println("id = " + id + ", name = " + name + ", age = " + age);return "success.jsp";
}
//修改用户信息
@PutMapping("/user/{id}/{name}")
public String updateUser(@PathVariable Integer id,@PathVariable String name){System.out.println("id = " + id + ", name = " + name);return "success.jsp";
}
3. 使用Restful显示页面
我们知道,为了提高安全性,可以把页面放入到WEB-INF中。但是放入到WEB-INF中之后,访问页面之前必须先执行控制器,可以使用Restful方式显示页面,这样可以大大减少显示页面的控制器数量。
@Controller
@RequestMapping("page")
public class PageController {@GetMapping("{pageName}")public String showPage(@PathVariable String pageName){return "/WEB-INF/" + pageName + ".jsp";}
}
二、@ResponseBody注解
1. @ResponseBody介绍
@ResponseBody注解是类或方法级注解。
当方法上添加@ResponseBody注解后,控制单元方法返回值将不再被视图解析器进行解析|不会使用转发。而是把返回值放入到响应流中进行响应。
2. 最简单使用
直接在方法上添加上@ResponseBody,Spring MVC会把返回值设置到响应流中。
package com.sh.controller;import com.sh.pojo.Emp;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
/*
* @ResponseBody : 控制单元添加了该注解 , 不会执行视图解析器, 将控制单元的返回值直接响应会到客户端
* 要求:
* 默认:
* 1.控制单元只能返回String类型的数据.返回其他数据类型出现406状态码
* 2.配合@RequestMapping(produces = "text/plain;charset=utf-8")设置响应内容类型及编码格式。
*
* */
//交给SpringMVC
@Controller
public class EmpController {/* 走视图解析器 */@RequestMapping("a1")public void a1(HttpServletResponse response, HttpServletRequest req){//什么都不做}@RequestMapping("a2")public String a2(HttpServletResponse response, HttpServletRequest req){return "index";}@RequestMapping("a3")public String a3(HttpServletResponse response, HttpServletRequest req) throws IOException {response.getWriter().print("ok");return "index";}@RequestMapping("a4")public void a4(HttpServletResponse response, HttpServletRequest req) throws IOException {response.setContentType("text/plain;charset=utf-8");Emp emp = new Emp(1, "zs", "bj", new Date());//自动调用了toString()方法response.getWriter().print(emp);}/* 添加@ResponseBody 默认只能返回String类型,其他类型返回406状态码 */@RequestMapping("a5")@ResponseBodypublic Emp a5(HttpServletResponse response, HttpServletRequest req) throws IOException {Emp emp = new Emp(1, "zs", "bj", new Date());return emp;}@RequestMapping("a6")@ResponseBodypublic String a6(HttpServletResponse response, HttpServletRequest req) throws IOException {Emp emp = new Emp(1, "zs", "bj", new Date());String s = emp.toString();return s;}@RequestMapping(value = "a7",produces = "text/plain;charset=utf-8")@ResponseBodypublic String a7(HttpServletResponse response, HttpServletRequest req) throws IOException {//返回字符串中文时会出现乱码,需要配合@RequestMapping(produces = "text/plain;charset=utf-8")设置响应内容类型及编码格式return "你好";}}
3. 自动转换为JSON字符串
@ResponseBody注解可以把控制单元返回值自动转换为JSON字符串。主要完成下面几个事情:
(1)判断返回值是否为JavaBean、JavaBean数组、List<JavaBean类型>、Map等满足键值对的类型。
(2)如果满足键值对类型,会使用Jackson把对象转换为JSON字符串,设置到响应流中。
同时会设置响应内容类型(Content-Type)为application/json;charset=utf-8
因为Spring MVC默认使用Jackson作为JSON转换工具,所以必须保证项目中存在Jackson的依赖。
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.10.8</version>
</dependency>
@ResponseBody : 控制单元添加了该注解 , 不会执行视图解析器, 将控制单元的返回值直接响应会到客户端要求:默认:1.控制单元只能返回String类型的数据.返回其他数据类型出现406状态码2.配合@RequestMapping(produces = "text/plain;charset=utf-8")设置响应内容类型及编码格式。导入json依赖之后:1.控制单元可以返回,JavaBean,数据[元素为JavaBean],集合{元素为JavaBean},Map2.SpringMVC默认使用jack将控制单元的返回值变为json格式的字符串,设置响应内容类型为application/json;charset=utf-8@RequestBody:将客户端请求参数为 json ,xml 转换为 javabean。需要引入相关依赖。
//交给SpringMVC
@Controller
public class EmpController {/* 走视图解析器 */@RequestMapping("a1")public void a1(HttpServletResponse response, HttpServletRequest req){//什么都不做}@RequestMapping("a2")public String a2(HttpServletResponse response, HttpServletRequest req){return "index";}@RequestMapping("a3")public String a3(HttpServletResponse response, HttpServletRequest req) throws IOException {response.getWriter().print("ok");return "index";}@RequestMapping("a4")public void a4(HttpServletResponse response, HttpServletRequest req) throws IOException {response.setContentType("text/plain;charset=utf-8");Emp emp = new Emp(1, "zs", "bj", new Date());//自动调用了toString()方法response.getWriter().print(emp);}/* 添加@ResponseBody 默认只能返回String类型,其他类型返回406状态码 */@RequestMapping("a5")@ResponseBodypublic Emp a5(HttpServletResponse response, HttpServletRequest req) throws IOException {Emp emp = new Emp(1, "zs", "bj", new Date());return emp;}@RequestMapping("a6")@ResponseBodypublic String a6(HttpServletResponse response, HttpServletRequest req) throws IOException {Emp emp = new Emp(1, "zs", "bj", new Date());String s = emp.toString();return s;}@RequestMapping(value = "a7",produces = "text/plain;charset=utf-8")@ResponseBodypublic String a7(HttpServletResponse response, HttpServletRequest req) throws IOException {//返回字符串中文时会出现乱码,需要配合@RequestMapping(produces = "text/plain;charset=utf-8")设置响应内容类型及编码格式return "你好";}@RequestMapping("a8")@ResponseBodypublic Emp a8(HttpServletResponse response, HttpServletRequest req) throws IOException {Emp emp = new Emp(1, "zs", "bj", new Date());return emp;//{"id":1,"uname":"zs","addr":"bj","bir":1698236241717}}@RequestMapping(value = "a11")@ResponseBody //将符合要求的内容转换为json,必须引入json工具类。public List<Emp> a11() throws IOException {Emp people = new Emp(1, "张三", "北京", new Date());Emp people1 = new Emp(2, "张三1", "北京", new Date());Emp people2 = new Emp(3, "张三2", "北京", new Date());ArrayList<Emp> list = new ArrayList<>();Collections.addAll(list, people, people1, people2);return list;}@RequestMapping(value = "a12")@ResponseBody //将符合要求的内容转换为json,必须引入json工具类。public Emp[] a12() throws IOException {Emp people = new Emp(1, "张三", "北京", new Date());Emp people1 = new Emp(2, "张三1", "北京", new Date());Emp people2 = new Emp(3, "张三2", "北京", new Date());Emp[] people3 = {people, people1, people2};return people3;}@RequestMapping(value = "a13")@ResponseBody //将符合要求的内容转换为json,必须引入json工具类。public Map<String, Object> a13() throws IOException {Map<String, Object> map = new HashMap<>();//使用map集合代替实体类map.put("id", 1);map.put("name", "张三");map.put("addr", "北京");map.put("bir", new Date());return map;}}
5. 转换为XML文件
XML格式在一些开放平台上用的比较多。例如:微信里面很多接口都是XML格式。
在Spring MVC中支持把返回值转换为XML文件。如果还是使用jackson-databind依赖,默认只能转换返回值为类类型的控制单元,返回值为List是无法转换为XML的,同时还要求实体类上必须有@XmlRootElement,才能转换。
如果项目中所有控制单元返回值结果都希望是XML格式,可以按照下面步骤完成。
5.1 导入依赖
导入依赖时注意:
(1)不要导入jackson-databind,只导入jackson-dataformat-xml。
(2)jackson-dataformat-xml版本不要太高,和Tomcat8插件不兼容。2.9.9和Spring 5.3.x可以正确兼容。
(3)因为上面练习导入的是jackson-databind,所以需要点击Maven面板 -> Lifecycle -> Clean 清空下缓存。
<dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId><version>2.9.9</version>
</dependency>
5.2 编写控制单元
控制单元方法和转换为JSON时写法完全相同。
6. @RestController注解
对于页面中使用前端框架时的项目。例如页面时通过:EasyUI、BootStrap、Vue等前端框架进行编写时,客户端向服务端发送的请求都是异步Ajax(或类似Ajax的异步请求)。对于这样的项目,控制器中所有的方法都包含@ResponseBody注解。
补充知识
实际开发中一般响应结果会创建一个类来接收 例如创建一个Result类
package com.sh.pojo;import java.io.Serializable;public class Result<T> implements Serializable {private String msg; //消息private int code; //自定义的状态码 200 成功 500 失败private T data; //数据public Result() {}public Result(String msg, int code, T data) {this.msg = msg;this.code = code;this.data = data;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public int getCode() {return code;}public void setCode(int code) {this.code = code;}public T getData() {return data;}public void setData(T data) {this.data = data;}@Overridepublic String toString() {return "Result{" +"msg='" + msg + '\'' +", code=" + code +", data=" + data +'}';}
}
三、@RequestBody注解
1. 介绍
@RequestBody注解底层依赖的依然是Jackson工具包,其作用是把客户端传递过来的请求体中JSON或XML数据转换为Map、类、List<类>、List<Map>等类型。
既然是转换为请求体数据,所以不能是GET类型请求(GET没有请求体),多用在POST类型的请求中。
@RequestBody注解在单体架构项目使用的不是特别多。主要用在分布式项目中多个项目之间传递数据或一些开发平台中(例如微信开发平台接口返回XML数据)
如果希望在单体架构项目中使用@RequestBody注解,需要在客户端中使用Ajax请求,刻意设置请求的内容类型(Content-Type)为JSON或XML。
2. 请求内容类型详解
在客户端中无论使用的是<form>
表单,还是Ajax请求,post请求内容类型都是application/x-www-form-urlencoded,表示普通表单参数。普通表单参数接收方式和上次课讲解的参数接收方式是相同的。因为是默认请求内容类型,所以在谷歌浏览器开发者工具中有时不会特意的显示,有时会显示。
2.1 表单参数接收
普通表单写法:
<form action="/testContentType" method="post">编号:<input type="text" name="id"/><br/>姓名:<input type="text" name="name"/><br/><input type="submit" value="提交"/>
</form>
谷歌开发者工具中可以看到Content-Type为application/x-www-form-urlencoded。
对于普通表单参数,使用同名参数或JavaBean接收都可以。
@Controller
public class Demo2Controller {// 使用多个简单数据类型接收请求参数@RequestMapping("/testContentType")public String testContentType(int id, String name) {System.out.println(id + "," + name);return "/index.jsp";}
// 使用JavaBean接收请求参数@RequestMapping("/testContentType")public String testContentType2(People peo){System.out.println(peo);return "/index.jsp";}
}
2.2 Ajax请求参数
使用Ajax请求时,默认的参数类型也是普通表单参数(Form Data)。
$.ajax({url:"/testContentTypeAjax",data:{"id":1,"name":"张三"},type:"post",success:function (data) {console.log(data);},dataType:"json"
});
3. 修改请求内容类型
如果希望修改请求内容类型,可以使用HTML的<form>
中enctype属性或使用Ajax中contentType属性进行设置。
注意:<form>
的enctype属性一般只有在文件上传时才会修改,所以希望传递特定类型请求参数内容时,都是通过Ajax进行请求。
下面演示下,请求参数内容为JSON字符串的写法。
在下面代码中有三次需要重点注意的地方:
(1)contentType:必须设置。常见取值“application/json”或"application/xml"。如果没有设置这个属性,取值默认是application/x-www-form-urlencoded,表示普通表单参数。当设置为"application/json"时,会把data取值设置到请求体中,所以服务端接收参数时就不能按照普通表单参数进行接收。
(2)data:请求参数。必须是JSON字符串类型,不能是JSON格式的对象。因为在JSON中key两次必须有双引号,所以data取值两侧用单引号包含。因为在JavaScript中字符串string类型可以使用单引号包含,也可以使用双引号包含。
(3)type:请求类型不能是get类型,因为get类型没有请求体。常用就是post类型。
$.ajax({url:"testContentType",contentType:"application/json",// 修改请求内容类型为JSONdata:'{"id":1,"name":"张三"}',// 取值两次必须有单引号,没有单引号无效type:"post",// 不能是GET类型请求success:function (data) {console.log(data);},dataType:"json"
});
服务端接收请求体中包含JSON字符串的请求时,需要在参数前面添加@RequestBody。表示使用Jackson把请求体中JSON/XML格式数据转换为JavaBean或Map。
小提示:
-
因为一个请求只有一个请求体。控制单元参数中绝对不允许出现两个@RequestBody注解。
-
因为@RequestBody底层使用Jackson,所以只适用于把请求体数据转换为JavaBean或Map。绝对不能在@RequestBody后面使用String等类型接收请求体内容。也就是说,客户端把JSON或XML设置到请求体,服务端使用JavaBean或Map接收请求体数据时,才能在控制单元参数前面添加@RequestBody注解。
四、Spring MVC文件上传
1. 文件上传介绍
文件上传就是把客户端的文件上传到服务端进行保存。在文件上传时文件和其他请求参数是在请求体中进行传递。所以不支持GET类型请求。
默认的表单内容类型application/x-www-form-urlencoded不支持传递文件流。所以需要在<form>
的enctype中设置enctype="multipart/form-data"才表示把文件和其他表单参数设置到请求体中。
Spring MVC的文件上传是通过MultipartResovler组件实现的。提供了两个具体的实现类
必须在Spring MVC的配置文件中配置CommonsMultipartResovler组件的Bean,同时也得在项目中导入Commons-Fileupload的依赖。
(1)客户端:
请求方式必须是POST
enctype必须为multipart/form-data
(2)服务端:
必须配置MultipartResovler。否则无法解析上传文件的流数据。(<bean>
的id值必须叫做multipartResovler)如果没有配置MultipartResovler不仅仅是文件流数据无法解析,连带着其他表单域数据也无法解析。因为文件流数据和表单数据都在请求体中,不解析的话,文件流数据和表单数据都接收不到。
注意文件域的name取值,文件域必须MultipartFile类型接收。且name的取值必须和MultipartFile对象名相同。
2. 文件上传实现流程
2.1 导入依赖
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version>
</dependency>
2.2 在页面中编写文件上传代码
要设置method="post" enctype="multipart/form-data"
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body><form action="/upload" method="post" enctype="multipart/form-data">姓名:<input type="text" name="name"/><br/>头像:<input type="file" name="photo"/><br/>地址:<input type="text" name="address"/><br/><input type="submit" value="提交"/><br/></form>
</body>
</html>
2.3 配置上传解析器bean
<!-- 文件上传时,必须配置文件解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>
2.4 编写单元方法处理上传请求
我们直接在单元方法上声明形参来接收请求数据即可,普通表单数据还是直接使用键名获取即可,上传的时候解析后会被存储到MultipartFile对象中,我们声明MultipartFile类型的形参接即可,但是形参名必须和file标签的name属性值一致。然后我们在单元方法中将接收到的上传资源通过流存储到服务器的硬盘中即可。
小提示:
如果客户端就一个文件域使用一个MultipartFile对象接收就可以了。
如果客户端是多个同名的文件域使用MultipartFile 数组接收。
如果客户端是多个不同名的文件域使用多个MultipartFile对象接收就可以了。
@Controller
public class PeopleController {/*** 文件上传控制单元方法实现** @param name 也可以使用JavaBean接收name的值* @param address 也可以使用JavaBean接收address的值* @param photo 名字必须和表单中文件域的name属性值相同* @return* @throws IOException transferTo抛出的异常,可以使用try...catch处理异常。示例中为了让代码看起来简洁直接抛出了。*/@RequestMapping("/upload")public String upload(String name, String address, MultipartFile photo) throws IOException {photo.transferTo(new File("D:/images", photo.getOriginalFilename()));return "/upload.jsp";}
}
3. 生成唯一文件名
在上面代码中,保存文件名称时是使用文件上传时的名称进行保存。这样做存在一个问题:如果存在同名文件,后上传文件会覆盖之前文件内容。
所以在文件上传时都会生成一个全局唯一的文件名。常见有两种方式:
(1)时间戳+随机数
(2)UUID
//时间戳long l = System.currentTimeMillis();System.out.println(l);//UUIDUUID uuid = UUID.randomUUID();String s = uuid.toString().replace("-","");System.out.println(s);
文件名是全局唯一的,但是保存时文件扩展名要和上传文件的扩展名保持一致。
@RequestMapping("/upload")public String upload(String name, String address, MultipartFile photo) throws IOException {// 判断上传文件流是否为空。如果不为空继续执行if(!photo.isEmpty()) {// 使用UUID生成文件名称// String fileName = UUID.randomUUID().toString();// 使用时间戳+随机数生成文件名long timeMillis = System.currentTimeMillis();Random random = new Random();String fileName = timeMillis + "" + random.nextInt(1000);// 获取上传时文件名String oldName = photo.getOriginalFilename();// 获取上传时文件的扩展名String suffix = oldName.substring(oldName.lastIndexOf("."));// 获取到当前项目images目录,发布到Tomcat后的绝对路径。String realPath = request.getServletContext().getRealPath("/images");System.out.println(realPath);// 保存到当前项目的images目录中。photo.transferTo(new File(realPath,fileName + suffix));}return "/upload.jsp";}
5. 限制上传文件大小
在很多项目中是对上传文件做严格大小限制的。当文件太大会占用服务器存储空间。当文件太小(尤其是图片)可能显示不清晰。
在CommonsMultipartResolver中提供了setmaxUploadSize(long)方法,表示设置上传文件的大小。单位是字节byte。默认值为-1,表示无限制。
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><property name="maxUploadSize" value="1024"></property>
</bean>
五、Spring MVC文件下载
1. 文件下载介绍
文件下载就是把服务器中的资源下载到本地。
但是需要注意的是浏览器本身作为一款软件,能够打开的文件格式比较多。
例如:.html文件、图片文件、.txt文件、.xml文件、.json文件等。当超链接访问的是浏览器本身能打开的资源。浏览器直接打开。这个特点就是响应头参数Content-Disposition控制的,其默认值为inline,表示能打开就打开,不能打开就下载。
Content-Disposition可取值有两个:
(1)inline。直接在浏览器中打开(能打开就打开,不能打开就下载)。
(2)attachment。以附件形式下载。
2. 测试inline效果
因为Content-Disposition默认值就是inline。所以不需要特殊设置。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body><a href="/images/a.png">a.png</a><a href="/images/b.json">b.json</a><a href="/images/c.rar">c.rar</a>
</body>
</html>
3. 测试attachment效果
如果希望所有的文件都是下载,而不是能打开则打开。可以在响应头中设置Content-Disposition参数为attachment。attachment结合filename可以设置下载文件的名称。
@RequestMapping("/download")
public void download(HttpServletRequest req, HttpServletResponse response, String filename) {try {// filename=的值就是客户端看到的下载文件名称response.setHeader("Content-Disposition", "attachment;filename=" + filename);File file = new File(req.getServletContext().getRealPath("/images"), filename);FileInputStream fis = new FileInputStream(file);ServletOutputStream os = response.getOutputStream();IOUtils.copy(fis, os);} catch (IOException e) {e.printStackTrace();}
}
4. 文件下载中包含中文名称解决办法
如果文件下载时包含中文名称,需要保证filename=后面的内容是ISO-8859-1编码。如果filename=后面是UTF-8编码且包含中文会乱码。
改写控制器代码,需要反复进行编码转换
@RequestMapping("/download")
public void download(HttpServletRequest req, HttpServletResponse response, String filename) {try {// 因为是GET请求,所以要解决请求参数中文乱码问题String fileNameUtf8 = new String(filename.getBytes("iso-8859-1"), "utf-8");// 图片名称满足固定格式String newFilenameUtf8 = "来自尚学堂的"+fileNameUtf8;String newFilenameISO = new String(newFilenameUtf8.getBytes("utf-8"),"iso-8859-1");// 此处是ISO-8859-1编码的内容response.setHeader("Content-Disposition", "attachment;filename=" + newFilenameISO);// 此处必须是UTF-8解决参数乱码问题的名称File file = new File(req.getServletContext().getRealPath("/images"), fileNameUtf8);FileInputStream fis = new FileInputStream(file);ServletOutputStream os = response.getOutputStream();IOUtils.copy(fis, os);} catch (IOException e) {e.printStackTrace();}
}