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

核心优势:

  • ✅ 功能完善:支持多种文件上传方式
  • ✅ 安全可靠:完善的参数校验和异常处理
  • ✅ 易于扩展:代码结构清晰,便于二次开发
  • ✅ 生产可用:经过充分的测试和优化
相关推荐
毕设源码-邱学长18 小时前
【开题答辩全过程】以 胡小楼行政村农用灌溉机井预约管理系统的设计与实现为例,包含答辩的问题和答案
java·eclipse
JTCC18 小时前
Java 设计模式西游篇 - 第五回:装饰者模式添法力 悟空披挂新战袍
java·观察者模式·设计模式
智能工业品检测-奇妙智能18 小时前
docker如何进行离线部署springboot项目
spring boot·docker·容器
Soofjan18 小时前
Go Map SwissTable GetMap 查找流程(源码笔记 3)
后端
xiaoye370818 小时前
哪些因素会影响Spring Bean的线程安全?
java·spring
Soofjan18 小时前
Go Map SwissTable ModMap 插入与更新(源码笔记 4)
后端
荔枝要好学18 小时前
一个jar包通过java -jar 指令找不到启动类,那么我是否可以通过java -cp命令指定启动类的方式启动?
java
java1234_小锋19 小时前
Java高频面试题:Mysql里where1=1会不会影响性能?
java·开发语言
qq_124987075319 小时前
基于springboot的微信小程序的博物馆文创系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·spring·微信小程序·毕业设计·计算机毕设
krack716x19 小时前
第1天:面向对象与基础语法
java·开发语言