前言:本人为还未毕业的实习小菜鸟,出于不泄露公司源码的考虑,以下代码均为简化版示例,其中详细逻辑和异常处理已省略。
初始场景:
现有一批量上传文件的接口
@PostMapping("/imgUpload")
public String imgUpload(List<Multipartfile> files) {
for(Multipartfile file : files) {
//upload file...
}
return "上传成功!";
}
改进需求:
项目组长提到,功能是实现了,但要考虑多种实际情况。比如说客户可能一次性要上传几百几千个文件,总大小超过1G甚至更大,那么上面这个接口会一直到for循环内的逻辑全部执行完后才return。造成的结果就是客户这边发起请求后,浏览器转了半天没反应,而且后台处理的事务对于用户来说是不可见的,更何况一般网站还得设置请求超时限制,所以这个简单的批量上传接口必须优化:使后台业务不会堵塞在前端,而且要有操作提示,让用户知道目前是什么情况。
正文开始:
先是“后台业务处理不堵塞前端”这个需求,首先想到的肯定就是利用多线程:让线程来执行具体的批量上传逻辑,Controller的方法在启动完该线程后就直接返回响应。(至于线程内如何给前端返回操作提示和实时信息,这个是题外话,以后有机会再写一篇博客记录)
于是有了以下改进版代码:
//批量上传文件的线程
class ImageUploadThread extends Thread {
private List<Multipartfile> files;
public void setFiles(List<Multipartfile> files){
this.files = files;
}
@Override
public void run(){
for(Multipartfile file : files){
//upload file...
System.out.println("[线程]文件" + file.getName() + "已上传。");
}
System.out.println("[线程]文件上传完成");
}
}
//请求调用的接口
@PostMapping("/imgUpload")
public String imgUpload(List<Multipartfile> files) {
ImageUploadThread imgThread = new ImageUploadThread();
imgThread.setFiles(files);
imgThreaD.start();
return "已启动上传文件线程。";
}
开始测试,但输出结果为:
已启动上传文件线程。
[线程]文件img1.jpg已上传。
[线程]文件img2.jpg已上传。
[线程]文件img3.jpg已上传。
[线程]文件img4.jpg已上传。
java.io.FileNotFoundException: ...(找不到某个xxxxx00001.tmp文件,省略若干报错信息)
java.io.FileNotFoundException: ...(找不到某个xxxxx00002.tmp文件,省略若干报错信息)
java.io.FileNotFoundException: ...(找不到某个xxxxx00003.tmp文件,省略若干报错信息)
java.io.FileNotFoundException: ...(找不到某个xxxxx00004.tmp文件,省略若干报错信息)
java.io.FileNotFoundException: ...(找不到某个xxxxx00005.tmp文件,省略若干报错信息)
java.io.FileNotFoundException: ...(找不到某个xxxxx00006.tmp文件,省略若干报错信息)
java.io.FileNotFoundException: ...(找不到某个xxxxx00007.tmp文件,省略若干报错信息)
...
[线程]文件img5.jpg已上传。
[线程]文件img6.jpg已上传。
[线程]文件img7.jpg已上传。
[线程]文件img8.jpg已上传。
查看上传文件目录(我的情景是上传到阿里云oss),发现虽然提示信息里所有文件的输出语句都执行了,但上传目标目录里只有孤零零一个img1.jpg
经百度,首先得知抛FileNotFoundException的原因是:服务器在接收到带文件的请求时会首先把文件拷贝一份临时tmp到web容器目录下,这也是List<MultipartFile> files得以处理的原因。
但在上述情景中,我的Controller方法在把files作为参数传给Thread类并启动线程后就return了,而Controller方法一return,就视为一次请求-响应已结束,所以web容器里存放临时文件的目录也就被清空了,上面抛的找不到文件异常自然就是找不到这些临时文件。解决方法是使用流作为传入参数。
网上案例都是单个文件异步上传,不过道理是通用的,以下为本人修改后的批量异步上传代码示例:
//修改后的批量上传文件线程
class ImageUploadThread extends Thread {
private Map<String, InputStream> streams;//之前的文件集合list改为文件流集合map
public void setStreams(Map<String, InputStream> streams){
this.streams = streams;
}
@Override
public void run(){
for(Map.Entry<String, InputStream> stream : streams.entrySet()){//遍历Map
//upload file...
System.out.println("[线程]文件" + stream.getKey() + "已上传。");
}
System.out.println("[线程]文件上传完成");
}
}
//修改后的请求调用接口
@PostMapping("/imgUpload")
public String imgUpload(List<Multipartfile> files) {
Map<String, InputStream> streams = new HashMap<>();
if(!files.isEmpty) {
for(Multipartfile file : files) {
stream.put(file.getOriginalFileName, file,getInputStream);
}
}
ImageUploadThread imgThread = new ImageUploadThread();
imgThread.setStreams(streams);//传入文件流集合
imgThreaD.start();
return "已启动上传文件线程。";
}
将入参改为文件流后即使Controller方法已return,也不会影响线程继续上传文件。(如你所见,Map的遍历是无序的)
已启动上传文件线程。
[线程]文件img2.jpg已上传。
[线程]文件img8.jpg已上传。
[线程]文件img1.jpg已上传。
[线程]文件img5.jpg已上传。
[线程]文件img3.jpg已上传。
[线程]文件img6.jpg已上传。
[线程]文件img7.jpg已上传。
[线程]文件img4.jpg已上传。