MinIO 分布式存储实践:如果那年没嘻哈(磁盘IO),乃亮至少有个家(通过minio)
本文记录在campusai智慧园区项目中,如何基于若依(RuoYi)框架实现MinIO分布式文件存储的完整技术方案。重点剖析配置化存储策略、MinIO集成细节与工程化设计,解决本地磁盘存储的单点故障问题。不仅关注技术实现,更深入探讨架构设计背后的思考与最佳实践。
一、问题与场景:本地磁盘存储的局限性及其工程化挑战
在智慧园区项目初期,我们采用了若依框架默认的本地磁盘存储方案。这种方案看似简单直接,但在实际运行中逐渐暴露出诸多问题,尤其是在企业级应用场景下,其局限性愈发明显。
文件与服务器强绑定的致命弱点
本地磁盘存储的核心问题在于文件与服务器形成了强绑定关系。当管理员在若依后台上传一份PDF技术手册时,文件被直接写入服务器的本地磁盘目录。这种设计存在一个致命缺陷:一旦服务器发生硬件故障、系统崩溃或磁盘损坏,所有已上传的文件将瞬间不可用,甚至永久丢失。对于智慧园区系统而言,这意味着学生无法查阅最新的宿舍管理规定,教师无法获取教学资料,整个系统的文档服务功能彻底瘫痪。更糟糕的是,这种故障模式是"全有或全无"的——要么系统正常运行,要么所有文件服务同时中断,没有任何中间状态。
扩展困难,磁盘容量有限带来的发展瓶颈
随着智慧园区业务的不断发展,文档数量呈指数级增长。PDF技术手册、TXT操作指南、社团活动照片、公告通知等文件迅速占满服务器磁盘空间。传统的解决方案是购买更大容量的硬盘或增加服务器,但这不仅成本高昂,而且操作复杂。每次扩容都需要停机迁移数据,对系统可用性造成严重影响。此外,单一服务器的存储容量终究有限,当文档总量达到TB级别时,本地磁盘方案已无法满足业务需求。
数据安全,缺乏冗余备份的高风险隐患
本地磁盘存储缺乏数据冗余机制,文件仅存一份,没有任何备份。这意味着硬盘一旦发生物理损坏,数据将永久丢失。虽然可以通过定时备份脚本缓解此问题,但备份过程本身存在时间窗口,且需要额外的存储设备和复杂的运维操作。在智慧园区这种对数据可靠性要求较高的场景中,这种"裸奔"式的数据存储方式显然不符合企业级应用的标准。
人工干预带来的效率低下
本地存储方案需要大量的手工操作。管理员需要定期清理磁盘空间,监控磁盘使用率,手动执行备份任务。当需要迁移服务器时,更是需要人工拷贝所有文件,操作繁琐且容易出错。这种依赖人工干预的运维模式不仅效率低下,还增加了人为失误的风险。
二、架构升级:MinIO分布式存储方案
面对本地磁盘存储的诸多痛点,我们需要一套能够彻底解决问题的架构方案。MinIO分布式对象存储因其高性能、高可用性和易用性成为我们的首选,但如何将其无缝集成到现有若依框架中,并确保平滑迁移和长期可维护性,成为我们面临的核心挑战。

2.1 设计目标:
我们的设计目标不仅限于解决技术问题,更注重工程化的长期价值:
高可用:彻底解决单点故障
MinIO的分布式架构天然支持多副本存储,数据会自动在多个节点间复制。即使某个节点完全宕机,文件仍可从其他节点正常访问。我们的目标是将文件可用性从本地存储的约99%提升至99.9%以上,满足企业级应用的标准。
最小化系统改造风险
现有系统已稳定运行,任何架构改造都必须确保向后兼容。我们的方案必须支持配置化切换,管理员只需修改一个配置项即可在本地存储和MinIO存储之间无缝切换,无需修改任何业务代码。
降低人工干预成本
桶的创建、配置管理、故障恢复等操作都应实现自动化,减少对运维人员的依赖。应用启动时应自动完成必要的初始化工作,确保系统"开箱即用"。
按需使用避免浪费
MinIO客户端应在需要时才初始化,避免在仅使用本地存储的环境中占用不必要的资源。这种"按需装配"的设计符合微服务架构的最佳实践。
2.2 核心架构:配置化存储策略的工程化实现
我们设计了一套基于配置驱动的存储策略架构,其核心思想是将存储实现与业务逻辑彻底解耦:
若依后台 → 配置化存储策略 → 本地存储/MinIO存储 → 文件访问
↑ ↑ ↑ ↑
CRUD操作 动态选择 本地磁盘/对象存储 统一URL生成
配置驱动的设计思想
通过ruoyi.uploadType配置项控制存储策略,业务代码无需感知底层存储的具体实现。这种设计符合软件工程的"依赖倒置原则"——高层模块不依赖于低层模块,二者都依赖于抽象接口。
统一抽象层的好处
无论底层使用本地存储还是MinIO,上层业务看到的都是统一的接口和行为。这种抽象不仅简化了代码逻辑,还为未来的进一步扩展预留了空间。如果需要支持阿里云OSS、腾讯云COS等其他存储服务,只需添加新的实现,无需修改现有业务代码。
三、关键技术实现:
3.1 配置化存储策略:本地/MinIO动态切换的工程化实现
在CommonController中,我们实现了基于配置的动态存储选择机制。这个设计的关键在于将存储策略的选择逻辑从业务代码中完全抽离,实现真正的"配置驱动"。
@PostMapping("/upload")
@ResponseBody
public AjaxResult uploadFile(MultipartFile file) throws Exception {
try {
String fileName = null, url = null;
// 配置化存储选择:核心设计理念
if ("minio".equals(RuoYiConfig.getUploadType())) {
// MinIO存储路径
fileName = upload2Minio(file);
url = MinioConfig.getUrl() + "/" + MinioConfig.getBucket() + "/" + fileName;
} else {
// 默认本地存储路径
String filePath = RuoYiConfig.getUploadPath();
fileName = FileUploadUtils.upload(filePath, file);
url = serverConfig.getUrl() + fileName;
}
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
} catch (Exception e) {
return AjaxResult.error(e.getMessage());
}
}
设计价值的深度解析
这个看似简单的条件判断背后蕴含着重要的工程化思想:
-
开闭原则
系统对扩展开放,对修改关闭。新增存储类型只需添加新的条件分支,无需修改现有逻辑。这种设计确保了系统的长期可维护性。
-
配置驱动的架构优势
存储策略的选择完全由外部配置控制,实现了策略与实现的彻底分离。管理员可以根据环境需求(开发/测试/生产)灵活选择存储方案。
-
平滑迁移
通过保留本地存储路径,确保了在MinIO出现问题时可以快速回退。这种"渐进式迁移"策略大大降低了系统改造风险。
3.2 MinIO客户端配置:工程化设计的典范
MinIO客户端的配置类体现了Spring Boot最佳实践与工程化设计的完美结合:
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
private static String host;
private static String port;
private static String bucket;
private static String username;
private static String password;
private static String url;
@Bean
@Lazy
@ConditionalOnProperty(name = "ruoyi.uploadType", havingValue = "minio")
public MinioClient minioClient() throws Exception {
// 域名转IP解决SDK兼容性问题:工程化细节处理
if (!InetAddressValidator.getInstance().isValid(host)){
logger.warn("MinIO:host is not format as ip,change it!");
InetAddress inetAddress = InetAddress.getByName(host);
host = inetAddress.getHostAddress();
logger.info("MinIO:host change to : {}" , host);
}
MinioClient minioClient = new MinioClient(host, Integer.valueOf(port), username, password, false);
// 桶自动创建:自动化运维的关键
boolean found = minioClient.bucketExists(bucket);
if (!found) {
minioClient.makeBucket(bucket);
}
logger.info("MinIO:bucket init ok , bucket={}" , bucket);
return minioClient;
}
// 静态工具方法,便于全局访问
public static String getBucket() { return bucket; }
public static String getUrl() { return url; }
// ... 其他getter/setter
}
关键技术点的深度剖析:
-
条件化装配
@ConditionalOnProperty注解确保仅在ruoyi.uploadType=minio时创建MinIO客户端。这种设计避免了在不使用MinIO的环境中占用不必要的资源,体现了"按需使用"的工程化原则。结合@Lazy注解,进一步优化了启动性能,确保资源只在真正需要时才被初始化。 -
地址解析:
MinIO旧版本SDK要求endpoint必须是IP格式,但实际生产环境中我们更倾向于使用域名。我们的地址解析逻辑自动将域名转换为IP,既解决了技术兼容性问题,又保持了配置的语义清晰。这种"技术适配层"的设计体现了工程化思维——不因技术限制而牺牲最佳实践。
-
桶自初始化:
应用启动时自动检查并创建所需存储桶,避免了因桶不存在导致的运行时错误。这种"自初始化"设计大大降低了运维复杂度,确保系统能够"开箱即用",符合DevOps最佳实践。
-
静态工具类:
将配置参数设计为静态字段并提供静态访问方法,实现了全局统一的配置管理。业务代码无需注入配置类实例,直接通过
MinioConfig.getBucket()即可获取配置值,既简化了代码,又确保了配置的一致性。
3.3 MinIO上传实现:资源管理的工程化优化
上传功能的实现体现了对资源管理和异常处理的深度思考:
private String upload2Minio(MultipartFile file) {
// 唯一文件名生成策略:避免覆盖的关键
String name = Seq.getId(Seq.uploadSeqType) + "." +
FilenameUtils.getExtension(file.getOriginalFilename());
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
minioClient.putObject(
MinioConfig.getBucket(),
name,
inputStream,
file.getSize(),
null, null,
file.getContentType()
);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
// 关键:显式关闭流,避免临时文件残留
if (inputStream != null) inputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return name;
}
优化细节的工程化解读:
-
唯一文件名策略:数据安全的基础保障
使用
Seq.getId()生成唯一文件名,有效避免了文件覆盖问题。在分布式环境中,文件名冲突可能导致严重的数据安全问题,这种设计确保了每个文件的唯一性和可追溯性。 -
流式上传:性能与资源管理的平衡
采用流式上传方式,避免将整个文件加载到内存中。对于大文件(如高清视频、大型PDF文档),这种设计可有效防止内存溢出,确保系统的稳定性。
-
资源管理:finally块的正确使用
在finally块中显式关闭输入流,解决了Spring无法自动清理临时文件的问题。这种资源管理方式体现了"谁申请谁释放"的原则,避免了资源泄漏和文件锁定的问题。
-
元数据保留:数据完整性的工程化考量
完整传递文件大小、内容类型等元数据信息,确保存储在MinIO中的文件信息完整。这种细节处理体现了对数据完整性的重视,是工程化设计的重要体现。
四、配置文件与工作流程:
4.1 配置文件:配置驱动的核心载体
# application.yml
minio:
host: minio.example.com
port: 9000
bucket: campusai-docs
username: admin
password: password123
url: http://minio.example.com:9000
ruoyi:
uploadType: minio # 关键配置:控制存储策略
配置设计的工程化思考:
-
环境隔离:不同环境(开发/测试/生产)可使用不同的MinIO配置
-
安全考虑:敏感信息(密码)通过配置管理,避免硬编码
-
灵活切换:
uploadType配置项实现存储策略的动态切换
4.2 完整工作流程:从启动到上传的详细解析
启动阶段:自动化初始化的工程化实践
-
Spring启动解析
minio前缀配置,注入MinioConfig静态字段 -
检测
ruoyi.uploadType=minio,触发MinIO客户端创建条件 -
域名解析:
minio.example.com→192.168.1.100(解决SDK限制) -
桶检查:自动创建
campusai-docs桶(自动化运维) -
连接测试:验证MinIO服务可用性
上传阶段:业务逻辑的完整执行
-
前端调用
/common/upload接口上传PDF文档 -
后端根据配置选择MinIO存储策略
-
生成唯一文件名:
20250218_abc123.pdf -
流式上传至MinIO集群
-
返回访问URL:
http://minio.example.com:9000/campusai-docs/20250218_abc123.pdf
访问阶段:高可用性的体现
-
通过生成的URL直接访问文件
-
MinIO支持权限控制(私有/公开桶)
-
支持CDN集成加速文件访问
-
多副本机制确保文件高可用
五、设计亮点与价值
5.1 配置化存储策略
开闭原则的完美体现
系统对扩展开放,对修改关闭。新增存储类型只需添加新的实现,无需修改现有业务代码。这种设计确保了系统的长期可维护性和扩展性。
依赖倒置原则的应用
高层业务模块不依赖于底层存储的具体实现,二者通过配置抽象解耦。这种设计大大降低了系统的耦合度,提高了模块的可测试性和可替换性。
配置驱动的架构优势
存储策略的选择完全由外部配置控制,实现了策略与实现的彻底分离。管理员可以根据实际需求灵活选择最适合的存储方案,无需重新部署系统。
5.2 工程化优化:
条件化装配:
通过@ConditionalOnProperty和@Lazy注解实现按需初始化,避免不必要的资源消耗。这种设计在微服务架构中尤为重要,能够显著提升系统的性能和资源利用率。
地址解析:
自动将域名解析为IP地址,解决了MinIO SDK的技术限制。这种"适配层"设计体现了工程化思维——不因技术框架的限制而牺牲最佳实践。
桶自初始化:
应用启动时自动创建所需存储桶,避免了因配置遗漏导致的运行时错误。这种设计大大降低了运维复杂度,确保系统能够"开箱即用"。
资源管理:
显式关闭文件流,避免临时文件残留和内存泄漏。这种资源管理方式体现了对系统稳定性的高度重视,是工程化设计的重要体现。
5.3 高可用保障:
多副本存储:
MinIO的分布式架构支持数据多副本存储,即使单个节点完全宕机,文件仍可从其他节点正常访问。这种设计将文件可用性从本地存储的约99%提升至99.9%以上。
故障转移:
MinIO集群支持自动故障转移,当某个存储节点发生故障时,系统会自动将请求路由到健康的节点。这种机制确保了服务的连续性,提升了用户体验。
横向扩展:
MinIO支持动态扩容,可以根据业务需求随时增加存储节点。这种设计确保了系统能够适应业务的快速增长,避免了频繁的架构改造。
六、总结
通过MinIO分布式存储方案的实施,我们成功解决了智慧园区项目的文件存储瓶颈,但更重要的是,这套方案体现了工程化设计的核心价值:
文件可用性提升至99.9%以上,满足企业级应用的标准。多副本存储机制确保了数据的安全性,即使硬件发生故障,文件服务也能持续可用。
支持PB级存储,满足海量文档需求。横向扩展能力确保了系统能够适应业务的快速增长,无需频繁进行架构改造。
配置驱动的存储策略实现了业务逻辑与存储实现的彻底解耦。条件化装配、资源管理优化等技术细节体现了对系统性能和稳定性的高度重视。
兼容现有本地存储,支持配置化切换。这种渐进式迁移策略大大降低了系统改造风险,确保了业务的连续性。
桶自动创建、配置集中管理、故障自动恢复等特性大大降低了运维复杂度,符合DevOps最佳实践。
这套方案不仅解决了具体的技术问题,更体现了软件工程的系统化思维。从配置驱动的架构设计到资源管理的细节优化,每一个技术决策都经过了深思熟虑,确保了系统的长期可维护性和可扩展性。这种工程化设计能力,正是构建高质量企业级应用的关键所在。
相关完整代码:
通用请求处理
package com.ruoyi.web.controller.common;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.common.config.ServerConfig;
import com.ruoyi.common.utils.uuid.Seq;
import com.ruoyi.web.core.config.MinioConfig;
import io.minio.MinioClient;
import io.minio.errors.*;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
import org.xmlpull.v1.XmlPullParserException;
/**
* 通用请求处理
*
* @author ruoyi
*/
@Controller
@RequestMapping("/common")
public class CommonController
{
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
@Autowired
private ServerConfig serverConfig;
private static final String FILE_DELIMETER = ",";
@Autowired
private MinioClient minioClient;
/**
* 通用下载请求
*
* @param fileName 文件名称
* @param delete 是否删除
*/
@GetMapping("/download")
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
{
try
{
if (!FileUtils.checkAllowDownload(fileName))
{
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
}
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
String filePath = RuoYiConfig.getDownloadPath() + fileName;
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, realFileName);
FileUtils.writeBytes(filePath, response.getOutputStream());
if (delete)
{
FileUtils.deleteFile(filePath);
}
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}
private String upload2Minio(MultipartFile file){
String name = Seq.getId(Seq.uploadSeqType)+"."+ FilenameUtils.getExtension(file.getOriginalFilename());
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
minioClient.putObject(
MinioConfig.getBucket(), name,inputStream,file.getSize(),null,null, file.getContentType()
);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
try {
inputStream.close(); //minio上传完成后,要关闭文件流,否则造成临时文件占用,spring无法自动清除
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return name;
}
/**
* 通用上传请求(单个)
*/
@PostMapping("/upload")
@ResponseBody
public AjaxResult uploadFile(MultipartFile file) throws Exception
{
try
{
String fileName=null,url=null;
// 上传并返回新文件名称
if ("minio".equals(RuoYiConfig.getUploadType())){
//如果指定了minio上传
fileName = upload2Minio(file);
url = MinioConfig.getUrl()+"/"+MinioConfig.getBucket()+"/"+fileName;
}else{
//默认上传本地
String filePath = RuoYiConfig.getUploadPath();
fileName = FileUploadUtils.upload(filePath, file);
url = serverConfig.getUrl() + fileName;
}
AjaxResult ajax = AjaxResult.success();
ajax.put("url", url);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
/**
* 通用上传请求(多个)
*/
@PostMapping("/uploads")
@ResponseBody
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
{
try
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
List<String> urls = new ArrayList<String>();
List<String> fileNames = new ArrayList<String>();
List<String> newFileNames = new ArrayList<String>();
List<String> originalFilenames = new ArrayList<String>();
for (MultipartFile file : files)
{
String fileName=null,url=null;
// 上传并返回新文件名称
if ("minio".equals(RuoYiConfig.getUploadType())){
//如果指定了minio上传
fileName = upload2Minio(file);
url = MinioConfig.getUrl()+"/"+MinioConfig.getBucket()+"/"+fileName;
}else{
//默认上传本地
fileName = FileUploadUtils.upload(filePath, file);
url = serverConfig.getUrl() + fileName;
}
urls.add(url);
fileNames.add(fileName);
newFileNames.add(FileUtils.getName(fileName));
originalFilenames.add(file.getOriginalFilename());
}
AjaxResult ajax = AjaxResult.success();
ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
/**
* 本地资源通用下载
*/
@GetMapping("/download/resource")
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
throws Exception
{
try
{
if (!FileUtils.checkAllowDownload(resource))
{
throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
}
// 本地资源路径
String localPath = RuoYiConfig.getProfile();
// 数据库资源地址
String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
// 下载名称
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, downloadName);
FileUtils.writeBytes(downloadPath, response.getOutputStream());
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}
}
MinioConfig
package com.ruoyi.web.core.config;
import io.minio.MinioClient;
import io.minio.org.apache.commons.validator.routines.InetAddressValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import java.net.InetAddress;
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
final static Logger logger = LoggerFactory.getLogger(MinioConfig.class);
private static String host;
private static String port;
private static String bucket;
private static String username;
private static String password;
private static String url;
@Bean
@Lazy
@ConditionalOnProperty(name = "ruoyi.uploadType", havingValue = "minio")
public MinioClient minioClient() throws Exception {
//minio的初始化存在一个小问题,endpoint必须是ip形式,不能是host
if (!InetAddressValidator.getInstance().isValid(host)){
logger.warn("MinIO:host is not format as ip,change it!");
InetAddress inetAddress = InetAddress.getByName(host);
host = inetAddress.getHostAddress();
logger.info("MinIO:host change to : {}" , host);
}
MinioClient minioClient = new MinioClient(host,
Integer.valueOf(port),
username,
password,false);
logger.info("minio connected, buckets="+minioClient.listBuckets());
boolean found = minioClient.bucketExists(bucket);
if (!found) {
// 创建桶
minioClient.makeBucket(bucket);
}
logger.info("MinIO:bucket init ok , bucket={}" , bucket);
return minioClient;
}
public static String getHost() {
return host;
}
public static String getPort() {
return port;
}
public static String getBucket() {
return bucket;
}
public static String getUsername() {
return username;
}
public static String getPassword() {
return password;
}
public static String getUrl() {
return url;
}
public void setHost(String host) {
MinioConfig.host = host;
}
public void setPort(String port) {
MinioConfig.port = port;
}
public void setBucket(String bucket) {
MinioConfig.bucket = bucket;
}
public void setUsername(String username) {
MinioConfig.username = username;
}
public void setPassword(String password) {
MinioConfig.password = password;
}
public void setUrl(String url) {
MinioConfig.url = url;
}
}
关键代码解释:

更多推荐
所有评论(0)