所有代码点击此处下载.
开始
最近在做关于文件上传的小项目(前端LayUI,后端SpringBoot),需要实现上传进度监听。首先介绍本次测试代码框架。
代码框架
作为Maven项目,首先就是引入依赖:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.0</version>
<scope>test</scope>
</dependency>
<!-- Thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- SpringBoot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
配置文件:
spring:
servlet:
multipart:
max-file-size: 10000MB
max-request-size: 100000MB
项目启动类:
@SpringBootApplication
public class UploadApplication {
public static void main(String[] args) {
SpringApplication.run(UploadApplication.class, args);
}
}
控制器:
@Controller
public class IndexController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private String baseFile = "D:\\Temp\\Upload";
@RequestMapping({"/", "/index"})
public String index() {
return "index";
}
@PostMapping("/upload")
@ResponseBody
public String upload(@RequestParam("file") MultipartFile multipartFile) {
File parentFile = new File(baseFile);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
String originalFilename = multipartFile.getOriginalFilename();
File destFile = new File(baseFile + File.separator + originalFilename);
try {
multipartFile.transferTo(destFile);
} catch (IOException e) {
e.printStackTrace();
return "{\"code\":0}";
}
return "{\"code\":1}";
}
}
文件上传前端:
<!DOCTYPE HTML>
<html>
<head>
<title>File Upload</title>
<link rel="stylesheet" href="https://www.layuicdn.com/layui/css/layui.css"/>
</head>
<body>
<div class="layui-container">
<div class="layui-row">
<div class="layui-col-xs4 layui-col-sm7 layui-col-md8">
<div class="grid-demo layui-bg-green">|</div>
<br />
<blockquote class="layui-elem-quote">Upload</blockquote>
<div class="layui-upload-drag" id="test10">
<i class="layui-icon"></i>
<p>点击上传,或将文件拖拽到此处</p>
<div class="layui-hide" id="uploadDemoView">
<hr>
<img src="" alt="上传成功" style="max-width: 196px">
</div>
<br />
<!-- 进度条-->
<div class="layui-progress layui-progress-big" lay-showpercent="true" lay-filter="demo">
<div id="progress" class="layui-progress-bar layui-bg-red" lay-percent="0%"></div>
</div>
</div>
</div>
</div>
<br />
<button type="button" class="layui-btn" id="testListAction">click to submit</button>
</div>
</body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://www.layuicdn.com/layui-v2.5.6/layui.js"></script>
<script>
layui.use('form', function(){
var form = layui.form;
form.render();
});
layui.use(['upload','jquery','element'], function(){
var $ = layui.jquery
,upload = layui.upload
,element = layui.element;
element.init();
//拖拽上传
upload.render({
elem: '#test10'
,url: "/upload"
//限制大小 100M
// ,size: 100000
,auto: false//是否选完文件后自动上传
,bindAction: '#testListAction'//指向一个按钮触发上传
,method: 'post'
//接收任何类型的文件
,accept: 'file'
//完成回调
,done: function(res){
layer.msg(res.code);
if (res.code){
layui.$('#uploadDemoView').removeClass('layui-hide').attr('src');
}
element.progress('demo','100%');
}
});
});
</script>
</html>
前端主要使用了LayUI的拖拽上传组件。在想文件上传进度监听的时候,我一开始尝试使用LayUI文件上传监听,可是失败了。
后来,我在看代码时无意间发现SpringBoot的MultipartFile
接口实现了InputStreamSource
接口,那么就说明,它可以直接获取文件的InputStream
,那么如果放弃使用transferTo
方法,转而自己完成文件传输代码,同时记录已传输字节数何总字节数,再借助Redis或者Session缓存这个记录,那么便可以提供一个上传进度接口以达到上传进度监听的目的了,想到这,我快乐地开始测试。
第一次尝试——getInputStream()
思路有了代码就简单了。
v1
文件上传映射方法:
@PostMapping("/upload")
@ResponseBody
public String upload(@RequestParam("file") MultipartFile multipartFile) {
File parentFile = new File(baseFile);
if (!parentFile.exists()) {
parentFile.mkdirs();
}
String originalFilename = multipartFile.getOriginalFilename();
File destFile = new File(baseFile + File.separator + originalFilename);
try {
// multipartFile.transferTo(destFile);
doUpload(multipartFile, destFile);
} catch (IOException e) {
e.printStackTrace();
return "{\"code\":0}";
}
return "{\"code\":1}";
}
其他东西没变,就是把transferTo
方法换成了自定义的doUpload
方法。
doUpload
方法主要实现文件传输的具体逻辑同时缓存传输中间状态。
Progress和ProgressData都是封装的进度:
public class Progress {
private Long bytesRead;
private Long totalBytes;
//Getter、Setter
}
//该类主要用于请求返回
public class ProgressData {
private Integer data;
private Boolean success;
//Getter、Setter
}
这些代码毕竟都是测试用的,所以这里就不用Redis了,直接用Session得了。
private void doUpload(MultipartFile file, File destFile) throws IOException {
logger.info("Upload Begin {}", new Date().toString());
Progress progress = new Progress();
progress.setBytesRead(0l);
progress.setTotalBytes(file.getSize());
session.setAttribute("progress", progress);
BufferedInputStream bufferedInputStream = new BufferedInputStream(file.getInputStream());
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(destFile));
byte[] buffer = new byte[102400];
int read = 0;
while ((read = bufferedInputStream.read(buffer)) != -1) {
bufferedOutputStream.write(buffer);
Progress targetProgress = (Progress) session.getAttribute("progress");
// 取出数据并更新
targetProgress.setBytesRead(targetProgress.getBytesRead() + read);
session.setAttribute("progress", targetProgress);
}
logger.info("Upload end {}", new Date().toString());
bufferedOutputStream.flush();
session.removeAttribute("progress");
}
剩下的就很简单了,写个上传进度监听接口:
@ResponseBody
@GetMapping("/progress")
public ProgressData getProgress(HttpServletRequest request) {
HttpSession session = request.getSession();
Progress progress = (Progress) session.getAttribute("progress");
ProgressData progressData = new ProgressData();
if (progress == null) {
progressData.setSuccess(false);
progressData.setData(0);
return progressData;
}
logger.info("read/all {}/{}", progress.getBytesRead(), progress.getTotalBytes());
progressData.setData((int) ((double)progress.getBytesRead() * 100 / progress.getTotalBytes()));
progressData.setSuccess(true);
return progressData;
}
逻辑一目了然,从Session中获取进度状态并返回。
前端进度条更新:
layui.use('form', function(){
var form = layui.form;
form.render();
});
layui.use(['upload','jquery','element'], function(){
var $ = layui.jquery
,upload = layui.upload
,element = layui.element;
element.init();
//拖拽上传
upload.render({
elem: '#test10'
,url: "/upload"
//限制大小 100M
// ,size: 100000
,auto: false//是否选完文件后自动上传
,bindAction: '#testListAction'//指向一个按钮触发上传
,method: 'post'
//接收任何类型的文件
,accept: 'file'
,before: function (obj){
getProgress(element);
}
//完成回调
,done: function(res){
layer.msg(res.code);
if (res.code){
layui.$('#uploadDemoView').removeClass('layui-hide').attr('src');
}
element.progress('demo','100%');
}
});
});
function getProgress(element) {
$.ajax({
url: 'http://localhost:8080/progress',
type: 'get',
dataType: 'json',
success: function (data) {
//方法中传入的参数data为后台获取的数据
console.log(data);
var percent = data.data;
if (percent != 0) {
progress = percent;
element.progress('demo', progress + '%');
}
console.log(percent);
$('#progress').attr("lay-percent", percent);
if (data.data != null){
setTimeout(getProgress, 2000, element);
}
}
})
}
简直🐂🍺。
我美滋滋地开始运行测试,结果发现前端获取到的ProgressData全都是
{"data":0,"success":false}
我日噢,怎么回事,在我不断Debug下,终于发现了猫腻,一次Ajax请求一个Session。
v2
简单,我直接用第一次请求的Session不就行了。于是我给IndexController加一个HttpSession属性:
// 仅用于测试!!
private HttpSession session;
在用户访问首页的时候赋值:
@RequestMapping({"/", "/index"})
public String index(HttpServletRequest request) {
session = request.getSession();
return "index";
}
修改进度获取:
@ResponseBody
@GetMapping("/progress")
public ProgressData getProgress() {
Progress progress = (Progress) session.getAttribute("progress");
ProgressData progressData = new ProgressData();
if (progress == null) {
progressData.setSuccess(false);
progressData.setData(0);
return progressData;
}
logger.info("read/all {}/{}", progress.getBytesRead(), progress.getTotalBytes());
progressData.setData((int) ((double)progress.getBytesRead() * 100 / progress.getTotalBytes()));
progressData.setSuccess(true);
return progressData;
}
测试:
这美滋滋啊,大功告成。
等我屁颠屁颠把代码传到服务器测试发现,不管怎么获取进度条数据永远都是0。
查看日志发现:
3------------------Sun Sep 13 15:23:17 CST 2020
Time Spent: 591 ms
4------------------Sun Sep 13 15:23:18 CST 2020
100M的文件0.5s通过我这破网络传完你能信?实际等待时间也远远不止0.5s,然而前端获取到的进度数据永远为0.
查询后发现,Tomcat处理文件上传不会直接上传到目的位置,而是先上传到临时目录,比如Linux下是/tmp,Windows下应该是C:\Users\ {username}\AppData\Local\Temp
logger.info(System.getProperty("java.io.tmpdir"));
但我还是没找到是啥原因造成,知道的同志麻烦教教我。
第二次尝试——自定义解析器
我又继续查资料,终于看到这么一篇文章:SpringBoot+fileUpload获取文件上传进度。
觉得好像很有道理的样子,于是开始尝试。
导入文件上传依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
自定义解析器:
public class CustomMultipartResolver extends CommonsMultipartResolver {
@Autowired
private FileUploadProgressListener progressListener;
@Override
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
FileUpload fileUpload = prepareFileUpload("UTF-8");
progressListener.setSession(request.getSession());
fileUpload.setProgressListener(progressListener);
try {
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
return parseFileItems(fileItems, "UTF-8");
} catch (FileUploadException e) {
e.printStackTrace();
}
return super.parseRequest(request);
}
}
这里需要注意的是导入的大部分文件上传相关的类应该在包org.apache.commons.fileupload
下。
进度监听器:
@Component
public class FileUploadProgressListener implements ProgressListener{
private HttpSession session;
public void setSession(HttpSession session) {
this.session = session;
Progress progress = new Progress();
progress.setTotalBytes(0l);
progress.setBytesRead(0l);
session.setAttribute("progress", progress);
}
@Override
public void update(long l, long l1, int i) {
Progress progress = (Progress) session.getAttribute("progress");
if (progress.getBytesRead() == progress.getTotalBytes()
&& progress.getBytesRead() != 0) {
session.removeAttribute("progress");
}
progress.setBytesRead(l);
progress.setTotalBytes(l1);
session.setAttribute("progress", progress);
}
}
配置自定义解析器替换原来的解析器:
@Configuration
public class UploadConfig {
@Bean(name = "multipartResolver")
public MultipartResolver multipartResolver(){
CustomMultipartResolver customMultipartResolver = new CustomMultipartResolver();
return customMultipartResolver;
}
}
当然,还有,需要关闭文件上传的自动配置:
@SpringBootApplication(exclude = MultipartAutoConfiguration.class)
public class UploadApplication {
public static void main(String[] args) {
SpringApplication.run(UploadApplication.class, args);
}
}
结果:
现在服务端运行正常,还剩个问题就是上传速度有些慢。
发现了个问题,MySQL字符集为UTF- 8时插入不了表情。