参考:https://javajgs.com/archives/26157
一.背景
1-1 需求
前端上传Word文档,后端将接收到的Word文档①上传到文件服务器②将Word转为Pdf。
1-2 方案
因为Word转Pdf的耗时较长,为了及时给到前端返回信息,在将文件上传到文件服务器后,异步将Word转为Pdf。
二.实现
创建一个SpringBoot项目。
1 package com.trent.upload.action; 2 3 import com.trent.upload.service.UploadService; 4 import org.springframework.web.bind.annotation.PostMapping; 5 import org.springframework.web.bind.annotation.RequestMapping; 6 import org.springframework.web.bind.annotation.RestController; 7 import org.springframework.web.multipart.MultipartFile; 8 9 import javax.annotation.Resource; 10 11 /** 12 * 上传文件的Action层 13 * 14 * @author Hutao 15 * @date 2022/8/16 15:10 16 * @since 1.0 17 */ 18 @RequestMapping 19 @RestController 20 public class UploadAction { 21 22 @Resource 23 private UploadService uploadService; 24 25 /** 26 * 文件上传接口 27 * @param multipartFile 上传的文件 28 * @return 上传结果 29 * 30 * @author Hutao 31 * @date 2022/8/16 15:10 32 * @since 1.0 33 */ 34 @PostMapping("/upload") 35 public String upload(MultipartFile multipartFile) { 36 37 uploadService.dealFile(multipartFile); 38 return "上传成功"; 39 } 40 }
1 package com.trent.upload.service; 2 3 import org.springframework.stereotype.Service; 4 import org.springframework.web.multipart.MultipartFile; 5 6 import java.util.concurrent.ExecutorService; 7 import java.util.concurrent.Executors; 8 import java.util.concurrent.TimeUnit; 9 10 /** 11 * 上传文件的Service层 12 * 13 * @author Hutao 14 * @date 2022/8/16 15:11 15 * @since 1.0 16 */ 17 @Service 18 public class UploadService { 19 20 /** 21 * 线程池(仅用于简单演示) 22 */ 23 private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool(); 24 25 /** 26 * 处理上传的文件 27 * @param multipartFile 上传的文件 28 * 29 * @author Hutao 30 * @date 2022/8/16 15:13 31 * @since 1.0 32 */ 33 public void dealFile(MultipartFile multipartFile) { 34 // 模拟上传Word文件到文件服务器 35 System.out.println("上传Word文件到文件服务器"); 36 37 // 异步将Word文件转为Pdf 38 EXECUTOR_SERVICE.execute(() -> convertToPdf(multipartFile)); 39 } 40 41 /** 42 * 将文件转为Pdf 43 * @param multipartFile 待转换的源文件 44 * 45 * @author Hutao 46 * @date 2022/8/16 15:13 47 * @since 1.0 48 */ 49 public void convertToPdf(MultipartFile multipartFile) { 50 try { 51 52 // 获取上传的文件的输入流,用于转为Pdf,如果成功获取到了输入流,就认为转Pdf成功 53 multipartFile.getInputStream(); 54 System.out.println("Word转Pdf成功"); 55 56 } catch (Exception e) { 57 System.out.println("Word转Pdf失败"); 58 e.printStackTrace(); 59 } 60 } 61 }
三.问题
3-1 问题描述
以上是一个简单的演示,在实际的项目中,会偶现如下异常。
上传Word文件到文件服务器 Word转Pdf失败 java.io.FileNotFoundException: C:\Users\Liujl\AppData\Local\Temp\tomcat.8080.567748920478140755\work\Tomcat\localhost\ROOT\upload_8e27d0a7_9cf4_4f8a_aecc_ea051653749e_00000006.tmp (系统找不到指定的文件。) at java.io.FileInputStream.open0(Native Method) at java.io.FileInputStream.open(FileInputStream.java:195) at java.io.FileInputStream.<init>(FileInputStream.java:138) at org.apache.tomcat.util.http.fileupload.disk.DiskFileItem.getInputStream(DiskFileItem.java:198) at org.apache.catalina.core.ApplicationPart.getInputStream(ApplicationPart.java:100) at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.getInputStream(StandardMultipartHttpServletRequest.java:254) at com.trent.upload.service.UploadService.convertToPdf(UploadService.java:57) at com.trent.upload.service.UploadService.lambda$dealFile$0(UploadService.java:43) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
意思是找不到某个临时文件。
3-2 问题复现
在UploadService类的51行添加如下代码,可保证每次上传文件操作都会出现3-1中的异常。
1 public void convertToPdf(MultipartFile multipartFile) { 2 try { 3 TimeUnit.MILLISECONDS.sleep(10); 4 // 获取上传的文件的输入流,用于转为Pdf,如果成功获取到了输入流,就认为转Pdf成功 5 multipartFile.getInputStream(); 6 System.out.println("Word转Pdf成功"); 7 8 } catch (Exception e) { 9 System.out.println("Word转Pdf失败"); 10 e.printStackTrace(); 11 } 12 }
3-3 原因分析
1.后台用MultipartFile接收到前端传来的文件后,会在本地生成一个临时文件,以.tmp结尾;
2.MultipartFile对应的临时文件的生命周期是一个请求会话,会话结束,MultipartFile的临时文件会被自动清理;
3.因为将文件转为Pdf的方法是在请求主线程之外的另一个线程中执行的,所以不在请求会话的生命周期内。如果请求会话的主线程结束了(将请求结果返回给前端了),这个请求传来的MultipartFile的临时文件就会被清理掉,在将文件转Pdf的线程中就拿不到MultipartFile对应的临时文件,也就获取不到对应的输入流,故抛出FileNotFoundException。
四.解决方案
主线程在用MultipartFile接收到前端传来文件后,立即将MultipartFile保存为本地文件。将文件转Pdf时,使用保存在本地的文件,转换完成后,删除本地文件。
注:
1.可使用MultipartFile的transferTo方法将MultipargFile转为本地文件,但需要注意,transferTo方法被调用后,也会删除MultipartFile对应的临时文件;
2.在使用transferTo的时候可能会出现绝对路径和相对路径的问题;
3.鉴于1.2中transferTo方法的局限性,建议手动获取MultipartFile的输入流,然后写到本地文件中。可以使用Hutool的FileUtils.copyInputStreamToFile(final InputStream source, final File destination)方法;
4.本地文件使用完成后,务必删除本地文件,避免服务器硬盘被占满。