MinIO分布式存储(从0到Vue+SpringBoot整合开发)2024版

MinIO分布式存储与Vue+SpringBoot整合开发实战指南

一、MinIO简介与环境搭建

1.1 MinIO概述

MinIO是一个高性能、分布式对象存储系统,专为云原生和AI工作负载设计。它兼容Amazon S3 API,是构建私有云存储的理想选择。

2.2 Docker部署MinIO

bash 复制代码
# 拉取MinIO镜像
docker pull minio/minio

# 运行MinIO容器
docker run -p 9000:9000 -p 9001:9001 \
  --name minio \
  -v /mnt/data:/data \
  -e "MINIO_ROOT_USER=admin" \
  -e "MINIO_ROOT_PASSWORD=password" \
  minio/minio server /data --console-address ":9001"

访问 http://localhost:9001 即可进入MinIO管理控制台。

二、SpringBoot后端集成

2.1 添加依赖配置

xml 复制代码
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.2</version>
</dependency>

2.2 MinIO配置类

java 复制代码
@Configuration
public class MinioConfig {
    
    @Value("${minio.endpoint}")
    private String endpoint;
    
    @Value("${minio.accessKey}")
    private String accessKey;
    
    @Value("${minio.secretKey}")
    private String secretKey;
    
    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

2.3 文件服务实现

java 复制代码
@Service
public class FileStorageService {
    
    @Autowired
    private MinioClient minioClient;
    
    @Value("${minio.bucketName}")
    private String bucketName;
    
    /**
     * 上传文件
     */
    public String uploadFile(MultipartFile file, String fileName) throws Exception {
        // 检查存储桶是否存在
        boolean found = minioClient.bucketExists(BucketExistsArgs.builder()
                .bucket(bucketName).build());
        if (!found) {
            minioClient.makeBucket(MakeBucketArgs.builder()
                    .bucket(bucketName).build());
        }
        
        // 上传文件
        minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());
        
        return endpoint + "/" + bucketName + "/" + fileName;
    }
    
    /**
     * 下载文件
     */
    public InputStream downloadFile(String fileName) throws Exception {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }
    
    /**
     * 删除文件
     */
    public void deleteFile(String fileName) throws Exception {
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(fileName)
                        .build());
    }
}

2.4 控制器层

java 复制代码
@RestController
@RequestMapping("/api/file")
public class FileController {
    
    @Autowired
    private FileStorageService fileStorageService;
    
    @PostMapping("/upload")
    public ResponseEntity<Map<String, String>> uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "fileName", required = false) String fileName) {
        try {
            if (fileName == null || fileName.isEmpty()) {
                fileName = file.getOriginalFilename();
            }
            
            String fileUrl = fileStorageService.uploadFile(file, fileName);
            
            Map<String, String> response = new HashMap<>();
            response.put("url", fileUrl);
            response.put("fileName", fileName);
            response.put("message", "文件上传成功");
            
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of("error", "文件上传失败: " + e.getMessage()));
        }
    }
    
    @GetMapping("/download/{fileName}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
        try {
            InputStream fileStream = fileStorageService.downloadFile(fileName);
            InputStreamResource resource = new InputStreamResource(fileStream);
            
            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, 
                            "attachment; filename=\"" + fileName + "\"")
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(resource);
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }
    }
}

三、Vue前端实现

3.1 文件上传组件

vue 复制代码
<template>
  <div class="file-upload">
    <div class="upload-area" 
         @drop="handleDrop"
         @dragover="handleDragOver"
         @click="triggerFileInput">
      <input type="file" 
             ref="fileInput" 
             @change="handleFileSelect" 
             style="display: none" 
             multiple>
      <div class="upload-content">
        <i class="el-icon-upload"></i>
        <div class="upload-text">将文件拖到此处,或<em>点击上传</em></div>
      </div>
    </div>
    
    <div v-if="uploading" class="upload-progress">
      <el-progress :percentage="uploadProgress"></el-progress>
    </div>
    
    <div v-if="uploadedFiles.length > 0" class="file-list">
      <h3>已上传文件</h3>
      <div v-for="file in uploadedFiles" :key="file.url" class="file-item">
        <div class="file-info">
          <span class="file-name">{{ file.fileName }}</span>
          <span class="file-url">{{ file.url }}</span>
        </div>
        <div class="file-actions">
          <el-button size="small" @click="downloadFile(file)">下载</el-button>
          <el-button size="small" type="danger" @click="deleteFile(file)">删除</el-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: 'FileUpload',
  data() {
    return {
      uploading: false,
      uploadProgress: 0,
      uploadedFiles: []
    };
  },
  methods: {
    triggerFileInput() {
      this.$refs.fileInput.click();
    },
    
    handleFileSelect(event) {
      const files = event.target.files;
      this.uploadFiles(files);
    },
    
    handleDragOver(event) {
      event.preventDefault();
      event.stopPropagation();
    },
    
    handleDrop(event) {
      event.preventDefault();
      event.stopPropagation();
      const files = event.dataTransfer.files;
      this.uploadFiles(files);
    },
    
    async uploadFiles(files) {
      this.uploading = true;
      this.uploadProgress = 0;
      
      const formData = new FormData();
      for (let i = 0; i < files.length; i++) {
        formData.append('files', files[i]);
      }
      
      try {
        const response = await axios.post('/api/file/upload', formData, {
          headers: {
            'Content-Type': 'multipart/form-data'
          },
          onUploadProgress: (progressEvent) => {
            this.uploadProgress = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total
            );
          }
        });
        
        this.uploadedFiles.unshift(...response.data);
        this.$message.success('文件上传成功');
      } catch (error) {
        console.error('上传失败:', error);
        this.$message.error('文件上传失败');
      } finally {
        this.uploading = false;
        this.uploadProgress = 0;
        this.$refs.fileInput.value = '';
      }
    },
    
    async downloadFile(file) {
      try {
        const response = await axios.get(`/api/file/download/${file.fileName}`, {
          responseType: 'blob'
        });
        
        const url = window.URL.createObjectURL(new Blob([response.data]));
        const link = document.createElement('a');
        link.href = url;
        link.setAttribute('download', file.fileName);
        document.body.appendChild(link);
        link.click();
        link.remove();
        window.URL.revokeObjectURL(url);
      } catch (error) {
        console.error('下载失败:', error);
        this.$message.error('文件下载失败');
      }
    },
    
    async deleteFile(file) {
      try {
        await axios.delete(`/api/file/delete/${file.fileName}`);
        this.uploadedFiles = this.uploadedFiles.filter(f => f.url !== file.url);
        this.$message.success('文件删除成功');
      } catch (error) {
        console.error('删除失败:', error);
        this.$message.error('文件删除失败');
      }
    }
  }
};
</script>

<style scoped>
.upload-area {
  border: 2px dashed #dcdfe6;
  border-radius: 6px;
  padding: 40px;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.3s;
}

.upload-area:hover {
  border-color: #409eff;
}

.upload-content {
  color: #606266;
}

.upload-text em {
  color: #409eff;
  font-style: normal;
}

.file-list {
  margin-top: 20px;
}

.file-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  margin-bottom: 10px;
}

.file-info {
  flex: 1;
}

.file-name {
  font-weight: bold;
  display: block;
}

.file-url {
  color: #909399;
  font-size: 12px;
  display: block;
  margin-top: 5px;
}
</style>

3.2 配置文件

javascript 复制代码
// src/api/config.js
const API_BASE_URL = process.env.VUE_APP_API_BASE_URL || 'http://localhost:8080';

export default {
  UPLOAD_URL: `${API_BASE_URL}/api/file/upload`,
  DOWNLOAD_URL: `${API_BASE_URL}/api/file/download`,
  DELETE_URL: `${API_BASE_URL}/api/file/delete`
};

四、高级功能实现

4.1 大文件分片上传

java 复制代码
// SpringBoot后端分片上传支持
@PostMapping("/chunk-upload")
public ResponseEntity<?> chunkUpload(
        @RequestParam("file") MultipartFile file,
        @RequestParam("chunkNumber") Integer chunkNumber,
        @RequestParam("totalChunks") Integer totalChunks,
        @RequestParam("identifier") String identifier) {
    try {
        // 存储分片文件
        String chunkName = identifier + "_" + chunkNumber;
        fileStorageService.uploadChunk(file, chunkName);
        
        // 检查是否所有分片都已上传完成
        if (chunkNumber.equals(totalChunks)) {
            String finalFileName = fileStorageService.mergeChunks(identifier, totalChunks);
            return ResponseEntity.ok(Map.of("url", finalFileName, "message", "文件上传完成"));
        }
        
        return ResponseEntity.ok(Map.of("message", "分片上传成功"));
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", e.getMessage()));
    }
}

4.2 文件预览功能

vue 复制代码
<template>
  <div class="file-preview">
    <div v-if="isImage(file)" class="image-preview">
      <img :src="file.url" :alt="file.fileName" @click="showPreview = true">
    </div>
    <div v-else-if="isPdf(file)" class="pdf-preview">
      <embed :src="file.url" type="application/pdf" width="100%" height="600px">
    </div>
    <div v-else class="default-preview">
      <i class="el-icon-document"></i>
      <span>{{ file.fileName }}</span>
    </div>
    
    <el-dialog :visible.sync="showPreview" :title="file.fileName">
      <img :src="file.url" style="width: 100%">
    </el-dialog>
  </div>
</template>

<script>
export default {
  props: {
    file: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      showPreview: false
    };
  },
  methods: {
    isImage(file) {
      return file.fileName.match(/\.(jpg|jpeg|png|gif|webp)$/i);
    },
    isPdf(file) {
      return file.fileName.match(/\.pdf$/i);
    }
  }
};
</script>

五、部署与配置

5.1 应用配置

yaml 复制代码
# application.yml
minio:
  endpoint: http://localhost:9000
  accessKey: admin
  secretKey: password
  bucketName: my-bucket

spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB

5.2 跨域配置

java 复制代码
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:8081")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowCredentials(true);
    }
}

六、最佳实践建议

  1. 安全性:在生产环境中使用HTTPS,定期轮换访问密钥
  2. 错误处理:实现完善的异常处理和日志记录
  3. 性能优化:对大文件使用分片上传,配置合适的超时时间
  4. 监控告警:监控MinIO集群状态和存储使用情况
  5. 备份策略:制定定期备份和灾难恢复计划

这个完整的实现方案提供了从MinIO部署到前后端整合的全套代码,可以直接用于项目开发。根据实际需求,您可以进一步扩展和优化这些功能。

相关推荐
用户9083246027312 小时前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840821 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解1 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解1 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记2 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者2 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840822 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解2 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者3 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺3 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端