> 文章列表 > vue+springboot上传大文件

vue+springboot上传大文件

vue+springboot上传大文件

前言

众所周知,上传文件是一件很麻烦的事情,假如一条路走到黑,直接一次性把文件上传上去,对于小文件是可以这样做,但是对于大文件可能会出现网络问题,请求响应时长等等导致文件上传失败,那么这次来教大家如何用vue+srpingboot项目上传大文件

逻辑

需要做大文件上传应该考虑到如下逻辑:

  1. 大文件上传一般需要将文件切片(chunk)上传,然后再将所有切片合并为完整的文件。可以按以下逻辑进行实现:
  1. 前端在页面中选择要上传的文件,并使用Blob.slice方法对文件进行切片,一般每个切片大小为固定值(比如5MB),并记录总共有多少个切片。
  1. 将切片分别上传到后端服务,可以使用XMLHttpRequest或Axios等库发送Ajax请求。对于每个切片,需要包含三个参数:当前切片索引(从0开始)、切片总数、切片文件数据
  1. 后端服务接收到切片后,保存到指定路径下的临时文件中,并记录已上传的切片索引和上传状态。如果某个切片上传失败,则通知前端重传该切片。
  1. 当所有切片都上传成功后,后端服务读取所有切片内容并将其合并为完整的文件。可以使用java.io.SequenceInputStream和BufferedOutputStream来实现文件合并。
  1. 最后返回文件上传成功的响应结果给前端即可。

前端

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>File Upload</title>
</head>
<body><input type="file" id="fileInput"><button onclick="upload()">Upload</button><script>function upload() {let file = document.getElementById("fileInput").files[0];let chunkSize = 5 * 1024 * 1024; // 切片大小为5MBlet totalChunks = Math.ceil(file.size / chunkSize); // 计算切片总数let index = 0;while (index < totalChunks) {let chunk = file.slice(index * chunkSize, (index + 1) * chunkSize);let formData = new FormData();formData.append("file", chunk);formData.append("index", index);formData.append("totalChunks", totalChunks);// 发送Ajax请求上传切片$.ajax({url: "/uploadChunk",type: "POST",data: formData,processData: false,contentType: false,success: function () {if (++index >= totalChunks) {// 所有切片上传完成,通知服务端合并文件$.post("/mergeFile", {fileName: file.name}, function () {alert("Upload complete!");})}}});}}</script>
</body>
</html>

后端

controller层:

@RestController
public class FileController {@Value("${file.upload-path}")private String uploadPath;@PostMapping("/uploadChunk")public void uploadChunk(@RequestParam("file") MultipartFile file,@RequestParam("index") int index,@RequestParam("totalChunks") int totalChunks) throws IOException {// 以文件名+切片索引号为文件名保存切片文件String fileName = file.getOriginalFilename() + "." + index;Path tempFile = Paths.get(uploadPath, fileName);Files.write(tempFile, file.getBytes());// 记录上传状态String uploadFlag = UUID.randomUUID().toString();redisTemplate.opsForList().set("upload:" + fileName, index, uploadFlag);// 如果所有切片已上传,则通知合并文件if (isAllChunksUploaded(fileName, totalChunks)) {sendMergeRequest(fileName, totalChunks);}}@PostMapping("/mergeFile")public void mergeFile(String fileName) throws IOException {// 所有切片均已成功上传,进行文件合并List<File> chunkFiles = new ArrayList<>();for (int i = 0; i < getTotalChunks(fileName); i++) {String chunkFileName = fileName + "." + i;Path tempFile = Paths.get(uploadPath, chunkFileName);chunkFiles.add(tempFile.toFile());}Path destFile = Paths.get(uploadPath, fileName);try (OutputStream out = Files.newOutputStream(destFile);SequenceInputStream seqIn = new SequenceInputStream(Collections.enumeration(chunkFiles));BufferedInputStream bufIn = new BufferedInputStream(seqIn)) {byte[] buffer = new byte[1024];int len;while ((len = bufIn.read(buffer)) > 0) {out.write(buffer, 0, len);}}// 清理临时文件和上传状态记录for (int i = 0; i < getTotalChunks(fileName); i++) {String chunkFileName = fileName + "." + i;Path tempFile = Paths.get(uploadPath, chunkFileName);Files.deleteIfExists(tempFile);redisTemplate.delete("upload:" + chunkFileName);}}private int getTotalChunks(String fileName) {// 根据文件名获取总切片数return Objects.requireNonNull(Paths.get(uploadPath, fileName).toFile().listFiles()).length;}private boolean isAllChunksUploaded(String fileName, int totalChunks) {// 判断所有切片是否已都上传完成List<String> uploadFlags = redisTemplate.opsForList().range("upload:" + fileName, 0, -1);return uploadFlags != null && uploadFlags.size() == totalChunks;}private void sendMergeRequest(String fileName, int totalChunks) {// 发送合并文件请求new Thread(() -> {try {URL url = new URL("http://localhost:8080/mergeFile");HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setRequestMethod("POST");conn.setDoOutput(true);conn.setDoInput(true);conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");OutputStream out = conn.getOutputStream();String query = "fileName=" + fileName;out.write(query.getBytes());out.flush();out.close();BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));while (br.readLine() != null) ;br.close();} catch (IOException e) {e.printStackTrace();}}).start();}@Autowiredprivate RedisTemplate<String, Object> redisTemplate;
}

其中,file.upload-path为文件上传的保存路径,可以在application.properties或application.yml中进行配置。同时需要添加RedisTemplate的Bean以便记录上传状态。

RedisTemplate配置

如果需要使用RedisTemplate,需要引入下方的包

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

同时在yml配置redis的信息

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0

然后在自己的类中这样使用

@Component
public class myClass {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;public void set(String key, Object value) {redisTemplate.opsForValue().set(key, value);}public Object get(String key) {return redisTemplate.opsForValue().get(key);}
}

注意事项

需要控制每次上传的切片大小,以兼顾上传速度和稳定性,避免占用过多服务器资源或因网络不稳定而导致上传失败。

切片上传存在先后顺序,需要保证所有切片都上传完成后再进行合并,否则可能会出现文件不完整或者文件合并错误等情况。

上传完成后需要及时清理临时文件,避免因为占用过多磁盘空间而导致服务器崩溃。可以设置一个定期任务来清理过期的临时文件。

结语

以上就是vue+springboot上传大文件的逻辑

股市行情