最近在项目里搞大文件传输功能,频繁遇到传输中断的糟心事,反复踩坑调试后终于摸索出一套解决方案!这里简单记录分享一下,附上手写代码和时序图。
开发文件上传下载功能时,传输大文件就像开盲盒——要么传着传着突然中断,要么直接弹出文件大小超限的报错。其实这些问题都有迹可循,下面就结合具体场景拆解解决方案。
一、传输中断的“元凶”大盘点
- 服务器设置太“小气”:Spring 默认限制请求体大小,Tomcat 等容器还会自动掐断长时间没动静的连接,就像外卖小哥等太久直接取消订单。
- 网络“掉链子”:WiFi 信号弱、4G 信号差,都可能让传输半路夭折。
- 内存“爆仓”风险:直接把大文件一股脑塞进内存,分分钟触发内存溢出,程序直接“罢工”。
二、核心方案:断点续传+全链路优化
断点续传是解决传输中断的“救命稻草”,就像下载电影暂停后还能接着下。下面用代码和时序图详解实现过程。
1. 服务端分块上传处理
核心代码实现:
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@PostMapping(value = "/chunked", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<?> uploadChunk(
@RequestParam("file") MultipartFile file,
@RequestParam("fileName") String fileName,
@RequestParam("chunkIndex") int chunkIndex,
// 其他参数...
) {
try {
// 保存分块到临时目录
Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "upload");
Files.createDirectories(tempDir);
Path chunkPath = tempDir.resolve(fileName + "." + chunkIndex);
file.transferTo(chunkPath.toFile());
// 记录分块状态到Redis
redisTemplate.opsForHash().put(fileName, "chunk_" + chunkIndex, true);
// 检查是否全部上传完成
if (isAllChunksUploaded(fileName, totalChunks)) {
mergeChunks(fileName, totalChunks, tempDir);
cleanupChunks(fileName, tempDir);
return ResponseEntity.ok().build();
}
return ResponseEntity.accepted().build();
} catch (Exception e) {
log.error("上传分块失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
2. 客户端断点续传逻辑
客户端代码片段:
public class ResumableFileUploader {
public void uploadFile(File file) throws IOException {
// 获取已上传的分块
Set<Integer> uploadedChunks = getUploadedChunks(fileName);
try (FileInputStream fis = new FileInputStream(file)) {
for (int i = 0; i < totalChunks; i++) {
if (uploadedChunks.contains(i)) {
continue;
}
// 创建分块
FileSystem fs = FileSystems.getDefault();
Path tempFilePath = fs.getPath(System.getProperty("java.io.tmpdir"), fileName + "." + i);
try (FileOutputStream fos = new FileOutputStream(tempFilePath.toFile())) {
fis.transferTo(fos);
}
// 上传分块
uploadChunk(fileName, i, totalChunks, fileSize, tempFilePath.toFile());
// 记录上传成功
markChunkAsUploaded(fileName, i);
Files.delete(tempFilePath);
}
}
}
}
三、进阶优化:异步处理+进度监控+安全防护
1. 异步处理:让上传任务“后台运行”
有时候上传大文件会卡住页面,这时就需要把上传任务丢到后台线程处理。
配置与代码实现:
// 启用异步支持
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("upload-");
executor.initialize();
return executor;
}
}
// 异步上传服务
@Service
public class AsyncUploadService {
@Async
public CompletableFuture<Void> processUpload(MultipartFile file, String fileName) {
try {
// 处理大文件上传
Path destination = Paths.get("/data/uploads", fileName);
Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
throw new RuntimeException("上传失败", e);
}
}
}
2. 进度监控:给用户一颗“定心丸”
上传大文件时,显示实时进度条能大大提升体验。我们可以通过自定义输入流来实现(篇幅原因具体的实现,感兴趣的伙伴可以再去搜一下,这里只简单讲述一下思路)。
关键代码:
// 自定义带进度监听的MultipartFile
public class ProgressAwareMultipartFile implements MultipartFile {
private final MultipartFile delegate;
private final ProgressListener listener;
private long bytesRead = 0;
public ProgressAwareMultipartFile(MultipartFile delegate, ProgressListener listener) {
this.delegate = delegate;
this.listener = listener;
}
@Override
public InputStream getInputStream() throws IOException {
return new ProgressAwareInputStream(delegate.getInputStream());
}
private class ProgressAwareInputStream extends FilterInputStream {
protected ProgressAwareInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int b = super.read();
if (b != -1) {
bytesRead++;
listener.onProgress(bytesRead, delegate.getSize());
}
return b;
}
}
// 省略其他方法...
}
// 进度监听器接口
public interface ProgressListener {
void onProgress(long bytesRead, long totalBytes);
}
3. 安全防护:给文件传输加上“防盗门”
- 文件类型校验:只允许上传指定类型的文件,防止恶意上传可执行文件。
- 文件名过滤:避免用户通过特殊文件名进行路径遍历攻击。
- 并发限制:使用令牌桶算法限制同时上传的任务数,防止服务器被“压垮”。
// 示例:文件类型校验
@PostMapping("/upload")
public ResponseEntity<?> uploadFile(
@RequestParam("file") MultipartFile file) {
String[] allowedExtensions = {".pdf", ".docx", ".jpg"};
String originalFilename = file.getOriginalFilename();
boolean isValid = Arrays.stream(allowedExtensions)
.anyMatch(originalFilename::endsWith);
if (!isValid) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("不支持的文件类型");
}
// 其他上传逻辑...
}
四、前端协作:让体验更丝滑
前端用 JavaScript 的 File API 实现分块上传,并把已上传记录存到 localStorage
,即使页面刷新也能接着传。
function uploadFile(file) {
const chunkSize = 1024 * 1024; // 1MB
const totalChunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min((i + 1) * chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
// 其他参数...
fetch('/upload/chunked', {
method: 'POST',
body: formData
})
.then(response => {
if (response.ok) {
localStorage.setItem(`upload_${file.name}_chunk_${i}`, 'done');
console.log(`分块 ${i} 上传成功`);
} else {
console.error(`分块 ${i} 上传失败`);
// 触发重试逻辑
}
})
.catch(error => {
console.error('上传异常:', error);
});
}
}
五、生产环境避坑指南
- 监控告警:实时盯着上传成功率、失败率,一旦异常立刻报警。
- CDN 加速:用 CDN 分担服务器压力,让传输速度起飞。
- 数据备份:定期备份上传文件和元数据,防止数据丢失。
搞定这些方案后,项目里的大文件传输功能稳得一批!如果你在实践中遇到问题,欢迎在评论区留言交流