java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Boot  Minio 文件存储

Spring Boot 整合 Minio 实现高效文件存储解决方案(本地和线上)

作者:FC_nian

Minio 是一个高性能的分布式对象存储系统,专为云原生应用而设计,本文将详细介绍如何在Spring Boot项目中集成Minio实现高效文件存储,实现文件的上传、下载、删除等核心功能,感兴趣的朋友一起看看吧

前言

一、配置

1.配置文件:application.yml

vehicle:
  minio:
    url: http://localhost:9000 # 连接地址,如果是线上的将:localhost->ip
    username: minio # 登录用户名
    password: 12345678 # 登录密码
    bucketName: vehicle # 存储文件的桶的名字

2.配置类: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;
}

3.工具类: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;
/**
 * 文件操作工具类
 */
@RequiredArgsConstructor
@Component
public class MinioUtil {
    private final MinioProperties minioProperties;//配置类
    private MinioClient minioClient;//连接客户端
    private String bucketName;//桶的名字
    // 初始化 Minio 客户端
    @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 初始化失败", e);
        }
    }
    /*
     * 上传文件
     */
    public String uploadFile(MultipartFile file,String extension) {
        if (file == null || file.isEmpty()) {
            throw new RuntimeException("上传文件不能为空");
        }
        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("文件上传失败", e);
        }
    }
    /**
     * 上传已处理的图片字节数组到 MinIO
     *
     * @param imageData 处理后的图片字节数组
     * @param extension 文件扩展名(如 ".jpg", ".png")
     * @param contentType 文件 MIME 类型(如 "image/jpeg", "image/png")
     * @return MinIO 中的文件路径(格式:/bucketName/yyyy-MM-dd/uuid.extension)
     */
    public String uploadFileByte(byte[] imageData, String extension, String contentType) {
        if (imageData == null || imageData.length == 0) {
            throw new RuntimeException("上传的图片数据不能为空");
        }
        if (extension == null || extension.isEmpty()) {
            throw new IllegalArgumentException("文件扩展名不能为空");
        }
        if (contentType == null || contentType.isEmpty()) {
            throw new IllegalArgumentException("文件 MIME 类型不能为空");
        }
        try {
            // 生成唯一文件名
            String uniqueFilename = generateUniqueFilename(extension);
            // 上传到 MinIO
            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("处理后的图片上传失败", e);
        }
    }
    /**
     * 上传本地生成的 Excel 临时文件到 MinIO
     * @param localFile  本地临时文件路径
     * @param extension 扩展名
     * @return MinIO 存储路径,格式:/bucketName/yyyy-MM-dd/targetName
     */
    public String uploadLocalExcel(Path localFile, String extension) {
        if (localFile == null || !Files.exists(localFile)) {
            throw new RuntimeException("本地文件不存在");
        }
        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 上传失败", e);
        }
    }
    /*
     * 根据URL下载文件
     */
    public void downloadFile(HttpServletResponse response, String fileUrl) {
        if (fileUrl == null || !fileUrl.contains(bucketName + "/")) {
            throw new IllegalArgumentException("无效的文件URL");
        }
        try {
            // 从URL中提取对象路径和文件名
            String objectUrl = fileUrl.split(bucketName + "/")[1];
            String fileName = objectUrl.substring(objectUrl.lastIndexOf("/") + 1);
            // 设置响应头
            response.setContentType("application/octet-stream");
            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
            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高效拷贝(内部缓冲区默认8KB)
                IOUtils.copy(inputStream, outputStream);
            }
        } catch (Exception e) {
            throw new RuntimeException("文件下载失败", e);
        }
    }
    /**
     * 根据 MinIO 路径生成带签名的直链
     * @param objectUrl 已存在的 MinIO 路径(/bucketName/...)
     * @param minutes   链接有效期(分钟)
     * @return 可直接访问的 HTTPS 下载地址
     */
    public String parseGetUrl(String objectUrl, int minutes) {
        if (objectUrl == null || !objectUrl.startsWith("/" + bucketName + "/")) {
            throw new IllegalArgumentException("非法的 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("生成直链失败", e);
        }
    }
    /*
     * 根据URL删除文件
     */
    public void deleteFile(String fileUrl) {
        try {
            // 从URL中提取对象路径
            String objectUrl = fileUrl.split(bucketName + "/")[1];
            minioClient.removeObject(RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectUrl)
                    .build());
        } catch (Exception e) {
            throw new RuntimeException("文件删除失败", e);
        }
    }
    /*
     * 检查文件是否存在
     */
    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 | InsufficientDataException | InternalException | InvalidKeyException |
                 InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
                 XmlParserException e) {
            if (e instanceof ErrorResponseException && ((ErrorResponseException) e).errorResponse().code().equals("NoSuchKey")) {
                return false;
            }
            throw new RuntimeException("检查文件存在失败", e);
        }
    }
    /**
     * 生成唯一文件名(带日期路径 + UUID)
     */
    private String generateUniqueFilename(String extension) {
        String dateFormat = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
        String uuid = UUID.randomUUID().toString().replace("-", ""); // 去掉 UUID 中的 "-"
        return dateFormat + "/" + uuid + extension;
    }
}

3.1 初始化方法

3.2 核心功能

方法名功能描述参数说明返回值
uploadFile()上传MultipartFile文件文件对象,扩展名文件路径
uploadFileByte()上传字节数组字节数据,扩展名,MIME类型文件路径
uploadLocalExcel()上传本地Excel文件文件路径,扩展名文件路径
downloadFile()下载文件到响应流HTTP响应对象,文件URL
parseGetUrl()生成带签名直链文件路径,有效期(分钟)直链URL
deleteFile()删除文件文件URL
fileExists()检查文件是否存在文件URL布尔值

3.3 关键技术点

二、使用示例

1.控制器类: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 = "文件")
@RestController
@RequestMapping("/file")
@RequiredArgsConstructor
public class FileController {
    private final FileService fileService;
    @ApiOperation("图片上传")
    @PostMapping("/image")
    public Result<String> imageUpload(MultipartFile file) throws IOException {
        String url = fileService.imageUpload(file);
        return Result.success(url);
    }
    @ApiOperation("图片下载")
    @GetMapping("/image")
    public void imageDownLoad(HttpServletResponse response, String url) throws IOException {
        fileService.imageDownload(response, url);
    }
    @ApiOperation("图片删除")
    @DeleteMapping("/image")
    public Result<Void> imageDelete(String url) {
        fileService.imageDelete(url);
        return Result.success();
    }
}

2.服务类

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);
}

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("文件不存在");
        }
        minioUtil.deleteFile(url);
    }
}

3.效果展示

利用Apifox测试下三个接口

图片上传

图片下载

删除图片

总结

本文通过 “配置 - 工具 - 业务” 三层架构,实现了 Spring Boot 与 MinIO 的集成,核心优势如下:

MinIO 作为轻量级对象存储方案,非常适合中小项目替代本地存储或云厂商 OSS(降低成本)。实际应用中需注意:生产环境需配置 MinIO 集群确保高可用;敏感文件需通过预签名 URL 控制访问权限;定期备份桶数据以防丢失。通过本文的方案,开发者可快速搭建稳定、可扩展的文件存储服务,为应用提供可靠的非结构化数据管理能力。

到此这篇关于Spring Boot 整合 Minio 实现高效文件存储解决方案(本地和线上)的文章就介绍到这了,更多相关Spring Boot Minio 文件存储内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文