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.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Master Spring Boot & MinIO Integration: Complete File Upload & Management Guide

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-bucket

3. 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaSpring BootMiniofile storage
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.