今天我们来讲讲MES系统的【附件管理】。
SpringBoot+MinIO双引擎存储实战
一、传统附件管理的架构痛点
在传统单体应用中,开发者常将FileUtils.save() 等硬编码写入业务层,这种设计在面对多云存储、混合存储等复杂场景时,会出现以下问题:
- 存储介质切换引发雪崩式代码修改
- 不同存储协议的API差异导致业务逻辑污染
- 扩展新存储方案时产生重复造轮子现象
策略模式可完美解决上述问题,下面通过双存储引擎实战演示架构设计过程。
二、策略模式驱动存储引擎设计
2.1 UNL核心模型解析
关键角色说明
- FileServiceStrategy: 抽象策略接口,定义
upload/delete/download核心方法 - LocalFileServiceStrategy: 本地磁盘策略实现类
- MinioFileServiceStrategy: MinIO对象存储实现类
- FileConfig: 策略执行配置,持有策略引用
2.2 双存储引擎实现
2.2.1 本地磁盘策略实现
/**
* 本地文件服务
* @author fwj
* @since 2025/4/24 */@Slf4j
@Service
@ConditionalOnProperty(name = "mom.upload.strategy", havingValue = "local")
public class LocalFileServiceImpl implements FileServiceStrategy {
@Autowired
private FileProperties properties;
@Override
public String uploadFile(MultipartFile file) throws FileException {
try {
return FileUtils.upload(properties.getUploadPath(), file);
} catch (IOException e) {
throw new FileException("文件上传失败", e);
}
}
@Override
public byte[] downloadFile(String filePath) throws FileException {
// 参数校验
if (filePath == null || filePath.trim().isEmpty()) {
throw new FileException("文件路径不能为空");
}
// 路径处理
Path fullPath;
try {
fullPath = Paths.get(properties.getUploadPath(), filePath).normalize();
} catch (InvalidPathException e) {
throw new FileException("无效的文件路径: " + filePath, e);
}
// 资源加载和校验
UrlResource resource;
try {
resource = new UrlResource(fullPath.toUri());
} catch (MalformedURLException e) {
throw new FileException("无效的文件URL路径: " + fullPath, e);
}
if (!resource.exists()) {
throw new FileException("文件不存在: " + fullPath);
}
if (!resource.isReadable()) {
throw new FileException("文件不可读: " + fullPath);
}
// 读取文件内容
try (InputStream inputStream = resource.getInputStream()) {
return inputStream.readAllBytes();
} catch (IOException e) {
throw new FileException("读取文件失败: " + fullPath, e);
}
}
@Override
public void deleteFile(String filePath) throws FileException {
// 参数校验
if (filePath == null || filePath.trim().isEmpty()) {
throw new IllegalArgumentException("文件路径不能为空或空字符串");
}
// 路径处理和安全检查
Path fullPath;
try {
fullPath = Paths.get(properties.getUploadPath(), filePath).normalize();
// 安全验证:确保目标路径在基础目录内
if (!fullPath.startsWith(Paths.get(properties.getUploadPath()).normalize())) {
throw new FileException("非法文件路径: 尝试访问外部目录");
}
} catch (InvalidPathException e) {
throw new FileException("无效的文件路径: " + filePath, e);
}
// 执行删除
try {
boolean deleted = Files.deleteIfExists(fullPath);
if (!deleted) {
log.warn("文件不存在,无需删除: {}", fullPath);
}
} catch (NoSuchFileException e) {
log.warn("文件不存在,无需删除: {}", fullPath);
} catch (AccessDeniedException e) {
throw new FileException("无权限删除文件: " + fullPath, e);
} catch (DirectoryNotEmptyException e) {
throw new FileException("无法删除非空目录: " + fullPath, e);
} catch (SecurityException e) {
throw new FileException("安全限制阻止文件删除: " + fullPath, e);
} catch (IOException e) {
throw new FileException("删除文件失败: " + fullPath, e);
}
}
}
2.2.2 MinIO对象存储策略实现
添加maven引用
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
Java实现类
/**
* Minio文件服务
* @author fwj
* @since 2025/4/24 */@Service
@ConditionalOnProperty(name = "mom.upload.strategy", havingValue = "minio")
public class MinioFileServiceImpl implements FileServiceStrategy {
@Autowired
private MinioConfig minioConfig;
@Override
public String uploadFile(MultipartFile file) throws FileException {
try {
return FileUtils.uploadMinio(file);
} catch (IOException e) {
throw new FileException("文件上传失败", e);
}
}
@Override
public byte[] downloadFile(String filePath) throws FileException {
if (filePath == null || filePath.trim().isEmpty()) {
throw new FileException("文件路径不能为空");
}
try (InputStream inputStream = minioConfig.getMinioClient().getObject(
GetObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(filePath)
.build())) {
if (inputStream == null) {
throw new FileException("无法获取文件输入流");
}
return inputStream.readAllBytes();
} catch (IOException ex) {
throw new FileException("文件下载失败: " + ex.getMessage(), ex);
} catch (Exception ex) {
throw new FileException("发生未知错误: " + ex.getMessage(), ex);
}
}
@Override
public void deleteFile(String filePath) throws FileException {
if (filePath == null || filePath.trim().isEmpty()) {
throw new FileException("文件路径不能为空");
}
if (minioConfig == null || minioConfig.getMinioClient() == null) {
throw new FileException("MinIO配置未初始化");
}
try {
minioConfig.getMinioClient().removeObject(
RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(filePath)
.build());
} catch (ErrorResponseException ex) {
// 文件不存在或其他MinIO特定错误
throw new FileException("删除文件失败: " + ex.getMessage(), ex);
} catch (InsufficientDataException | InternalException |
InvalidKeyException | InvalidResponseException |
IOException | NoSuchAlgorithmException |
ServerException | XmlParserException ex) {
// MinIO客户端可能抛出的其他异常
throw new FileException("删除文件时发生错误: " + ex.getMessage(), ex);
} catch (Exception ex) {
// 其他未预期的异常
throw new FileException("发生未知错误: " + ex.getMessage(), ex);
}
}
2.3、SpringBoot自动化装配
yml配置文件
mom:
#附件配置
upload:
strategy: local # 可选 local 或 minio
public-url: http://localhost:8081/uploads # 本地文件访问URL前缀
upload-path: ./uploads
minio:
accessKey: minioadmin
secretKey: minioadmin
bucketName: test
public-url: http://192.168.1.1:9000
Java代码
/**
* 文件配置信息
* @author fwj
* @since 2025/4/24 */@Configuration
public class FileConfig {
@Bean
public FileServiceContext fileUploadStrategyContext(
FileProperties properties,
FileServiceStrategy localFileServiceStrategy,
FileServiceStrategy minioFileServiceStrategy) {
FileServiceStrategy strategy = "minio".equals(properties.getStrategy())
? minioFileServiceStrategy
: localFileServiceStrategy;
return new FileServiceContext(strategy);
}
}
2.4 存储上下文实战应用
文件存储上下文类
/**
* 文件服务上下文
* @author fwj
* @since 2025/4/24 */@Service
public class FileServiceContext {
private FileServiceStrategy fileServiceStrategy;
public FileServiceContext(FileServiceStrategy fileServiceStrategy) {
this.fileServiceStrategy = fileServiceStrategy;
}
public void setFileService(FileServiceStrategy fileServiceStrategy) {
this.fileServiceStrategy = fileServiceStrategy;
}
public String uploadFile(MultipartFile file) throws FileException {
return fileServiceStrategy.uploadFile(file);
}
public byte[] downloadFile(String filePath) throws FileException {
return fileServiceStrategy.downloadFile(filePath);
}
public void deleteFile(String filePath) throws FileException {
fileServiceStrategy.deleteFile(filePath);
}
}
使用实例
@Autowired
private FileServiceContext fileServiceContext;
...
// 上传并返回新文件名称
String filePath = fileServiceContext.uploadFile(file);
备注: 其中策略实现类中的FileUtils工具类如需要了解请下载源码查看
三、架构设计精要
- 开闭原则:新增存储方案只需实现
FileServiceStrategy接口,无需修改已有代码 - 依赖倒置:高层模块依赖抽象接口,与具体存储实现解耦
四、拓展思考
- 混合存储策略:通过组合模式实现冷热数据分层存储
- 多云容灾方案:在不同云厂商间实现双活存储
- 智能路由:根据文件特征自动选择最优存储引擎
本文源码已上传Gitee 开源项目地址:
欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!
关注公众号「慧工云创」