springboot3.0 集成minio上传文件,支持多个桶名

1. 依赖引入

这里只引入minio 的依赖,springboot的省略

XML 复制代码
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.10</version>
</dependency>

2. 配置文件

springboot配置文件

XML 复制代码
minio:
  access-key: admin
  secret-key: admin123
  endpoint: http://192.168.197.148:9000
  enabled: true
  buckets:
    banner:
      name: banner-bucket
      autoCreate: true
  pool-config:
    max-total: 200
    max-per-route: 20
    connection-timeout: 3000
    read-timeout: 60000
    write-timeout: 60000
    keep-alive-duration: 60
  router: /wengen

3. 配置类

读取配置文件类

java 复制代码
/**
 * minio配置类
 */
@Data
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {

    /**
     * 是否启用MinIO功能
     */
    private boolean enabled = true;

    /**
     * MinIO服务端点 (必须)
     */
    private String endpoint;

    /**
     * MinIO访问密钥 (必须)
     */
    private String accessKey;

    /**
     * MinIO秘密密钥 (必须)
     */
    private String secretKey;


    /**
     * 全局区域设置 (可选)
     */
    private String region = "";
    /**
     * 路由文根
     */
    private String router = "";

    /**
     * 桶配置集合 (必须至少配置一个)
     */
    private Map<String, BucketConfig> buckets;


    /**
     * 连接池
     */
    private PoolConfig poolConfig;
}
/**
 * 桶配置
 * @author huat
 **/
@Data
public class BucketConfig {

    /**
     * 桶名称 (必须)
     */
    private String name;

    /**
     * 是否自动创建不存在的桶
     */
    private boolean autoCreate = true;

    /**
     * 桶特定区域 (可选, 优先于全局区域)
     */
    private String region;
}

/**
 * MinIO自动配置类
 * 企业级生产环境注意事项:
 * 1. 仅在MinioClient类存在时生效
 * 2. 提供缺失bean的条件创建
 * 3. 遵循Spring Boot自动配置规范
 */
@AutoConfiguration
@ConditionalOnClass(MinioClient.class)
@EnableConfigurationProperties(MinioProperties.class)
public class MinioAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MinioClientFactory minioClientFactory(MinioProperties properties) {
        return new MinioClientFactory(properties);
    }

    @Bean
    @ConditionalOnMissingBean
    public MinioService minioService(MinioClientFactory clientFactory) {
        return new MinioServiceImpl(clientFactory);
    }
}
/**
 * @author huat
 **/
@Data
public class PoolConfig {
    /**
     * 最大连接总数(默认100)
     */
    private int maxTotal = 100;

    /**
     * 每个路由的最大连接数(默认10)
     */
    private int maxPerRoute = 10;

    /**
     * 连接超时时间(毫秒)(默认5秒)
     */
    private int connectionTimeout;

    /**
     * 读取超时时间(毫秒)(默认30秒)
     */
    private int readTimeout;

    /**
     * 写入超时时间(毫秒)(默认30秒)
     */
    private int writeTimeout;

    /**
     * 连接保持活跃时间(秒)(默认30秒)
     */
    private int keepAliveDuration = 30;

}

4. client配置

java 复制代码
/**
 * MinIO客户端工厂 - 线程安全的多桶客户端管理
 * 企业级生产环境注意事项:
 * 1. 使用ConcurrentHashMap保证线程安全
 * 2. 客户端实例复用,避免频繁创建连接
 * 3. 优雅关闭所有客户端连接
 * 4. 详细的错误日志记录
 */
public class MinioClientFactory {
    @Getter
    private final MinioProperties properties;
    private final Map<String, MinioClient> clientMap = new ConcurrentHashMap<>();
    private final OkHttpClient sharedHttpClient; // 共享的连接池实例

    public MinioClientFactory(MinioProperties properties) {
        this.properties = properties;
        this.sharedHttpClient = createPooledHttpClient();
    }

    /**
     * 获取指定桶的客户端实例(线程安全,自动创建并缓存)
     */
    public MinioClient getClient(String bucketName) {
        return clientMap.computeIfAbsent(bucketName, this::createClientForBucket);
    }

    /**
     * 获取桶配置(配置不存在时抛出明确异常)
     */
    public BucketConfig getBucketConfig(String bucketName) {
        if (properties.getBuckets() == null || !properties.getBuckets().containsKey(bucketName)) {
            throw new IllegalArgumentException(String.format(
                    "Bucket configuration not found for: %s. Available buckets: %s",
                    bucketName,
                    properties.getBuckets() != null ? properties.getBuckets().keySet() : "none"
            ));
        }
        return properties.getBuckets().get(bucketName);
    }

    /**
     * 创建带连接池的HTTP客户端(所有桶共享同一个连接池)
     */
    private OkHttpClient createPooledHttpClient() {
        PoolConfig poolConfig = properties.getPoolConfig();

        return new OkHttpClient.Builder()
                .connectionPool(new ConnectionPool(
                        poolConfig.getMaxTotal(),
                        poolConfig.getKeepAliveDuration(),
                        TimeUnit.SECONDS
                ))
                .connectTimeout(poolConfig.getConnectionTimeout(), TimeUnit.MILLISECONDS)
                .readTimeout(poolConfig.getReadTimeout(), TimeUnit.MILLISECONDS)
                .writeTimeout(poolConfig.getWriteTimeout(), TimeUnit.MILLISECONDS)
                .build();
    }

    /**
     * 创建桶专用客户端(复用共享连接池)
     */
    private MinioClient createClientForBucket(String bucketName) {
        BucketConfig bucketConfig = getBucketConfig(bucketName);
        String region = StringUtils.hasText(bucketConfig.getRegion()) ?
                bucketConfig.getRegion() : properties.getRegion();

        return MinioClient.builder()
                .endpoint(properties.getEndpoint())
                .credentials(properties.getAccessKey(), properties.getSecretKey())
                .region(StringUtils.hasText(region) ? region : null)
                .httpClient(sharedHttpClient) // 注入共享连接池
                .build();
    }

    /**
     * 关闭所有客户端连接(安全释放资源)
     */
    public void shutdown() throws Exception {
        try {
            // 1. 关闭所有MinIO客户端
            for (MinioClient client : clientMap.values()) {
                try {
                    client.close();
                } catch (Exception e) {
                    // 记录关闭异常但不中断整体流程
                    System.err.println("Error closing MinioClient for bucket: " + e.getMessage());
                }
            }
            clientMap.clear();

            // 2. 关闭HTTP连接池资源
            sharedHttpClient.dispatcher().executorService().shutdown();
            sharedHttpClient.connectionPool().evictAll();

            // 3. 等待资源释放(最多5秒)
            if (!sharedHttpClient.dispatcher().executorService().awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("HTTP client shutdown timed out");
                Thread.currentThread().interrupt();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("HTTP client shutdown interrupted", e);
        }
    }
}

5. service配置

java 复制代码
/**
 * MinIO服务接口 - 企业级生产环境标准
 * 企业级生产环境注意事项:
 * 1. 接口方法定义清晰的业务语义
 * 2. 所有方法参数验证
 * 3. 返回值规范
 * 4. 不暴露底层实现细节
 */
public interface MinioService {

    /**
     * 上传文件到指定桶
     *
     * @param bucketName 桶名称(必须是配置中定义的)
     * @param objectName 对象名称(包含路径)
     * @param file 上传的文件
     * @return 可访问的URL(临时签名URL)
     */
    String uploadFile(String bucketName, String objectName, MultipartFile file) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException;

    /**
     * 上传文件流到指定桶
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param inputStream 文件流
     * @param size 文件大小
     * @param contentType 内容类型
     * @return 可访问的URL
     */
    String uploadFile(String bucketName, String objectName, InputStream inputStream, long size, String contentType) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException;

    /**
     * 获取对象的临时访问URL
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @param expiry 过期时间(秒)
     * @return 临时访问URL
     */
    String getPresignedObjectUrl(String bucketName, String objectName, int expiry) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException;

    /**
     * 获取对象输入流
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @return 对象输入流(调用方负责关闭)
     */
    InputStream getObject(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException;

    /**
     * 删除对象
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     */
    void removeObject(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException;

    /**
     * 检查桶是否存在
     *
     * @param bucketName 桶名称
     * @return 是否存在
     */
    boolean bucketExists(String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException;

    /**
     * 获取对象元数据
     *
     * @param bucketName 桶名称
     * @param objectName 对象名称
     * @return 包含size, contentType等信息的元数据
     */
    Map<String, String> getObjectMetadata(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException;
}


/**
 * MinIO服务实现 - 企业级生产环境实现
 * 企业级生产环境注意事项:
 * 1. 完整的参数校验
 * 2. 详细的错误处理和日志
 * 3. 资源安全关闭
 * 4. 业务逻辑与技术实现分离
 */
public class MinioServiceImpl implements MinioService {

    private final MinioClientFactory clientFactory;

    public MinioServiceImpl(MinioClientFactory clientFactory) {
        this.clientFactory = clientFactory;
    }

    @Override
    public String uploadFile(String bucketName, String objectName, MultipartFile file) throws IOException, ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

        return uploadFile(
                bucketName,
                objectName,
                file.getInputStream(),
                file.getSize(),
                file.getContentType()
        );

    }

    @Override
    public String uploadFile(String bucketName, String objectName, InputStream inputStream, long size, String contentType) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

        try {
            MinioClient client = clientFactory.getClient(bucketName);
            ensureBucketExists(bucketName, client);


            PutObjectArgs args = PutObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .stream(inputStream, size, -1)
                    .contentType(contentType != null ? contentType : "application/octet-stream")
                    .build();

            client.putObject(args);

            // 默认返回24小时有效的预签名URL
            return getPresignedObjectUrl(bucketName, objectName, 24 * 60 * 60);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
    }

    @Override
    public String getPresignedObjectUrl(String bucketName, String objectName, int expiry) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

        MinioClient client = clientFactory.getClient(bucketName);
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                .method(Method.GET)
                .bucket(bucketName)
                .object(objectName)
                .expiry(expiry)
                .build();
        String presignedObjectUrl = client.getPresignedObjectUrl(args);
        return replaceMinioEndpoint(presignedObjectUrl);
    }

    @Override
    public InputStream getObject(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

        MinioClient client = clientFactory.getClient(bucketName);
        GetObjectArgs args = GetObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build();
        return client.getObject(args);
    }

    @Override
    public void removeObject(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {

        MinioClient client = clientFactory.getClient(bucketName);
        RemoveObjectArgs args = RemoveObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build();
        client.removeObject(args);
    }

    @Override
    public boolean bucketExists(String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        MinioClient client = clientFactory.getClient(bucketName);
        return client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    @Override
    public Map<String, String> getObjectMetadata(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        Assert.hasText(bucketName, "Bucket name must not be empty");
        Assert.hasText(objectName, "Object name must not be empty");
        InputStream stream = getObject(bucketName, objectName);
        StatObjectArgs args = StatObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .build();
        StatObjectResponse stat = clientFactory.getClient(bucketName).statObject(args);

        Map<String, String> metadata = new HashMap<>();
        metadata.put("size", String.valueOf(stat.size()));
        metadata.put("contentType", stat.contentType());
        metadata.put("etag", stat.etag());
        metadata.put("lastModified", String.valueOf(stat.lastModified()));
        metadata.put("versionId", stat.versionId());

        return metadata;
    }

    /**
     * 确保桶存在,根据配置决定是否自动创建
     */
    private void ensureBucketExists(String bucketName, MinioClient client) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
            BucketConfig config = clientFactory.getBucketConfig(bucketName);
            if (config.isAutoCreate()) {
                String region = config.getRegion();

                client.makeBucket(MakeBucketArgs.builder()
                        .bucket(bucketName)
                        .region(StringUtils.hasText(region) ? region : null)
                        .build());
            } else {
                throw new IllegalStateException("Bucket does not exist and autoCreate is disabled: " + bucketName);
            }
        }
    }
    /**
     * 直接替换MinIO服务地址(IP+端口+协议)为自定义域名
     *
     * @param rawUrl MinIO SDK生成的原始URL
     * @return 处理后的安全URL
     */
    private String replaceMinioEndpoint( String rawUrl) {
        // 2. 获取MinIO服务原始地址(协议+IP+端口)
        MinioProperties properties = clientFactory.getProperties();

        // 3. 直接字符串替换(关键:保留路径和查询参数不变)
        return rawUrl.replace(properties.getEndpoint(),properties.getRouter());
    }
}
相关推荐
不会C语言的男孩1 小时前
Linux 系统编程 · 第 1 章:Linux 系统概述
c语言·开发语言
m0_547722921 小时前
从零搭建乒乓球比赛管理系统——Spring Boot + 原生 HTML 实战
spring boot·后端·html
码云骑士1 小时前
05-Python字典底层原理-Hash表与有序性的真相
开发语言·python·哈希算法
J2虾虾1 小时前
Android支持Java语言的标准
android·java·开发语言
Cloud_Shy6181 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第六章 Item 44 - 47)
开发语言·人工智能·经验分享·笔记·python
mxlwd1681 小时前
movielen 100k lr模型训练过程
开发语言·python·机器学习
Oo_行者_oO2 小时前
Spring Schedule + ShedLock + RabbitMQ 生产级落地方案 - 云楼(中国)
java·后端