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.
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 filesurl : 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.
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.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.
