问题背景:
最近在做人脸相关的接入,很简单的一个demo,需要图片上传功能,那么我们该怎么操作才能将图片从本地上传到服务器呢?
问题本质:
图片的上传(其实算文件上传的一种)本质上就是文件的读写加上文件在http请求中的传输。
文件: 保存在磁盘的一段二进制数字(0或者1)
传输:通过http协议将这段二进制数字传输到服务器端,并在磁盘创建空间,将这段文字写进去。
http协议是否提供给我们这样一个类似的协议呢?肯定是有的。
HTTP协议:
http协议主要有请求报文和相应报文,我们今天重点说下请求报文,相应报文其实差不多。
http请求报文分为三个部分:请求行、请求头、请求体。先来看下这张图:
我们这里需要重点关注的是 请求头中“Content-Type”属性。这个属性是告诉服务器我们发送的数据是什么根式的内容。查阅资料发现,如果我们需要上传图片的话,需要将Content-Type 设置为:multipart/form-data。
SpringMVC中如何上传文件:
我们可以使用 commons-fileupload 来帮助我们更加简洁的实现文件的上传:
maven 的pom.xml中引入相关的文件包:
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.pool</artifactId>
<version>1.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
后台相关逻辑:
@RequestMapping("/uploadFile")
public void uploadFile(HttpServletRequest request, HttpServletResponse response) {
ProgressListener progressListener = new ProgressListener() {
@Override
public void update(long l, long l1, int i) {
}
};
String savePath = "D:\\faceImage\\";
File file = new File(savePath);
if (!file.exists() && file.isDirectory()) {
System.out.println(savePath + "目录不存在,需要创建");
file.mkdir();
}
try {
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("UTF-8");
if (!ServletFileUpload.isMultipartContent(request)) {
//
}
upload.setProgressListener(progressListener);
List<FileItem> fileItemList = upload.parseRequest(request);
fileItemList.forEach(item -> {
if (item.isFormField()) {
//普通的参数对应的读取
String name = item.getFieldName();
try {
String value = item.getString("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
} else {
//如果fileupload中是上传文件
String fileName = item.getName();
if (fileName == null || fileName.trim().equals("")) {
return;
}
if (fileName.contains("\\")) {
fileName = fileName.substring(fileName.lastIndexOf("\\" + 1));
}
try {
InputStream inputStream = item.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream(savePath + "\\" + fileName);
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer);
}
inputStream.close();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
前端页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:action="@{/uploadFile}" enctype="multipart/form-data" method="post">
<label style="float: left">上传文件:</label>
<input type="file" name="filename" style="height: 40px;padding-top: 10px" class="showFileName">
<button type="submit" class="btn btn-primary">保存</button>
</form>
</body>
</html>
注意:form表单的 eenctype需要设置multipart/form-data。这个属性表明发送的请求体的内容是多表单元素的。也就是请求体中会有各式各样的数据,比如二进制、表单数据。我们可以F12看下发送的内容
springboot中如何上传文件:
springboot 中有两种上传文件的方式:
- 和springmvc 类似,使用commons-fileupload组件上传。
- 利用MultipartRequest来处理文件内容。
我们这里先使用第二种方式来上传文件,首先先对MultipartRequest做一个简单的介绍:
MultipartReques类主要是对文件上传进行的处理,在上传文件时,编码格式为enctype="multipart/form-data"格式,以二进制形式提交数据,提交方式为post方式。
后台代码:
@RequestMapping("/uploadFile")
@ResponseBody
public String test(HttpServletRequest request, HttpServletResponse response) {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(request.getSession().getServletContext());
int personId = Integer.valueOf(request.getParameter("personId"));
//判断 request 是否有文件上传,即多部分请求
if (multipartResolver.isMultipart(request)) {
//转换成多部分request
try {
MultipartHttpServletRequest params = ((MultipartHttpServletRequest) request);
List<MultipartFile> files = ((MultipartHttpServletRequest) request).getFiles("file");
MultipartFile file = null;
BufferedOutputStream stream = null;
for (int i = 0; i < files.size(); ++i) {
file = files.get(i);
if (!file.isEmpty()) {
if (file.getSize() > 2000000) {
return "上传失败,文件大小不成超过2M";
}
InputStream inputStream = file.getInputStream();
//保存图片
ImageUtils.SaveFileFromInputStream(inputStream, "D:\\faceImage\\", file.getOriginalFilename());
String base64 = ImageUtils.getImageBinary("D:\\faceImage\\" + file.getOriginalFilename());
//注册
ReturnBean returnBean = register(personId, base64);
if (returnBean.getCode() == 0) {
returnBean.setMsg("注册成功");
} else {
String result = returnBean.getMsg();
returnBean.setMsg("注册失败 " + result);
}
return gson.toJson(returnBean);
} else {
return "You failed to upload " + i
+ " because the file was empty.";
}
}
return "upload successful";
} catch (Exception e) {
e.printStackTrace();
return "上传失败," + e.getMessage();
}
}
return null;
}
相比较第一种方法,这种方法其实方便不少,不需要引入相关的maven依赖。代码量也相对较少。
2 使用commons-fileupload组件上传:
springboot 中如果使用commons-fileupload上传时,代码不变,pom和之前一样。但是在执行过程会出现一个问题:
在执行到 upload.parseRequest(request);发现获取到 List 为空,也就是没有数据。
这个问题是由于springboot中已经认识到这次的request是一个MultipartRequest,所以在获取到request之后,springboot帮我们做了一个转化,将普通的httpRequest转化成MultipartRequest。
如何解决:
类似这种springboot自动配置的参数,我们都可以在properties自己设置:
spring.servlet.multipart.enabled=false
这样spirngboot 就不会自动帮我们处理enctype="multipart/form-data”格式的请求。
总结:
仔细看下来,文件上传的主要逻辑还是在后端的逻辑处理。
- 创建工厂类
- 使用核心类,也就是ServletFileUpload文件上传解析器。
- 解析request请求,遍历请求体,拿出上传内容和普通表单内容。
- 对二进制文件进行读写操作,写入到具体的磁盘位置。
通过这次学习文件上传,我再次发现基础真的很重要,我在写代码的时候,发现最简单的读写流操作都不写,基础对file操作也不会。