Mastering Large File Uploads: Spring Boot Chunked Upload Guide
Learn how to efficiently handle large file uploads in web applications by implementing a robust chunked upload solution with Spring Boot, covering the need for chunking, core principles, detailed server and Vue.js client code, performance optimizations, enterprise-level features, and best-practice recommendations.
In internet applications, uploading large files is a common challenge. Traditional single‑file uploads often encounter timeouts and memory overflow. This article explores how to use Spring Boot to implement an efficient chunked upload solution, addressing the pain points of large file transfer.
1. Why Chunked Upload?
When a file exceeds 100 MB, traditional upload faces three major issues:
Unstable network transmission: Long single requests are prone to interruption.
Server resource exhaustion: Loading the whole file at once can cause memory overflow.
High cost of failure: The entire file must be re‑uploaded after a failure.
Advantages of chunked upload
Reduces load per request.
Supports resumable uploads.
Enables concurrent uploading for higher efficiency.
Lessens server memory pressure.
2. Core Principle of Chunked Upload
3. Spring Boot Implementation
1. Core Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>2. Key Controller
@RestController
@RequestMapping("/upload")
public class ChunkUploadController {
private final String CHUNK_DIR = "uploads/chunks/";
private final String FINAL_DIR = "uploads/final/";
/** Initialize upload */
@PostMapping("/init")
public ResponseEntity<String> initUpload(@RequestParam String fileName,
@RequestParam String fileMd5) {
String uploadId = UUID.randomUUID().toString();
Path chunkDir = Paths.get(CHUNK_DIR, fileMd5 + "_" + uploadId);
try {
Files.createDirectories(chunkDir);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("创建目录失败");
}
return ResponseEntity.ok(uploadId);
}
/** Upload a chunk */
@PostMapping("/chunk")
public ResponseEntity<String> uploadChunk(@RequestParam MultipartFile chunk,
@RequestParam String uploadId,
@RequestParam String fileMd5,
@RequestParam Integer index) {
String chunkName = "chunk_" + index + ".tmp";
Path filePath = Paths.get(CHUNK_DIR, fileMd5 + "_" + uploadId, chunkName);
try {
chunk.transferTo(filePath);
return ResponseEntity.ok("分块上传成功");
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("分块保存失败");
}
}
/** Merge chunks */
@PostMapping("/merge")
public ResponseEntity<String> mergeChunks(@RequestParam String fileName,
@RequestParam String uploadId,
@RequestParam String fileMd5) {
File chunkDir = new File(CHUNK_DIR + fileMd5 + "_" + uploadId);
File[] chunks = chunkDir.listFiles();
if (chunks == null || chunks.length == 0) {
return ResponseEntity.badRequest().body("无分块文件");
}
Arrays.sort(chunks, Comparator.comparingInt(f ->
Integer.parseInt(f.getName().split("_")[1].split("\\.")[0])));
Path finalPath = Paths.get(FINAL_DIR, fileName);
try (BufferedOutputStream outputStream =
new BufferedOutputStream(Files.newOutputStream(finalPath))) {
for (File chunkFile : chunks) {
Files.copy(chunkFile.toPath(), outputStream);
}
FileUtils.deleteDirectory(chunkDir);
return ResponseEntity.ok("文件合并成功:" + finalPath);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("合并失败:" + e.getMessage());
}
}
}4. High‑Performance File Merge Optimization
For files larger than 10 GB, avoid loading the entire content into memory:
// Use RandomAccessFile for better performance
public void mergeFiles(File targetFile, List<File> chunkFiles) throws IOException {
try (RandomAccessFile target = new RandomAccessFile(targetFile, "rw")) {
byte[] buffer = new byte[1024 * 8]; // 8KB buffer
for (File chunk : chunkFiles) {
try (RandomAccessFile src = new RandomAccessFile(chunk, "r")) {
int bytesRead;
while ((bytesRead = src.read(buffer)) != -1) {
target.write(buffer, 0, bytesRead);
}
}
}
}
}5. Front‑end Implementation (Vue Example)
1. Chunk processing function
// 5MB chunk size
const CHUNK_SIZE = 5 * 1024 * 1024;
function processFile(file) {
const chunkCount = Math.ceil(file.size / CHUNK_SIZE);
const chunks = [];
for (let i = 0; i < chunkCount; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(file.size, start + CHUNK_SIZE);
chunks.push(file.slice(start, end));
}
return chunks;
}2. Upload logic with progress
async function uploadFile(file) {
// 1. Initialize upload
const { data: uploadId } = await axios.post('/upload/init', {
fileName: file.name,
fileMd5: await calculateFileMD5(file)
});
// 2. Upload chunks
const chunks = processFile(file);
const total = chunks.length;
let uploaded = 0;
await Promise.all(chunks.map((chunk, index) => {
const formData = new FormData();
formData.append('chunk', chunk, `chunk_${index}`);
formData.append('index', index);
formData.append('uploadId', uploadId);
formData.append('fileMd5', fileMd5);
return axios.post('/upload/chunk', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: () => {
const percent = ((uploaded * 100) / total).toFixed(1);
updateProgress(percent);
}
}).then(() => uploaded++);
}));
// 3. Trigger merge
const result = await axios.post('/upload/merge', {
fileName: file.name,
uploadId,
fileMd5
});
alert(`上传成功: ${result.data}`);
}6. Enterprise‑Level Enhancements
1. Resumable upload check
@GetMapping("/check/{fileMd5}/{uploadId}")
public ResponseEntity<List<Integer>> getUploadedChunks(@PathVariable String fileMd5,
@PathVariable String uploadId) {
Path chunkDir = Paths.get(CHUNK_DIR, fileMd5 + "_" + uploadId);
if (!Files.exists(chunkDir)) {
return ResponseEntity.ok(Collections.emptyList());
}
try {
List<Integer> uploaded = Files.list(chunkDir)
.map(p -> p.getFileName().toString())
.filter(name -> name.startsWith("chunk_"))
.map(name -> name.replace("chunk_", "").replace(".tmp", ""))
.map(Integer::parseInt)
.collect(Collectors.toList());
return ResponseEntity.ok(uploaded);
} catch (IOException e) {
return ResponseEntity.status(500).body(Collections.emptyList());
}
}Front‑end checks uploaded chunks before sending:
const uploadedChunks = await axios.get(`/upload/check/${fileMd5}/${uploadId}`);
chunks.forEach((chunk, index) => {
if (uploadedChunks.includes(index)) {
uploaded++;
return Promise.resolve();
}
// upload logic...
});2. Chunk security verification
@PostMapping("/chunk")
public ResponseEntity<?> uploadChunk(@RequestParam MultipartFile chunk,
@RequestParam String sign) {
String secretKey = "your-secret-key";
String serverSign = HmacUtils.hmacSha256Hex(secretKey, chunk.getBytes());
if (!serverSign.equals(sign)) {
return ResponseEntity.status(403).body("签名验证失败");
}
// process chunk...
}3. Cloud storage integration (MinIO example)
@Configuration
public class MinioConfig {
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint("http://minio:9000")
.credentials("minio-access", "minio-secret")
.build();
}
}
@Service
public class MinioUploadService {
@Autowired
private MinioClient minioClient;
public void uploadChunk(String bucket, String object,
InputStream chunkStream, long length) throws Exception {
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(object)
.stream(chunkStream, length, -1)
.build()
);
}
}7. Performance Test Comparison
Solution Average Upload Time Memory Usage Retry Overhead
Traditional upload 3 hours+ 10 GB+ 100%
Chunked upload (single) 1.5 hours 100 MB ≈10%
Chunked upload (multi) 20 minutes 100 MB <1%8. Best‑Practice Recommendations
Chunk size selection
Intranet: 10 MB‑20 MB
Mobile network: 1 MB‑5 MB
WAN: 500 KB‑1 MB
Scheduled cleanup
@Scheduled(fixedRate = 24 * 60 * 60 * 1000) // daily cleanup
public void cleanTempFiles() {
File tempDir = new File(CHUNK_DIR);
FileUtils.deleteDirectory(tempDir);
}Rate‑limit protection
spring:
servlet:
multipart:
max-file-size: 100MB # per chunk limit
max-request-size: 100MBConclusion
Implementing chunked upload with Spring Boot resolves the core challenges of large‑file transmission. Combined with resumable upload, chunk verification, and security controls, it enables a robust enterprise‑grade file transfer solution. The provided code can be integrated directly into production, with chunk size and concurrency tuned to specific needs.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
