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