基于Spring Boot和MinIO实现企业级文件上传工具类

📚 引言

在企业级应用开发中,文件上传下载是一个常见且重要的功能模块。传统的本地文件存储存在诸多问题,如容量限制、单点故障、扩展性差等。MinIO作为一个高性能的分布式对象存储服务,完美兼容Amazon S3 API,成为越来越多开发者的首选。

本文将详细介绍如何在Spring Boot项目中集成MinIO,实现一个功能完善、易于使用的文件上传工具类,帮助开发者快速构建可靠的文件管理功能。

🔧 环境准备

技术栈

  • JDK: 1.8
  • Spring Boot: 2.7.x
  • MinIO Client: 8.5.x
  • 构建工具: Maven

Maven依赖配置

pom.xml中添加以下依赖:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>minio-upload-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>minio-upload-demo</name>
    <description>Spring Boot MinIO文件上传工具类示例</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <minio.version>8.5.7</minio.version>
        <lombok.version>1.18.30</lombok.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starter Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- MinIO Client -->
        <dependency>
            <groupId>io.minio</groupId>
            <artifactId>minio</artifactId>
            <version>${minio.version}</version>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <!-- Apache Commons IO -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.15.1</version>
        </dependency>

        <!-- Spring Boot Configuration Processor -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Spring Boot Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

MinIO服务配置

使用Docker快速启动MinIO

bash 复制代码
docker run -d \
  -p 9000:9000 \
  -p 9001:9001 \
  --name minio \
  -e "MINIO_ROOT_USER=admin" \
  -e "MINIO_ROOT_PASSWORD=admin123456" \
  -v /data/minio/data:/data \
  quay.io/minio/minio server /data --console-address ":9001"

配置文件

application.yml中配置MinIO连接信息:

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: minio-upload-demo

minio:
  # MinIO服务地址
  endpoint: http://localhost:9000
  # 访问密钥
  accessKey: admin
  # 秘钥
  secretKey: admin123456
  # 存储桶名称
  bucketName: file-storage
  # 文件访问地址前缀
  filePrefix: http://localhost:9000/
  # 连接超时时间(毫秒)
  connectTimeout: 10000
  # 写入超时时间(毫秒)
  writeTimeout: 60000
  # 读取超时时间(毫秒)
  readTimeout: 10000

🚀 实现步骤

1. 创建MinIO配置属性类

java 复制代码
package com.example.minio.config;

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

/**
 * MinIO配置属性类
 * 用于读取application.yml中的MinIO相关配置
 * 
 * @author example
 * @since 2024-01-01
 */
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {

    /**
     * MinIO服务地址
     */
    private String endpoint;

    /**
     * 访问密钥
     */
    private String accessKey;

    /**
     * 秘钥
     */
    private String secretKey;

    /**
     * 存储桶名称
     */
    private String bucketName;

    /**
     * 文件访问地址前缀
     */
    private String filePrefix;

    /**
     * 连接超时时间(毫秒)
     */
    private Long connectTimeout = 10000L;

    /**
     * 写入超时时间(毫秒)
     */
    private Long writeTimeout = 60000L;

    /**
     * 读取超时时间(毫秒)
     */
    private Long readTimeout = 10000L;
}

2. 创建MinIO客户端配置类

java 复制代码
package com.example.minio.config;

import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MinIO客户端配置类
 * 用于初始化MinIO客户端并检查存储桶是否存在
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j
@Configuration
public class MinioConfig {

    @Autowired
    private MinioProperties minioProperties;

    /**
     * 创建MinIO客户端Bean
     * 
     * @return MinioClient实例
     */
    @Bean
    public MinioClient minioClient() {
        try {
            log.info("开始初始化MinIO客户端,endpoint: {}", minioProperties.getEndpoint());
            
            MinioClient minioClient = MinioClient.builder()
                    .endpoint(minioProperties.getEndpoint())
                    .credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
                    .build();
            
            log.info("MinIO客户端初始化成功");
            return minioClient;
            
        } catch (Exception e) {
            log.error("MinIO客户端初始化失败", e);
            throw new RuntimeException("MinIO客户端初始化失败: " + e.getMessage());
        }
    }
}

3. 实现MinIO文件上传工具类

java 复制代码
package com.example.minio.utils;

import com.example.minio.config.MinioProperties;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * MinIO文件上传工具类
 * 提供文件上传、下载、删除、查询等功能
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j
@Component
public class MinioUtil {

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MinioProperties minioProperties;

    /**
     * 允许上传的文件扩展名
     */
    private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList(
            "jpg", "jpeg", "png", "gif", "bmp", "webp",
            "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx",
            "txt", "zip", "rar", "7z", "mp4", "mp3", "avi"
    );

    /**
     * 最大文件大小(100MB)
     */
    private static final long MAX_FILE_SIZE = 100 * 1024 * 1024;

    /**
     * 初始化存储桶
     * 在Bean初始化后执行,检查存储桶是否存在,不存在则创建
     */
    @PostConstruct
    public void init() {
        try {
            String bucketName = minioProperties.getBucketName();
            boolean bucketExists = minioClient.bucketExists(
                    BucketExistsArgs.builder()
                            .bucket(bucketName)
                            .build()
            );
            
            if (!bucketExists) {
                log.info("存储桶不存在,开始创建存储桶: {}", bucketName);
                
                minioClient.makeBucket(
                        MakeBucketArgs.builder()
                                .bucket(bucketName)
                                .build()
                );
                
                log.info("存储桶创建成功: {}", bucketName);
            } else {
                log.info("存储桶已存在: {}", bucketName);
            }
            
        } catch (Exception e) {
            log.error("初始化存储桶失败", e);
            throw new RuntimeException("初始化存储桶失败: " + e.getMessage());
        }
    }

    /**
     * 本地文件上传到MinIO
     * 
     * @param filePath 本地文件路径
     * @param objectName 存储在MinIO中的对象名称(文件路径)
     * @return 文件访问URL
     */
    public String uploadLocalFile(String filePath, String objectName) {
        // 参数校验
        if (StringUtils.isBlank(filePath)) {
            throw new IllegalArgumentException("文件路径不能为空");
        }
        if (StringUtils.isBlank(objectName)) {
            throw new IllegalArgumentException("对象名称不能为空");
        }

        File file = new File(filePath);
        if (!file.exists()) {
            throw new IllegalArgumentException("文件不存在: " + filePath);
        }
        if (file.length() > MAX_FILE_SIZE) {
            throw new IllegalArgumentException("文件大小超过限制");
        }

        try (InputStream inputStream = new FileInputStream(file)) {
            return uploadStream(inputStream, file.getName(), objectName);
        } catch (IOException e) {
            log.error("上传本地文件失败: {}", filePath, e);
            throw new RuntimeException("上传本地文件失败: " + e.getMessage());
        }
    }

    /**
     * MultipartFile上传到MinIO
     * 
     * @param file Spring Boot MultipartFile对象
     * @param objectName 存储在MinIO中的对象名称(文件路径)
     * @return 文件访问URL
     */
    public String uploadMultipartFile(MultipartFile file, String objectName) {
        // 参数校验
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }
        if (StringUtils.isBlank(objectName)) {
            throw new IllegalArgumentException("对象名称不能为空");
        }

        // 文件大小校验
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new IllegalArgumentException("文件大小超过限制");
        }

        // 文件扩展名校验
        String originalFilename = file.getOriginalFilename();
        String extension = getFileExtension(originalFilename);
        if (!ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) {
            throw new IllegalArgumentException("不支持的文件类型: " + extension);
        }

        try (InputStream inputStream = file.getInputStream()) {
            return uploadStream(inputStream, originalFilename, objectName);
        } catch (IOException e) {
            log.error("上传MultipartFile失败", e);
            throw new RuntimeException("上传文件失败: " + e.getMessage());
        }
    }

    /**
     * 从远程URL下载文件并上传到MinIO
     * 
     * @param fileUrl 远程文件URL
     * @param objectName 存储在MinIO中的对象名称(文件路径)
     * @return 文件访问URL
     */
    public String uploadRemoteFile(String fileUrl, String objectName) {
        // 参数校验
        if (StringUtils.isBlank(fileUrl)) {
            throw new IllegalArgumentException("文件URL不能为空");
        }
        if (StringUtils.isBlank(objectName)) {
            throw new IllegalArgumentException("对象名称不能为空");
        }

        InputStream inputStream = null;
        String fileName = "downloaded_file";

        try {
            URL url = new URL(fileUrl);
            URLConnection connection = url.openConnection();
            connection.setConnectTimeout(10000);
            connection.setReadTimeout(30000);
            
            inputStream = connection.getInputStream();
            
            // 尝试从URL中获取文件名
            String urlPath = url.getPath();
            if (StringUtils.isNotBlank(urlPath)) {
                int lastSlash = urlPath.lastIndexOf('/');
                if (lastSlash > 0) {
                    fileName = urlPath.substring(lastSlash + 1);
                }
            }
            
            return uploadStream(inputStream, fileName, objectName);
            
        } catch (IOException e) {
            log.error("下载远程文件失败: {}", fileUrl, e);
            throw new RuntimeException("下载远程文件失败: " + e.getMessage());
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    log.warn("关闭输入流失败", e);
                }
            }
        }
    }

    /**
     * 上传文件流到MinIO
     * 
     * @param inputStream 文件输入流
     * @param originalFilename 原始文件名
     * @param objectName 对象名称
     * @return 文件访问URL
     */
    private String uploadStream(InputStream inputStream, String originalFilename, String objectName) {
        try {
            // 获取文件MIME类型
            String contentType = getContentType(originalFilename);
            
            log.info("开始上传文件到MinIO: objectName={}, contentType={}", objectName, contentType);
            
            // 上传文件
            minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(objectName)
                            .stream(inputStream, inputStream.available(), -1)
                            .contentType(contentType)
                            .build()
            );
            
            // 构建文件访问URL
            String fileUrl = minioProperties.getFilePrefix() + 
                           minioProperties.getBucketName() + "/" + objectName;
            
            log.info("文件上传成功: {}", fileUrl);
            return fileUrl;
            
        } catch (Exception e) {
            log.error("上传文件流失败: objectName={}", objectName, e);
            throw new RuntimeException("上传文件失败: " + e.getMessage());
        }
    }

    /**
     * 删除MinIO中的文件
     * 
     * @param objectName 对象名称
     * @return 是否删除成功
     */
    public boolean deleteFile(String objectName) {
        if (StringUtils.isBlank(objectName)) {
            throw new IllegalArgumentException("对象名称不能为空");
        }

        try {
            minioClient.removeObject(
                    RemoveObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(objectName)
                            .build()
            );
            
            log.info("文件删除成功: {}", objectName);
            return true;
            
        } catch (Exception e) {
            log.error("删除文件失败: objectName={}", objectName, e);
            return false;
        }
    }

    /**
     * 批量删除文件
     * 
     * @param objectNames 对象名称列表
     * @return 删除成功的数量
     */
    public int deleteFiles(List<String> objectNames) {
        if (objectNames == null || objectNames.isEmpty()) {
            return 0;
        }

        int successCount = 0;
        
        for (String objectName : objectNames) {
            if (deleteFile(objectName)) {
                successCount++;
            }
        }
        
        return successCount;
    }

    /**
     * 检查文件是否存在
     * 
     * @param objectName 对象名称
     * @return 文件是否存在
     */
    public boolean fileExists(String objectName) {
        if (StringUtils.isBlank(objectName)) {
            return false;
        }

        try {
            minioClient.statObject(
                    StatObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(objectName)
                            .build()
            );
            return true;
        } catch (ErrorResponseException e) {
           if ("NoSuchKey".equals(e.errorResponse().code())) { // 注意:方法名是 errorResponse()
                return false;
            }
            log.error("检查文件存在性失败: objectName={}", objectName, e);
            return false;
        } catch (Exception e) {
            log.error("检查文件存在性失败: objectName={}", objectName, e);
            return false;
        }
    }

    /**
     * 获取文件信息
     * 
     * @param objectName 对象名称
     * @return 文件信息
     */
    public Map<String, Object> getFileInfo(String objectName) {
        if (StringUtils.isBlank(objectName)) {
            throw new IllegalArgumentException("对象名称不能为空");
        }

        try {
            StatObjectResponse stat = minioClient.statObject(
                    StatObjectArgs.builder()
                            .bucket(minioProperties.getBucketName())
                            .object(objectName)
                            .build()
            );
            
            Map<String, Object> fileInfo = new HashMap<>();
            fileInfo.put("objectName", objectName);
            fileInfo.put("size", stat.size());
            fileInfo.put("contentType", stat.contentType());
            fileInfo.put("lastModified", stat.lastModified());
            fileInfo.put("etag", stat.etag());
            
            return fileInfo;
            
        } catch (Exception e) {
            log.error("获取文件信息失败: objectName={}", objectName, e);
            throw new RuntimeException("获取文件信息失败: " + e.getMessage());
        }
    }

    /**
     * 下载文件到本地
     * 
     * @param objectName 对象名称
     * @param localPath 本地保存路径
     * @return 是否下载成功
     */
    public boolean downloadFile(String objectName, String localPath) {
        if (StringUtils.isBlank(objectName)) {
            throw new IllegalArgumentException("对象名称不能为空");
        }
        if (StringUtils.isBlank(localPath)) {
            throw new IllegalArgumentException("本地路径不能为空");
        }

        try (InputStream inputStream = minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(minioProperties.getBucketName())
                        .object(objectName)
                        .build()
        )) {
            
            File file = new File(localPath);
            File parentDir = file.getParentFile();
            if (parentDir != null && !parentDir.exists()) {
                parentDir.mkdirs();
            }
            
            try (FileOutputStream outputStream = new FileOutputStream(file)) {
                IOUtils.copy(inputStream, outputStream);
            }
            
            log.info("文件下载成功: {} -> {}", objectName, localPath);
            return true;
            
        } catch (Exception e) {
            log.error("下载文件失败: objectName={}", objectName, e);
            return false;
        }
    }

    /**
     * 获取文件预览URL(临时访问链接)
     * 
     * @param objectName 对象名称
     * @param expires 过期时间(秒)
     * @return 预览URL
     */
    public String getPresignedUrl(String objectName, int expires) {
        if (StringUtils.isBlank(objectName)) {
            throw new IllegalArgumentException("对象名称不能为空");
        }

        try {
            return minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(minioProperties.getBucketName())
                            .object(objectName)
                            .expiry(expires, TimeUnit.SECONDS)
                            .build()
            );
        } catch (Exception e) {
            log.error("获取预览URL失败: objectName={}", objectName, e);
            throw new RuntimeException("获取预览URL失败: " + e.getMessage());
        }
    }

    /**
     * 获取文件扩展名
     * 
     * @param filename 文件名
     * @return 文件扩展名
     */
    private String getFileExtension(String filename) {
        if (StringUtils.isBlank(filename)) {
            return "";
        }
        int lastDot = filename.lastIndexOf('.');
        if (lastDot > 0 && lastDot < filename.length() - 1) {
            return filename.substring(lastDot + 1);
        }
        return "";
    }

    /**
     * 根据文件名获取MIME类型
     * 
     * @param filename 文件名
     * @return MIME类型
     */
    private String getContentType(String filename) {
        String extension = getFileExtension(filename).toLowerCase();
        
        switch (extension) {
            case "jpg":
            case "jpeg":
                return "image/jpeg";
            case "png":
                return "image/png";
            case "gif":
                return "image/gif";
            case "bmp":
                return "image/bmp";
            case "webp":
                return "image/webp";
            case "pdf":
                return "application/pdf";
            case "doc":
                return "application/msword";
            case "docx":
                return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
            case "xls":
                return "application/vnd.ms-excel";
            case "xlsx":
                return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            case "ppt":
                return "application/vnd.ms-powerpoint";
            case "pptx":
                return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
            case "txt":
                return "text/plain";
            case "zip":
                return "application/zip";
            case "rar":
                return "application/x-rar-compressed";
            case "7z":
                return "application/x-7z-compressed";
            case "mp4":
                return "video/mp4";
            case "mp3":
                return "audio/mpeg";
            case "avi":
                return "video/x-msvideo";
            default:
                return "application/octet-stream";
        }
    }
}

📝 完整代码

上述代码已经实现了完整的MinIO工具类,包含以下核心功能:

  1. 初始化配置:自动创建存储桶
  2. 文件上传:支持本地文件、MultipartFile、远程URL上传
  3. 文件管理:删除、查询、下载功能
  4. 安全控制:文件类型校验、大小限制
  5. 异常处理:完善的异常捕获和日志记录

💡 使用示例

1. 创建文件上传Controller

java 复制代码
package com.example.minio.controller;

import com.example.minio.utils.MinioUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.HashMap;
import java.util.Map;

/**
 * 文件上传Controller
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j
@RestController
@RequestMapping("/api/file")
public class FileUploadController {

    @Autowired
    private MinioUtil minioUtil;

    /**
     * 上传单个文件
     * 
     * @param file 文件
     * @param objectPath 对象路径(可选,不传则自动生成)
     * @return 上传结果
     */
    @PostMapping("/upload")
    public Map<String, Object> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "objectPath", required = false) String objectPath) {
        
        Map<String, Object> result = new HashMap<>();
        
        try {
            // 生成对象名称(如果未指定)
            if (objectPath == null || objectPath.trim().isEmpty()) {
                String originalFilename = file.getOriginalFilename();
                String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
                objectPath = "uploads/" + System.currentTimeMillis() + extension;
            }
            
            // 上传文件
            String fileUrl = minioUtil.uploadMultipartFile(file, objectPath);
            
            result.put("success", true);
            result.put("message", "文件上传成功");
            result.put("data", fileUrl);
            
        } catch (Exception e) {
            log.error("文件上传失败", e);
            result.put("success", false);
            result.put("message", "文件上传失败: " + e.getMessage());
        }
        
        return result;
    }

    /**
     * 从远程URL上传文件
     * 
     * @param fileUrl 远程文件URL
     * @param objectPath 对象路径
     * @return 上传结果
     */
    @PostMapping("/upload-remote")
    public Map<String, Object> uploadRemoteFile(
            @RequestParam("fileUrl") String fileUrl,
            @RequestParam("objectPath") String objectPath) {
        
        Map<String, Object> result = new HashMap<>();
        
        try {
            String uploadUrl = minioUtil.uploadRemoteFile(fileUrl, objectPath);
            
            result.put("success", true);
            result.put("message", "远程文件上传成功");
            result.put("data", uploadUrl);
            
        } catch (Exception e) {
            log.error("远程文件上传失败", e);
            result.put("success", false);
            result.put("message", "远程文件上传失败: " + e.getMessage());
        }
        
        return result;
    }

    /**
     * 删除文件
     * 
     * @param objectName 对象名称
     * @return 删除结果
     */
    @DeleteMapping("/delete")
    public Map<String, Object> deleteFile(@RequestParam("objectName") String objectName) {
        
        Map<String, Object> result = new HashMap<>();
        
        boolean deleted = minioUtil.deleteFile(objectName);
        
        result.put("success", deleted);
        result.put("message", deleted ? "文件删除成功" : "文件删除失败");
        
        return result;
    }

    /**
     * 获取文件信息
     * 
     * @param objectName 对象名称
     * @return 文件信息
     */
    @GetMapping("/info")
    public Map<String, Object> getFileInfo(@RequestParam("objectName") String objectName) {
        
        Map<String, Object> result = new HashMap<>();
        
        try {
            Map<String, Object> fileInfo = minioUtil.getFileInfo(objectName);
            
            result.put("success", true);
            result.put("data", fileInfo);
            
        } catch (Exception e) {
            log.error("获取文件信息失败", e);
            result.put("success", false);
            result.put("message", "获取文件信息失败: " + e.getMessage());
        }
        
        return result;
    }
}

2. 在Service层使用

java 复制代码
package com.example.minio.service;

import com.example.minio.utils.MinioUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

/**
 * 文件服务类
 * 
 * @author example
 * @since 2024-01-01
 */
@Slf4j
@Service
public class FileService {

    @Autowired
    private MinioUtil minioUtil;

    /**
     * 保存用户头像
     */
    public String saveUserAvatar(String userId, MultipartFile avatarFile) {
        // 生成对象名称:avatars/userId_timestamp.jpg
        String originalFilename = avatarFile.getOriginalFilename();
        String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
        String objectName = "avatars/" + userId + "_" + System.currentTimeMillis() + extension;
        
        // 上传文件
        String avatarUrl = minioUtil.uploadMultipartFile(avatarFile, objectName);
        
        log.info("用户头像保存成功: userId={}, url={}", userId, avatarUrl);
        return avatarUrl;
    }

    /**
     * 保存文档文件
     */
    public String saveDocument(String categoryId, MultipartFile documentFile) {
        // 生成对象名称:documents/categoryId_timestamp.pdf
        String originalFilename = documentFile.getOriginalFilename();
        String objectName = "documents/" + categoryId + "/" + System.currentTimeMillis() + "_" + originalFilename;
        
        // 上传文件
        String documentUrl = minioUtil.uploadMultipartFile(documentFile, objectName);
        
        log.info("文档保存成功: categoryId={}, url={}", categoryId, documentUrl);
        return documentUrl;
    }
}

⚠️ 注意事项

1. 安全配置

  • 凭证安全:生产环境中,MinIO的accessKey和secretKey应使用配置中心或环境变量管理,切勿硬编码在代码中
  • HTTPS配置:生产环境建议使用HTTPS协议传输,确保数据安全
  • 访问控制:合理配置存储桶的访问策略,避免敏感数据泄露

2. 性能优化

  • 分片上传:对于大文件(>100MB),建议使用MinIO的分片上传功能
  • 连接池配置:合理配置HTTP连接池参数,提高并发性能
  • CDN加速:对于频繁访问的文件,建议配置CDN加速

3. 异常处理

  • 网络异常:处理网络超时、连接中断等异常情况
  • 存储空间不足:监控存储空间,及时扩容
  • 并发控制:对于高并发场景,注意线程安全和限流控制

4. 常见问题解决

Q1: 连接超时如何处理?

yaml 复制代码
minio:
  connectTimeout: 30000  # 增加连接超时时间
  readTimeout: 60000     # 增加读取超时时间
  writeTimeout: 60000    # 增加写入超时时间

Q2: 如何实现文件访问权限控制?

可以通过MinIO的Bucket Policy功能精细控制访问权限:

java 复制代码
public void setBucketPolicy() throws Exception {
    String policy = "{\n" +
            "    \"Version\": \"2012-10-17\",\n" +
            "    \"Statement\": [\n" +
            "        {\n" +
            "            \"Effect\": \"Allow\",\n" +
            "            \"Principal\": {\n" +
            "                \"AWS\": \"*\"\n" +
            "            },\n" +
            "            \"Action\": \"s3:GetObject\",\n" +
            "            \"Resource\": \"arn:aws:s3:::file-storage/*\"\n" +
            "        }\n" +
            "    ]\n" +
            "}";
    
    minioClient.setBucketPolicy(
            SetBucketPolicyArgs.builder()
                    .bucket(minioProperties.getBucketName())
                    .config(policy)
                    .build()
    );
}

Q3: 如何实现文件版本控制?

MinIO支持对象版本控制,可以在创建存储桶时启用:

java 复制代码
minioClient.setBucketVersioning(
    SetBucketVersioningArgs.builder()
        .bucket(bucketName)
        .config(new VersioningConfiguration(VersioningConfiguration.Status.ENABLED.toString(), false))
        .build()
);

🎯 总结

本文详细介绍了基于Spring Boot和MinIO实现企业级文件上传工具类的完整方案,涵盖了环境搭建、核心功能实现、使用示例和注意事项。通过这套工具类,开发者可以快速实现文件的存储、管理和访问功能,为项目提供可靠的对象存储支持。

核心优势:

  • ✅ 功能完善:支持多种文件上传方式
  • ✅ 安全可靠:完善的参数校验和异常处理
  • ✅ 易于扩展:代码结构清晰,便于二次开发
  • ✅ 生产可用:经过充分的测试和优化
相关推荐
木辰風7 小时前
PLSQL自定义自动替换(AutoReplace)
java·数据库·sql
heartbeat..7 小时前
Redis 中的锁:核心实现、类型与最佳实践
java·数据库·redis·缓存·并发
8 小时前
java关于内部类
java·开发语言
好好沉淀8 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
gusijin8 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder8 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
吨~吨~吨~8 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟8 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日8 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水8 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展