Seamlessly Integrate MinIO Object Storage with Spring Boot – Full Guide

This article provides a step‑by‑step tutorial on configuring Spring Boot to work with MinIO, covering YAML settings, property binding, utility classes for file upload/download, controller and service implementations, key technical points, and best‑practice recommendations for scalable backend file storage.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Seamlessly Integrate MinIO Object Storage with Spring Boot – Full Guide

Introduction

MinIO is a high‑performance distributed object storage system designed for cloud‑native applications.

It is an Amazon S3 compatible alternative that offers simple APIs for massive unstructured data.

In micro‑service architectures, file storage is a common requirement; MinIO’s lightweight, highly available, and easy‑to‑deploy characteristics make it an ideal choice.

1. Configuration

1.1 application.yml

vehicle:
  minio:
    url: http://localhost:9000 # connection address, replace localhost with IP in production
    username: minio # login username
    password: 12345678 # login password
    bucketName: vehicle # bucket name for storing files

url : MinIO server address; replace with actual IP or domain in production.

username/password : credentials for the MinIO console.

bucketName : name of the storage bucket, similar to a folder.

HTTPS note : when using a domain, the URL must be https://your.domain.name:9090.

1.2 Configuration class: MinioProperties

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@ConfigurationProperties(prefix = "vehicle.minio")
public class MinioProperties {
    private String url;
    private String username;
    private String password;
    private String bucketName;
}
@ConfigurationProperties

: binds properties from the YAML file to the class fields. @Component: registers the class as a Spring‑managed bean.

Provides all parameters required to connect to MinIO.

1.3 Utility class: MinioUtil

import cn.hutool.core.lang.UUID;
import com.fc.properties.MinioProperties;
import io.minio.*;
import io.minio.errors.*;
import lombok.RequiredArgsConstructor;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import io.minio.http.Method;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;

/**
 * File operation utility class
 */
@RequiredArgsConstructor
@Component
public class MinioUtil {
    private final MinioProperties minioProperties; // configuration class
    private MinioClient minioClient; // client instance
    private String bucketName;

    @PostConstruct
    public void init() {
        try {
            minioClient = MinioClient.builder()
                    .endpoint(minioProperties.getUrl())
                    .credentials(minioProperties.getUsername(), minioProperties.getPassword())
                    .build();
            bucketName = minioProperties.getBucketName();
            boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!bucketExists) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
        } catch (Exception e) {
            throw new RuntimeException("Minio initialization failed", e);
        }
    }

    /** Upload file */
    public String uploadFile(MultipartFile file, String extension) {
        if (file == null || file.isEmpty()) {
            throw new RuntimeException("Uploaded file cannot be null");
        }
        try {
            String uniqueFilename = generateUniqueFilename(extension);
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(uniqueFilename)
                    .stream(file.getInputStream(), file.getSize(), -1)
                    .contentType(file.getContentType())
                    .build());
            return "/" + bucketName + "/" + uniqueFilename;
        } catch (Exception e) {
            throw new RuntimeException("File upload failed", e);
        }
    }

    /** Upload processed image bytes */
    public String uploadFileByte(byte[] imageData, String extension, String contentType) {
        if (imageData == null || imageData.length == 0) {
            throw new RuntimeException("Uploaded image data cannot be null");
        }
        if (extension == null || extension.isEmpty()) {
            throw new IllegalArgumentException("File extension cannot be empty");
        }
        if (contentType == null || contentType.isEmpty()) {
            throw new IllegalArgumentException("MIME type cannot be empty");
        }
        try {
            String uniqueFilename = generateUniqueFilename(extension);
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(uniqueFilename)
                    .stream(new ByteArrayInputStream(imageData), imageData.length, -1)
                    .contentType(contentType)
                    .build());
            return "/" + bucketName + "/" + uniqueFilename;
        } catch (Exception e) {
            throw new RuntimeException("Processed image upload failed", e);
        }
    }

    /** Upload local Excel file */
    public String uploadLocalExcel(Path localFile, String extension) {
        if (localFile == null || !Files.exists(localFile)) {
            throw new RuntimeException("Local file does not exist");
        }
        try (InputStream in = Files.newInputStream(localFile)) {
            String objectKey = generateUniqueFilename(extension);
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectKey)
                    .stream(in, Files.size(localFile), -1)
                    .contentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
                    .build());
            return "/" + bucketName + "/" + objectKey;
        } catch (Exception e) {
            throw new RuntimeException("Excel upload failed", e);
        }
    }

    /** Download file by URL */
    public void downloadFile(HttpServletResponse response, String fileUrl) {
        if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {
            throw new IllegalArgumentException("Invalid file URL");
        }
        try {
            String objectUrl = fileUrl.split(bucketName + "/")[1];
            String fileName = objectUrl.substring(objectUrl.lastIndexOf('/') + 1);
            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
            try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectUrl)
                    .build());
                 OutputStream outputStream = response.getOutputStream()) {
                IOUtils.copy(inputStream, outputStream);
            }
        } catch (Exception e) {
            throw new RuntimeException("File download failed", e);
        }
    }

    /** Generate presigned URL */
    public String parseGetUrl(String objectUrl, int minutes) {
        if (objectUrl == null || !objectUrl.startsWith("/" + bucketName + "/")) {
            throw new IllegalArgumentException("Invalid objectUrl");
        }
        String objectKey = objectUrl.substring(("/" + bucketName + "/").length());
        try {
            return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(bucketName)
                    .object(objectKey)
                    .expiry(minutes, TimeUnit.MINUTES)
                    .build());
        } catch (Exception e) {
            throw new RuntimeException("Presigned URL generation failed", e);
        }
    }

    /** Delete file by URL */
    public void deleteFile(String fileUrl) {
        try {
            String objectUrl = fileUrl.split(bucketName + "/")[1];
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectUrl)
                    .build());
        } catch (Exception e) {
            throw new RuntimeException("File deletion failed", e);
        }
    }

    /** Check if file exists */
    public boolean fileExists(String fileUrl) {
        if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {
            return false;
        }
        try {
            String objectUrl = fileUrl.split(bucketName + "/")[1];
            minioClient.statObject(StatObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectUrl)
                    .build());
            return true;
        } catch (ErrorResponseException e) {
            if (e.errorResponse().code().equals("NoSuchKey")) {
                return false;
            }
            throw new RuntimeException("File existence check failed", e);
        } catch (Exception e) {
            throw new RuntimeException("File existence check failed", e);
        }
    }

    /** Generate unique filename (date/UUID + extension) */
    private String generateUniqueFilename(String extension) {
        String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return date + "/" + uuid + extension;
    }
}

Key Technical Points

Unique filename generation : date directory/UUID.extension format prevents name collisions.

Large‑file streaming : uses MinIO streaming APIs to avoid memory overflow.

Response header encoding : solves Chinese filename garbling during download.

Unified exception handling : converts MinIO‑specific exceptions into runtime exceptions.

Presigned URL : creates temporary HTTPS links for direct access.

2. Usage Example

2.1 Controller: FileController

import com.fc.result.Result;
import com.fc.service.FileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Api(tags = "File")
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {
    private final FileService fileService;

    @ApiOperation("Image upload")
    @PostMapping("/image")
    public Result<String> imageUpload(MultipartFile file) throws IOException {
        String url = fileService.imageUpload(file);
        return Result.success(url);
    }

    @ApiOperation("Image download")
    @GetMapping("/image")
    public void imageDownLoad(HttpServletResponse response, String url) throws IOException {
        fileService.imageDownload(response, url);
    }

    @ApiOperation("Image delete")
    @DeleteMapping("/image")
    public Result<Void> imageDelete(String url) {
        fileService.imageDelete(url);
        return Result.success();
    }
}

2.2 Service Interface: FileService

import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface FileService {
    String imageUpload(MultipartFile file) throws IOException;
    void imageDownload(HttpServletResponse response, String url) throws IOException;
    void imageDelete(String url);
}

2.3 Service Implementation: FileServiceImpl

import com.fc.exception.FileException;
import com.fc.service.FileService;
import com.fc.utils.ImageUtil;
import com.fc.utils.MinioUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Service
@RequiredArgsConstructor
public class FileServiceImpl implements FileService {
    private final MinioUtil minioUtil;

    @Override
    public String imageUpload(MultipartFile file) throws IOException {
        byte[] bytes = ImageUtil.compressImage(file, "JPEG");
        return minioUtil.uploadFileByte(bytes, ".jpeg", "image/jpeg");
    }

    @Override
    public void imageDownload(HttpServletResponse response, String url) throws IOException {
        minioUtil.downloadFile(response, url);
    }

    @Override
    public void imageDelete(String url) {
        if (!minioUtil.fileExists(url)) {
            throw new FileException("File does not exist");
        }
        minioUtil.deleteFile(url);
    }
}

3. Summary

The tutorial demonstrates a three‑layer architecture—configuration, utility, and business—to integrate Spring Boot with MinIO. It offers high usability through property binding and helper methods, flexibility by supporting multipart files, byte arrays, and local files, and extensibility for features such as permission policies, multipart uploads, and periodic cleanup. MinIO serves as a lightweight, cost‑effective object storage solution suitable for small‑to‑medium projects, while production deployments should consider clustering, signed URLs for sensitive data, and regular bucket backups.

Core functionality diagram
Core functionality diagram
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 Bootfile uploadMinioobject storage
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.