Spring Boot大文件分片上传实现
在Spring Boot里实现大文件的分片上传,而且Java要直接读取本地文件,通过POST提交。首先,我得理清楚分片上传的基本流程。分片上传通常是将大文件分成多个小块,分别上传到服务器,最后再合并这些块。这样做的好处是可以避免一次性上传大文件导致的超时或内存问题,而且支持断点续传。
那用户的需求是Java直接读取本地文件,可能是指客户端用Java程序来上传,而服务端用Spring Boot处理。或者是前端用某种方式,但用户提到Java直接读取,所以可能客户端也是Java写的,比如一个桌面应用或者命令行工具。但不管怎样,服务端需要用Spring Boot接收分片。
接下来要考虑分片上传的实现步骤。首先,客户端需要将文件切割成多个片,每个片要有唯一的标识,可能包括文件名、当前分片序号、总分片数等信息。然后通过POST请求发送每个分片到服务端。服务端接收到分片后,需要将这些分片暂存,等所有分片上传完成后合并成完整的文件。
对于服务端来说,需要有几个接口:一个是上传分片的接口,一个是合并分片的接口。可能还需要一个检查分片是否已上传的接口,用于断点续传,但用户可能暂时不需要这个,先做基本功能。
在Spring Boot中,处理文件上传通常用MultipartFile,但分片上传的话,可能需要自定义参数,比如当前分片序号、总分片数、文件唯一标识等。每个分片可以作为一个MultipartFile,或者作为二进制流传输。这里可能用POST请求,参数包括分片数据和元数据。
客户端方面,如果用Java来读取本地文件并分片上传,需要用Java的IO库来分割文件。比如,按固定大小读取文件块,每个块作为一个分片,然后通过HTTP客户端发送POST请求。这里可能需要用到HttpClient,比如Apache的HttpClient或者Spring的RestTemplate。
接下来具体步骤:
1. 客户端分割文件为多个块,每个块的大小比如1MB或更大。 2. 每个块上传时,携带分片信息:当前块序号、总块数、文件名或唯一标识、可能还有MD5校验等。 3. 服务端接收每个块,保存到临时目录,按文件名和块序号命名,或者统一管理。 4. 当所有块上传完毕,客户端发送合并请求,服务端将临时文件按顺序合并成完整文件。
需要注意的问题:
- 临时文件的存储和管理,避免冲突,可能用UUID作为上传会话ID。 - 分片上传的顺序可能乱序,服务端合并时需要按序号排序。 - 处理上传过程中可能出现的失败,比如某个分片失败需要重传。 - 文件合并的效率,特别是大文件合并时,要使用高效的方式,比如顺序写入。
现在,具体到代码实现。服务端需要两个接口,一个用于上传分片,一个用于合并。
上传分片的接口可能参数包括:file(分片文件)、chunkNumber(当前块号)、totalChunks(总块数)、identifier(文件唯一标识)。服务端将这些分片保存到临时文件夹,比如/tmp/upload/identifier/,每个分片保存为chunkNumber.part。
合并接口的参数可以是identifier和fileName,服务端读取所有分片,按序号排序后合并成目标文件。
客户端方面,Java代码需要读取本地文件,分块,然后逐个发送POST请求。比如用FileInputStream读取文件,每次读取固定大小的字节数组,然后通过HttpClient发送,携带分片参数。
需要考虑分片的大小设置,比如每个分片1MB或5MB,根据实际情况调整。另外,客户端需要跟踪已上传的分片,确保全部上传完成后触发合并请求。
可能的错误点:分片命名冲突,上传过程中断,服务端存储空间不足,分片校验等。为了简单起见,可能先不处理校验,但实际应用中需要校验每个分片的完整性,比如MD5哈希。
现在,编写示例代码:
服务端:
分片上传接口:
@PostMapping("/upload") public ResponseEntity<String> uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkNumber") int chunkNumber, @RequestParam("totalChunks") int totalChunks, @RequestParam("identifier") String identifier) { // 保存分片到临时目录 String tempDir = "/tmp/upload/" + identifier + "/"; File dir = new File(tempDir); if (!dir.exists()) { dir.mkdirs(); } String chunkFileName = chunkNumber + ".part"; try { file.transferTo(new File(tempDir + chunkFileName)); return ResponseEntity.ok("Chunk uploaded"); } catch (IOException e) { return ResponseEntity.status(500).body("Upload failed"); } }
合并接口:
@PostMapping("/merge") public ResponseEntity<String> mergeChunks(@RequestParam("identifier") String identifier, @RequestParam("fileName") String fileName) { String tempDir = "/tmp/upload/" + identifier + "/"; File dir = new File(tempDir); File[] chunks = dir.listFiles(); if (chunks == null || chunks.length == 0) { return ResponseEntity.badRequest().body("No chunks to merge"); } // 按分片序号排序 Arrays.sort(chunks, Comparator.comparingInt(f -> Integer.parseInt(f.getName().split("\\.")[0]))); File outputFile = new File("/path/to/destination/" + fileName); try (FileOutputStream fos = new FileOutputStream(outputFile)) { for (File chunk : chunks) { Files.copy(chunk.toPath(), fos); chunk.delete(); // 合并后删除分片 } // 删除临时目录 dir.delete(); return ResponseEntity.ok("Merge completed"); } catch (IOException e) { return ResponseEntity.status(500).body("Merge failed"); } }
客户端:
public void uploadFile(File file, String targetUrl) throws IOException { int chunkSize = 1 * 1024 * 1024; // 1MB byte[] buffer = new byte[chunkSize]; try (FileInputStream fis = new FileInputStream(file)) { int bytesRead; int chunkNumber = 0; String identifier = UUID.randomUUID().toString(); int totalChunks = (int) Math.ceil((double) file.length() / chunkSize); while ((bytesRead = fis.read(buffer)) != -1) { byte[] actualBytes = Arrays.copyOf(buffer, bytesRead); // 构建请求 HttpPost post = new HttpPost(targetUrl); MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.addBinaryBody("file", actualBytes, ContentType.DEFAULT_BINARY, "chunk"); builder.addTextBody("chunkNumber", String.valueOf(chunkNumber)); builder.addTextBody("totalChunks", String.valueOf(totalChunks)); builder.addTextBody("identifier", identifier); HttpEntity entity = builder.build(); post.setEntity(entity); // 发送请求 HttpClient httpClient = HttpClients.createDefault(); HttpResponse response = httpClient.execute(post); // 处理响应... chunkNumber++; } // 所有分片上传完成,调用合并接口 HttpPost mergePost = new HttpPost(targetUrl + "/merge"); List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("identifier", identifier)); params.add(new BasicNameValuePair("fileName", file.getName())); mergePost.setEntity(new UrlEncodedFormEntity(params)); HttpResponse mergeResponse = httpClient.execute(mergePost); // 处理合并响应... } }
这样应该可以实现基本的分片上传和合并。但需要注意几个问题:
1. 客户端在发送每个分片时,必须确保identifier一致,以便服务端将分片归到同一目录。 2. 分片序号从0开始还是从1开始需要前后端一致。 3. 合并时,分片文件的读取顺序必须正确,否则合并后的文件会损坏。 4. 临时目录的管理需要定期清理,避免占用过多磁盘空间。 5. 并发上传时,相同的identifier可能导致冲突,需要确保每个上传会话的identifier唯一。
另外,可能需要添加文件校验,比如在合并完成后检查文件MD5是否与客户端一致,确保上传正确。
如果用户需要断点续传的功能,服务端还需要提供一个接口,让客户端查询已经上传的分片,这样客户端可以跳过已上传的部分。但根据当前问题描述,可能暂时不需要,但可以作为优化点。
总结来说,分片上传的核心在于客户端分块、服务端接收并暂存、最终合并。代码示例中客户端使用HttpClient发送分片,服务端用Spring Boot处理上传和合并请求。需要注意分片顺序、唯一标识以及错误处理。
在Spring Boot中实现大文件分片上传需要分为客户端和服务端两部分。以下是详细的实现步骤和代码示例:
---
### **服务端实现(Spring Boot)**
#### 1. 添加依赖 确保`pom.xml`包含Web和文件处理依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ```
#### 2. 分片上传接口 处理每个分片的上传请求,保存到临时目录。 ```java @RestController public class FileUploadController {
@PostMapping("/upload") public ResponseEntity<String> uploadChunk( @RequestParam("file") MultipartFile file, @RequestParam("chunkNumber") int chunkNumber, @RequestParam("totalChunks") int totalChunks, @RequestParam("identifier") String identifier) throws IOException {
// 临时存储目录:根据唯一标识创建文件夹 String tempDir = "uploads/temp/" + identifier + "/"; File dir = new File(tempDir); if (!dir.exists()) dir.mkdirs();
// 保存分片文件,以 chunkNumber 命名 String chunkFilename = chunkNumber + ".part"; file.transferTo(new File(tempDir + chunkFilename));
return ResponseEntity.ok("Chunk uploaded successfully."); } } ```
#### 3. 合并分片接口 当所有分片上传完成后,合并分片为完整文件。 ```java @PostMapping("/merge") public ResponseEntity<String> mergeChunks( @RequestParam("identifier") String identifier, @RequestParam("fileName") String fileName) throws IOException {
String tempDir = "uploads/temp/" + identifier + "/"; File[] chunks = new File(tempDir).listFiles();
// 按分片序号排序 Arrays.sort(chunks, Comparator.comparingInt(f -> Integer.parseInt(f.getName().split("\\.")[0])));
// 创建目标文件 File outputFile = new File("uploads/" + fileName); try (FileOutputStream fos = new FileOutputStream(outputFile, true)) { // 追加模式 for (File chunk : chunks) { Files.copy(chunk.toPath(), fos); chunk.delete(); // 删除已合并的分片 } }
// 删除临时目录 FileUtils.deleteDirectory(new File(tempDir));
return ResponseEntity.ok("File merged successfully."); } ```
---
### **客户端实现(Java)**
使用Java读取本地文件并分片上传:
#### 1. 分片上传逻辑 ```java import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients;
import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.UUID;
public class ChunkedFileUploader {
private static final String SERVER_URL = "http://localhost:8080/upload"; private static final int CHUNK_SIZE = 1 * 1024 * 1024; // 1MB
public static void uploadFile(File file) throws IOException { String identifier = UUID.randomUUID().toString(); int totalChunks = (int) Math.ceil((double) file.length() / CHUNK_SIZE);
try (FileInputStream fis = new FileInputStream(file); CloseableHttpClient httpClient = HttpClients.createDefault()) {
byte[] buffer = new byte[CHUNK_SIZE]; int bytesRead; int chunkNumber = 0;
while ((bytesRead = fis.read(buffer)) != -1) { // 构建分片请求 HttpPost post = new HttpPost(SERVER_URL); HttpEntity entity = MultipartEntityBuilder.create() .addBinaryBody("file", new ByteArrayInputStream(buffer, 0, bytesRead), ContentType.APPLICATION_OCTET_STREAM, "chunk") .addTextBody("chunkNumber", String.valueOf(chunkNumber)) .addTextBody("totalChunks", String.valueOf(totalChunks)) .addTextBody("identifier", identifier) .build(); post.setEntity(entity);
// 发送请求 try (CloseableHttpResponse response = httpClient.execute(post)) { if (response.getStatusLine().getStatusCode() != 200) { throw new IOException("上传分片失败: " + chunkNumber); } } chunkNumber++; }
// 所有分片上传完成后,请求合并 mergeFile(identifier, file.getName()); } }
private static void mergeFile(String identifier, String fileName) throws IOException { HttpPost post = new HttpPost("http://localhost:8080/merge"); HttpEntity entity = MultipartEntityBuilder.create() .addTextBody("identifier", identifier) .addTextBody("fileName", fileName) .build(); post.setEntity(entity);
try (CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = httpClient.execute(post)) { if (response.getStatusLine().getStatusCode() != 200) { throw new IOException("合并文件失败"); } } }
public static void main(String[] args) throws IOException { uploadFile(new File("path/to/your/large/file.zip")); } } ```
---
### **关键点说明**
1. **分片命名与存储** - 每个分片以`chunkNumber.part`命名,存储在临时目录(如`uploads/temp/{identifier}/`)。 - 使用唯一标识符(UUID)区分不同文件的上传会话,避免冲突。
2. **合并分片** - 按分片序号排序后,逐个读取分片内容并写入目标文件。 - 合并完成后删除临时文件和目录。
3. **客户端分片逻辑** - 按固定大小(如1MB)读取文件,循环发送HTTP POST请求。 - 使用Apache HttpClient处理多部分表单数据上传。
4. **断点续传(可选)** - 服务端可增加接口检查已上传的分片,客户端据此跳过已传分片。 - 在客户端记录上传进度,支持从中断处继续上传。
---
### **测试与优化**
- **测试分片大小**:根据网络情况调整`CHUNK_SIZE`(如5MB)。 - **异常处理**:增加重试机制和分片校验(如MD5哈希)。 - **清理策略**:定期清理未完成的临时上传目录。
通过上述代码,您可以在Spring Boot应用中实现高效的大文件分片上传功能。 |