依星源码资源网,依星资源网

 找回密码
 立即注册

QQ登录

只需一步,快速开始

【好消息,好消息,好消息】VIP会员可以发表文章赚积分啦 !
查看: 5|回复: 0

Spring Boot大文件分片上传实现

[复制链接] 主动推送

1万

主题

1万

帖子

1万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
14135
发表于 4 小时前 | 显示全部楼层 |阅读模式
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应用中实现高效的大文件分片上传功能。

相关帖子

扫码关注微信公众号,及时获取最新资源信息!下载附件优惠VIP会员6折;永久VIP4折
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

免责声明:
1、本站提供的所有资源仅供参考学习使用,版权归原著所有,禁止下载本站资源参与商业和非法行为,请在24小时之内自行删除!
2、本站所有内容均由互联网收集整理、网友上传,并且以计算机技术研究交流为目的,仅供大家参考、学习,请勿任何商业目的与商业用途。
3、若您需要商业运营或用于其他商业活动,请您购买正版授权并合法使用。
4、论坛的所有内容都不保证其准确性,完整性,有效性,由于源码具有复制性,一经售出,概不退换。阅读本站内容因误导等因素而造成的损失本站不承担连带责任。
5、用户使用本网站必须遵守适用的法律法规,对于用户违法使用本站非法运营而引起的一切责任,由用户自行承担
6、本站所有资源来自互联网转载,版权归原著所有,用户访问和使用本站的条件是必须接受本站“免责声明”,如果不遵守,请勿访问或使用本网站
7、本站使用者因为违反本声明的规定而触犯中华人民共和国法律的,一切后果自己负责,本站不承担任何责任。
8、凡以任何方式登陆本网站或直接、间接使用本网站资料者,视为自愿接受本网站声明的约束。
9、本站以《2013 中华人民共和国计算机软件保护条例》第二章 “软件著作权” 第十七条为原则:为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。若有学员需要商用本站资源,请务必联系版权方购买正版授权!
10、本网站如无意中侵犯了某个企业或个人的知识产权,请来信【站长信箱312337667@qq.com】告之,本站将立即删除。
郑重声明:
本站所有资源仅供用户本地电脑学习源代码的内含设计思想和原理,禁止任何其他用途!
本站所有资源、教程来自互联网转载,仅供学习交流,不得商业运营资源,不确保资源完整性,图片和资源仅供参考,不提供任何技术服务。
本站资源仅供本地编辑研究学习参考,禁止未经资源商正版授权参与任何商业行为,违法行为!如需商业请购买各资源商正版授权
本站仅收集资源,提供用户自学研究使用,本站不存在私自接受协助用户架设游戏或资源,非法运营资源行为。
 
在线客服
点击这里给我发消息 点击这里给我发消息 点击这里给我发消息
售前咨询热线
312337667

微信扫一扫,私享最新原创实用干货

QQ|免责声明|小黑屋|依星资源网 ( 鲁ICP备2021043233号-3 )|网站地图

GMT+8, 2025-3-12 22:07

Powered by Net188.com X3.4

邮箱:312337667@qq.com 客服QQ:312337667(工作时间:9:00~21:00)

快速回复 返回顶部 返回列表