携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情
为了在项目中实现文件的管理,我们打算在项目中使用minio对象存储服务。如何搭建minio服务,可以查看Docker compose快速部署minio服务。
在minio服务搭建好的情况下,我们要准备在springboot项目当中集成minio SDK,以便我们项目中的文件能全部交由minio服务来管理。
准备工作
访问http://ip:9001
,通过管理员用户名密码登陆。
-
创建文件桶
从正常项目管理的角度来看,我们一般会提前把文件桶创建好。在团队开发中制定规范,达成共识,确定当前项目对应的文件桶名称,不同业务类型的文件放在什么名称的文件夹下面。
比如以当前项目名称创建一个文件桶,在这个文件桶里面再创建不同的文件夹,类似image、vedio等。这样的话,我们针对头像、音视频等文件就可以放到指定的文件夹里面了。
-
创建指定的用户,赋予对应的权限
先把需要创建的用户名和密码填好,其他可暂时不选,然后创建用户。
其次创建权限:
1.点击左侧栏Access;

2.点击Create Policy
;
权限配置:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::awesome-spring"
]
},
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::awesome-spring/*"
]
}
]
}
复制代码
首先awesome-spring
是文件桶的名称,里面一共包含两段权限:
- 第一段权限是允许在
awesome-spring
文件桶中执行GetBucketLocation
、GetObject(下载)
、PutObject
(上传)操作,基本上满足上传下载的业务。 - 第二段权限是允许在
awesome-spring
的子文件夹中做所有的操作。我们可以根据实际情况把权限设置得更细粒度。
权限和用户都创建完毕后,我们再回到用户列表,我们需要做的步骤就是把用户和权限关联起来:
-
通过以上步骤,我们把权限和用户关联起来了,这样的话,我们新创建的这个用户就有了操作这个bucket的权限了。
如果团队人员比较多的话,我建议通过创建Group的方式,给对应的Group赋予权限,最后把新创建的用户放进Group,这样的话,可以避免一个一个给用户配置权限。
-
创建开发需要的AccessKey和SecretKey
当我们拿到新创建的用户后,登陆
http://ip:9001
我们可以看到,目前的新用户只能看到当前拥有权限的bucket。
接下来,我们立马创建一个service account
来为我们顺利使用minio SDK作准备:
【注意】secret key只有再第一次创建的时候会显示,创建成功后就无法查看,建议下载下来保存好。
Minio SDK依赖
想要把Minio服务集成到springboot项目中,我们可以通过它所提供的SDK来做的,下面我们找到了Maven依赖:
<!--minio文件存储-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.3</version>
</dependency>
复制代码
如果是gradle或者其他依赖管理工具可以去mvnrepository.com/artifact/io… 查找相关的依赖。
文件上传下载功能实现
-
准备自定义minio config
minio: config: # 请填写自己minio服务的ip和端口 endpoint: "http://ip:9000" bucket-name: 文件桶名称 access-key: "pXLVexkIGrvhfPbC" secret-key: "wcI9GE9UeX4qVQik2dV9zK6DkeZVZ3TR" 复制代码
通过springboot的ConfigurationProperties的功能,将配置转换成java bean:
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; /** * @author zouwei * @className MinioConfigProperties * @date: 2022/8/4 下午4:35 * @description: */ @Data @Configuration @ConfigurationProperties(prefix = "minio.config") public class MinioConfigProperties { /** * minio服务API访问入口 */ private String endpoint; /** * 桶名称 */ private String bucketName; /** * 公钥 */ private String accessKey; /** * 私钥 */ private String secretKey; } 复制代码
-
实例化minio client
import io.minio.MinioClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author zouwei * @className MinioConfig * @date: 2022/8/4 下午4:27 * @description: */ @Configuration public class MinioConfig { @Autowired private MinioConfigProperties properties; @Bean public MinioClient minioClient() { return MinioClient.builder() .credentials(properties.getAccessKey(), properties.getSecretKey()) .endpoint(properties.getEndpoint()) .build(); } } 复制代码
通过java config的方式把MinioClient实例化交给spring ioc容器来管理,接下来就可以直接在spring框架体系下正常使用SDK的功能了。
-
上传下载业务实现
// 文件上传 @PostMapping("/image/upload") String upload(@RequestPart("userImage") MultipartFile userImage) throws Exception { fileService.putObject("image", userImage); return "success"; } /** * 下载 * * @param fileId * @param response * @throws Exception */ @GetMapping("/download/{fileId}") public void download(@PathVariable("fileId") String fileId, HttpServletResponse response) throws Exception { fileService.getObject(fileId, response); } 复制代码
controller接收到前端上传的文件,马上调用接口fileService的上传接口,把文件上传到minio服务;
import com.example.awesomespring.bo.FileUploadResult; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; /** * @author zouwei * @className FileService * @date: 2022/8/4 下午3:45 * @description: */ public interface FileService { // 上传功能,把文件提交到minio服务,并把提交结果持久化 FileUploadResult putObject(String dirs, MultipartFile file) throws Exception; // 下载功能,把minio服务中的文件下载并写入响应 void getObject(String fileId, HttpServletResponse response) throws Exception; } 复制代码
我们来看看具体实现:
import lombok.Data; /** * @author zouwei * @className FileUploadResult * @date: 2022/8/4 下午11:59 * @description: */ @Data public class FileUploadResult { // 文件桶名称 private String bucketName; // 文件存储的路径 private String filePath; // 文件名称 private String filename; // 文件上传类型 private String contentType; // 文件大小 private int length; } 复制代码
import com.example.awesomespring.bo.FileUploadResult; import com.example.awesomespring.config.MinioConfigProperties; import com.example.awesomespring.dao.entity.FileUploadRecord; import com.example.awesomespring.dao.mapper.FileUploadRecordMapper; import com.example.awesomespring.service.FileService; import io.minio.*; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Paths; import java.util.Date; import java.util.UUID; /** * @author zouwei * @className FileServiceImpl * @date: 2022/8/4 下午3:45 * @description: */ @Service public class FileServiceImpl implements FileService { @Autowired private MinioClient client; @Autowired private MinioConfigProperties properties; @Autowired private FileUploadRecordMapper fileUploadRecordMapper; /** * 上传文件 * * @param dirs 目标文件夹; 比如image、video * @param file 上传的文件 * @return * @throws Exception */ @Override public FileUploadResult putObject(String dirs, MultipartFile file) throws Exception { FileUploadResult result = putObject(dirs, file, true); // 保存到数据库 FileUploadRecord row = new FileUploadRecord(); row.setFileName(result.getFilename()); row.setCreateTime(new Date()); row.setFilePath(result.getFilePath()); row.setContentType(result.getContentType()); row.setBucketName(result.getBucketName()); row.setId(UUID.randomUUID().toString()); row.setSize(result.getLength()); fileUploadRecordMapper.insert(row); return result; } /** * 下载文件并写入响应中 * * @param fileId * @param response * @throws Exception */ @Override public void getObject(String fileId, HttpServletResponse response) throws Exception { FileUploadRecord row = fileUploadRecordMapper.selectByPrimaryKey(fileId); String path = row.getFilePath(); // 构建下载参数 GetObjectArgs objectArgs = GetObjectArgs.builder() .bucket(bucketName()) .object(path) .build(); // 下载并写入响应中 try (InputStream input = client.getObject(objectArgs); OutputStream outputStream = response.getOutputStream()) { response.setContentType(row.getContentType()); response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Content-Length", String.valueOf(length)); response.setHeader("Content-disposition", "attachment; filename=" + filename); outputStream.write(input.readAllBytes()); outputStream.flush(); } catch (Exception e) { // 建议包装成自定义异常,以便自定义异常处理捕获到 throw e; } } /** * 获取文件桶 * * @return */ private String bucketName() { return properties.getBucketName(); } /** * 如果文件桶不存在就创建 * * @param bucketName * @throws Exception */ private void createIfNotExistBucket(String bucketName) throws Exception { if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } } /** * 这个实现只针对于minio服务实现,所以不建议以接口暴露给外部调用 * * @param dirs * @param file * @param createIfNotExistBucket * @return * @throws Exception */ private FileUploadResult putObject(final String dirs, MultipartFile file, boolean createIfNotExistBucket) throws Exception { // 获取桶名称 final String bucketName = bucketName(); // 获取文件名称 final String filename = file.getOriginalFilename(); // 获取文件类型 final String contentType = file.getContentType(); // 拼接路径;因为不会把所有文件直接放在桶下面 String path = filename; if (StringUtils.isNotBlank(dirs)) { path = Paths.get(dirs, filename).toString(); } // 从上传来的文件中取流 try (InputStream fileStream = file.getInputStream()) { // 如果要求文件桶不存在就创建 if (createIfNotExistBucket) { createIfNotExistBucket(bucketName); } int length = fileStream.available(); // 准备好文件上传的参数 PutObjectArgs objectArgs = PutObjectArgs.builder() .bucket(bucketName) .object(path) .contentType(contentType) .stream(fileStream, length, -1) .build(); // 上传文件 client.putObject(objectArgs); // 返回上传结果 FileUploadResult result = new FileUploadResult(); result.setContentType(contentType); result.setFilePath(dirs); result.setFilename(filename); result.setBucketName(bucketName); result.setLength(length); return result; } catch (Exception e) { // 建议包装成自定义异常,以便自定义异常处理捕获到 throw e; } } } 复制代码
以上代码有几个需要注意的点:
1.文件上传成功到minio服务中后,并不会返回统一的哈希等唯一标识字段,所以我建议我们需要把上传结果保存一条记录到数据库。
2.我们提供给外面的下载链接应该尽可能的简单,比如:
http://127.0.0.1/download/{fileId}
;所以我在设计上传和下载的时候,上传结果用fileId来表示一个文件,下载的时候也只需要使用fileId就可以下载目标文件。3.在文件上传下载处理过程中,产生的Exception应该全部转换成自定义的异常抛出去,这样的话,可以方便后续的统一异常处理逻辑一次性解决服务端的异常问题。
至此,基于minio对象存储中间件的集成就完成了,小伙伴们可以根据自己的实际情况修改文件存储逻辑的具体实现。