Master Spring Boot & MinIO Integration: Complete File Upload & Management Guide
This article walks through choosing MinIO for file storage, setting up the environment, integrating MinIO with Spring Boot, and implementing comprehensive file operations—including simple and batch uploads, downloads, previews, large file multipart uploads, and instant upload checks—complete with code examples and testing steps.
Hello, I am Peng Lei.
In a recent project we needed to handle massive file storage and management. After comparing Nginx, FastDFS, Alibaba Cloud OSS and other solutions, we finally chose MinIO.
This article shares the complete process of integrating MinIO with Spring Boot, from why we chose MinIO to implementing various file operations such as simple upload, batch upload, download, preview, large file multipart upload and instant upload.
Why Choose MinIO?
When selecting a file storage solution we consider functionality, performance, cost and scalability. Compared with other common solutions, MinIO offers the following advantages:
1. Rich Features
MinIO supports the standard S3 protocol, allowing seamless integration with other S3‑compatible tools and services. It also provides a rich API for upload, download, preview, delete, version control and other file management needs.
2. Performance
MinIO is designed for high performance, uses a distributed architecture, can scale horizontally and supports petabyte‑level storage. Its read/write performance is especially strong for large files.
3. Open‑Source & Free
MinIO is an open‑source project under the AGPL‑v3 license, allowing enterprises to use it for free, which is a big advantage for small and medium businesses.
4. Easy Installation & Management
MinIO provides a simple dashboard and web UI, making configuration and management very convenient. You can finish setup within minutes.
5. Data Security
MinIO supports data encryption, access control, multi‑factor authentication and other security features to protect data privacy.
Comparison with Other Solutions
Nginx: Mainly for static file serving, does not support storage and large‑scale file management.
FastDFS: Features are relatively simple, lacks a unified management UI and has limited scalability.
Alibaba Cloud OSS: Higher cloud service cost, depends on network conditions and is less suitable for high‑privacy scenarios.
In summary, MinIO is a powerful, high‑performance, easy‑to‑deploy file storage solution suitable for enterprise‑level systems.
Environment Preparation
1. Install MinIO
docker run -p 9000:9000 -p 9001:9001 \
--name minio \
-v /data/minio/data:/data \
-v /data/minio/config:/root/.minio \
-e "MINIO_ROOT_USER=minioadmin" \
-e "MINIO_ROOT_PASSWORD=minioadmin" \
minio/minio server /data --console-address ":9001"After installation, access http://localhost:9001 to open the MinIO console and log in with username minioadmin and password minioadmin.
2. Create Spring Boot Project
Use Spring Initializr to generate a Spring Boot project and add the following dependencies:
Spring Web
Spring Boot Starter (the original text says “龙目岛”, which is a placeholder; replace with the appropriate dependency such as Spring Data JPA if needed)
MinIO Client
Integrating MinIO
1. Add Dependency
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.5</version>
</dependency>2. Configure MinIO Connection
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: test-bucket3. Create MinIO Configuration Class
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MinIOConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}Create MinIO Utility Class
The utility class encapsulates common operations such as bucket existence check, creation, file upload, download, delete, multipart upload, and hash generation.
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.*;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component
public class MinioUtil {
@Autowired
private MinioClient minioClient;
@Value("${minio.bucket-name}")
private String defaultBucketName;
@SneakyThrows
public boolean bucketExists(String bucketName) {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
@SneakyThrows
public void makeBucket(String bucketName) {
if (!bucketExists(bucketName)) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
@SneakyThrows
public List<Bucket> listBuckets() {
return minioClient.listBuckets();
}
@SneakyThrows
public void removeBucket(String bucketName) {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
@SneakyThrows
public Map<String, String> uploadFile(MultipartFile file, String bucketName) {
if (file == null || file.isEmpty()) {
return null;
}
if (!bucketExists(bucketName)) {
makeBucket(bucketName);
}
String originalFilename = file.getOriginalFilename();
String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf('.'));
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.contentType(file.getContentType())
.stream(file.getInputStream(), file.getSize(), -1)
.build());
Map<String, String> result = new HashMap<>();
result.put("fileName", fileName);
result.put("originalFilename", originalFilename);
result.put("url", getObjectUrl(bucketName, fileName, 7));
return result;
}
public Map<String, String> uploadFile(MultipartFile file) {
return uploadFile(file, defaultBucketName);
}
@SneakyThrows
public List<Map<String, String>> uploadFiles(List<MultipartFile> files, String bucketName) {
return files.stream()
.map(f -> uploadFile(f, bucketName))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
public List<Map<String, String>> uploadFiles(List<MultipartFile> files) {
return uploadFiles(files, defaultBucketName);
}
@SneakyThrows
public InputStream downloadFile(String bucketName, String objectName) {
return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
public InputStream downloadFile(String objectName) {
return downloadFile(defaultBucketName, objectName);
}
@SneakyThrows
public void deleteFile(String bucketName, String objectName) {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
public void deleteFile(String objectName) {
deleteFile(defaultBucketName, objectName);
}
@SneakyThrows
public List<DeleteError> deleteFiles(String bucketName, List<String> objectNames) {
List<DeleteObject> objects = objectNames.stream().map(DeleteObject::new).collect(Collectors.toList());
Iterable<Result<DeleteError>> results = minioClient.removeObjects(RemoveObjectsArgs.builder()
.bucket(bucketName)
.objects(objects)
.build());
List<DeleteError> errors = new ArrayList<>();
for (Result<DeleteError> result : results) {
errors.add(result.get());
}
return errors;
}
public List<DeleteError> deleteFiles(List<String> objectNames) {
return deleteFiles(defaultBucketName, objectNames);
}
@SneakyThrows
public String getObjectUrl(String bucketName, String objectName, int expires) {
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(expires, TimeUnit.DAYS)
.build());
}
public String getObjectUrl(String objectName, int expires) {
return getObjectUrl(defaultBucketName, objectName, expires);
}
@SneakyThrows
public boolean objectExists(String bucketName, String objectName) {
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
return true;
} catch (Exception e) {
return false;
}
}
public boolean objectExists(String objectName) {
return objectExists(defaultBucketName, objectName);
}
@SneakyThrows
public List<Item> listObjects(String bucketName) {
Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
List<Item> items = new ArrayList<>();
for (Result<Item> result : results) {
items.add(result.get());
}
return items;
}
public List<Item> listObjects() {
return listObjects(defaultBucketName);
}
@SneakyThrows
public String createMultipartUpload(String bucketName, String objectName) {
CreateMultipartUploadResponse response = minioClient.createMultipartUpload(CreateMultipartUploadArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
return response.result().uploadId();
}
@SneakyThrows
public String uploadPart(String bucketName, String objectName, String uploadId, int partNumber, InputStream stream, long size) {
UploadPartResponse response = minioClient.uploadPart(UploadPartArgs.builder()
.bucket(bucketName)
.object(objectName)
.uploadId(uploadId)
.partNumber(partNumber)
.stream(stream, size, -1)
.build());
return response.etag();
}
@SneakyThrows
public void completeMultipartUpload(String bucketName, String objectName, String uploadId, List<String> etags) {
List<CompletePart> parts = new ArrayList<>();
for (int i = 0; i < etags.size(); i++) {
parts.add(new CompletePart(i + 1, etags.get(i)));
}
minioClient.completeMultipartUpload(CompleteMultipartUploadArgs.builder()
.bucket(bucketName)
.object(objectName)
.uploadId(uploadId)
.parts(parts)
.build());
}
@SneakyThrows
public String generateFileHash(MultipartFile file) {
// Simple hash using size and original name; replace with MD5/SHA‑1 in real projects
return file.getSize() + "-" + file.getOriginalFilename();
}
}Create Controller
The controller exposes REST endpoints for all file operations.
import io.minio.messages.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.*;
@RestController
@RequestMapping("/api/minio")
public class MinioController {
@Autowired
private MinioUtil minioUtil;
@PostMapping("/upload")
public ResponseEntity<Map<String, Object>> uploadFile(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
Map<String, String> info = minioUtil.uploadFile(file);
if (info != null) {
result.put("code", 200);
result.put("message", "Upload successful");
result.put("data", info);
return ResponseEntity.ok(result);
} else {
result.put("code", 500);
result.put("message", "Upload failed");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
} catch (Exception e) {
result.put("code", 500);
result.put("message", "Upload exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@PostMapping("/upload/batch")
public ResponseEntity<Map<String, Object>> uploadFiles(@RequestParam("files") List<MultipartFile> files) {
Map<String, Object> result = new HashMap<>();
try {
List<Map<String, String>> infos = minioUtil.uploadFiles(files);
result.put("code", 200);
result.put("message", "Batch upload successful");
result.put("data", infos);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "Batch upload exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@GetMapping("/download/{fileName}")
public ResponseEntity<byte[]> downloadFile(@PathVariable("fileName") String fileName) {
try {
InputStream in = minioUtil.downloadFile(fileName);
byte[] bytes = in.readAllBytes();
in.close();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", fileName);
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(null, null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/preview/{fileName}")
public ResponseEntity<Map<String, Object>> previewFile(@PathVariable("fileName") String fileName) {
Map<String, Object> result = new HashMap<>();
try {
String url = minioUtil.getObjectUrl(fileName, 1);
result.put("code", 200);
result.put("message", "Preview URL retrieved");
result.put("url", url);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "Preview exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@DeleteMapping("/delete/{fileName}")
public ResponseEntity<Map<String, Object>> deleteFile(@PathVariable("fileName") String fileName) {
Map<String, Object> result = new HashMap<>();
try {
minioUtil.deleteFile(fileName);
result.put("code", 200);
result.put("message", "Delete successful");
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "Delete exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@GetMapping("/list")
public ResponseEntity<Map<String, Object>> listFiles() {
Map<String, Object> result = new HashMap<>();
try {
List<Item> items = minioUtil.listObjects();
result.put("code", 200);
result.put("message", "List retrieved");
result.put("data", items);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "List exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@PostMapping("/multipart/init")
public ResponseEntity<Map<String, Object>> initMultipartUpload(@RequestParam("fileName") String fileName) {
Map<String, Object> result = new HashMap<>();
try {
String uploadId = minioUtil.createMultipartUpload("test-bucket", fileName);
result.put("code", 200);
result.put("message", "Init successful");
result.put("uploadId", uploadId);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "Init exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@PostMapping("/multipart/upload")
public ResponseEntity<Map<String, Object>> uploadPart(@RequestParam("fileName") String fileName,
@RequestParam("uploadId") String uploadId,
@RequestParam("partNumber") int partNumber,
@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
String etag = minioUtil.uploadPart("test-bucket", fileName, uploadId, partNumber, file.getInputStream(), file.getSize());
result.put("code", 200);
result.put("message", "Part uploaded");
result.put("etag", etag);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "Part upload exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@PostMapping("/multipart/complete")
public ResponseEntity<Map<String, Object>> completeMultipartUpload(@RequestParam("fileName") String fileName,
@RequestParam("uploadId") String uploadId,
@RequestParam("etags") List<String> etags) {
Map<String, Object> result = new HashMap<>();
try {
minioUtil.completeMultipartUpload("test-bucket", fileName, uploadId, etags);
result.put("code", 200);
result.put("message", "Multipart upload completed");
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "Complete exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
@PostMapping("/check")
public ResponseEntity<Map<String, Object>> checkFile(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
try {
String hash = minioUtil.generateFileHash(file);
// In a real scenario, query DB/cache to see if the hash exists
boolean exists = false;
result.put("code", 200);
result.put("message", "Check completed");
result.put("exists", exists);
result.put("fileHash", hash);
return ResponseEntity.ok(result);
} catch (Exception e) {
result.put("code", 500);
result.put("message", "Check exception: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
}
}
}Large File Multipart Upload and Instant Upload
1. Large File Multipart Upload
The frontend splits a large file into fixed‑size chunks (e.g., 1 MB), generates a unique identifier for each chunk, and uploads them sequentially. The backend receives each chunk, stores it temporarily, records metadata (file name, chunk index, ETag), and merges all chunks once every part is uploaded.
2. Instant Upload
Before uploading, the frontend calculates a hash (MD5, SHA‑1, etc.) of the file and sends it to the server. The server checks whether a file with the same hash already exists; if it does, the server returns the existing file URL, avoiding a redundant upload.
Testing & Verification
Use tools such as Postman to test each endpoint:
Simple file upload: POST /api/minio/upload Batch upload: POST /api/minio/upload/batch File download: GET /api/minio/download/{fileName} File preview: GET /api/minio/preview/{fileName} Large file multipart upload: use a front‑end uploader (e.g., WebUploader, Plupload) to call the multipart init, upload, and complete APIs.
Instant upload: upload a file, record its hash, then upload the same file again to verify the server returns the existing URL.
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.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
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.
