从0到1构建MES系统7-附件管理

57 阅读4分钟

今天我们来讲讲MES系统的【附件管理】。

SpringBoot+MinIO双引擎存储实战

一、传统附件管理的架构痛点

在传统单体应用中,开发者常将FileUtils.save() 等硬编码写入业务层,这种设计在面对多云存储、混合存储等复杂场景时,会出现以下问题:

  1. 存储介质切换引发雪崩式代码修改
  2. 不同存储协议的API差异导致业务逻辑污染
  3. 扩展新存储方案时产生重复造轮子现象

策略模式可完美解决上述问题,下面通过双存储引擎实战演示架构设计过程。

二、策略模式驱动存储引擎设计

2.1 UNL核心模型解析

Pasted image 20250528171907.png 关键角色说明

  • 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工具类如需要了解请下载源码查看

三、架构设计精要

  1. 开闭原则:新增存储方案只需实现FileServiceStrategy接口,无需修改已有代码
  2. 依赖倒置:高层模块依赖抽象接口,与具体存储实现解耦

四、拓展思考

  1. 混合存储策略:通过组合模式实现冷热数据分层存储
  2. 多云容灾方案:在不同云厂商间实现双活存储
  3. 智能路由:根据文件特征自动选择最优存储引擎

本文源码已上传Gitee 开源项目地址

欢迎在评论区分享你的技术选型经验,或对本文方案的改进建议!

关注公众号「慧工云创」

扫码_搜索联合传播样式-标准色版.png