目录
前言
springboot作为目前主流的java开发框架,用来简化spring程序的创建和开发过程。在实际开发过程中我们往往会遇到部分涉及文件上传、下载的场景。经过多方筛选最终选用了Minio作为项目与中的文件存储工具。
Minio作为一款高性能、可扩展、分布式对象存储系统,并且它是完全开源免费的一款工具,非常适合实际项目的使用!!!
关于Minio的Windows的安装教程,各位小伙伴可以参考上一篇Minio安装的教程
地址:Minio基于Window系统的安装与使用-CSDN博客
下面让我们一起来了解一下SpringBoot整合Minio的具体实现方式。
一、环境准备
在使用Minio之前我们要做的第一件事情就是有一个SpringBoot项目,需要小伙伴自行创建或者使用现有项目。土豆这里使用的是自己原有的一个若以框架改过来的一个项目,但是这个项目此前没有使用过Minio,确认这点有助于更加顺利的完成我们的测试。
如果小伙伴的项目中原本使用过Minio的,也没关系哈,可以根据本文做一个参考进一步完善项目内容。
准备好项目之后咱们就可以开始本次的任务了,请跟随土豆一起搞起来,呦吼!!!
二、环境配置
1、引入依赖
在pom.xml中引入Minio的依赖:
<dependencies>
<!--Minio-->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.6</version>
</dependency>
<!--okhttp3 依赖-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
</dependencies>
2、添加配置文件
application.yml配置信息
# 配置minio
minio:
server: http://localhost
port: 9000
endpoint: ${minio.server}:${minio.port}
access-key: admin # minio账号
secret-key: admin118110 # minio密码
bucket: demo001 # 指定桶名
三、代码编写
完成项目的基础配置之后,接下来咱开始来编写我们的代码
1、添加配置类
import io.minio.MinioClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* @author YHL
* @date 2024/4/24 9:36
* @description 当前配置类不建议使用@Data注解,需要手动生成get,set方法
*/
@Component
@ConfigurationProperties(prefix = "minio")
public class MinIoClientConfig {
@Value("${minio.server}")
private String server;
@Value("${minio.port}")
private int port;
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Value("${minio.bucket}")
private String bucket;
/**
* 创建minio连接对象
* @return
*/
@Bean
public MinioClient minioClient(){
return MinioClient.builder()
.endpoint(server,port,false)
.credentials(accessKey,secretKey)
.build();
}
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getAccessKey() {
return accessKey;
}
public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}
public String getSecretKey() {
return secretKey;
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public String getBucket() {
return bucket;
}
public void setBucket(String bucket) {
this.bucket = bucket;
}
}
2、添加工具类
package com.psy.common.utils;
import cn.hutool.core.util.ObjectUtil;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.apache.commons.lang.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author YHL
* @date 2024/4/24 9:51
* @description
*/
@Component
public class MinioUtil {
@Autowired
private MinioClient minioClient;
@Value("${minio.bucket}")
private String bucket;
/**
* description: 判断bucket是否存在,不存在则创建
*/
@SneakyThrows
public boolean existBucket(String name) {
boolean exists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(name).build());
if (!exists) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(name).build());
}
return exists;
}
/**
* 创建存储bucket
*
* @param bucketName 存储bucket名称
* @return Boolean
*/
@SneakyThrows
public Boolean makeBucket(String bucketName) {
boolean exist = existBucket(bucketName);
if (!exist) {
minioClient.makeBucket(MakeBucketArgs.builder()
.bucket(bucketName)
.build());
return true;
}
return false;
}
/**
* 删除存储bucket
*
* @param bucketName 存储bucket名称
* @return Boolean
*/
@SneakyThrows
public Boolean removeBucket(String bucketName) {
boolean exist = existBucket(bucketName);
if (!exist) return false;
Iterable<Result<Item>> results =
minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
for (Result<Item> result : results) {
Item item = result.get();
// 桶不为空不允许删除
if (item.size() > 0) {
return false;
}
}
minioClient.removeBucket(RemoveBucketArgs.builder()
.bucket(bucketName)
.build());
return true;
}
/**
* 列出所有存储桶
*/
@SneakyThrows
public List<Bucket> listBuckets() {
return minioClient.listBuckets();
}
/**
* 列出所有存储桶名称
*/
public List<String> listBucketNames() {
List<Bucket> bucketList = listBuckets();
if (ObjectUtil.isEmpty(bucketList))
return null;
List<String> bucketListName = new ArrayList<>();
for (Bucket bucket : bucketList) {
bucketListName.add(bucket.name());
}
return bucketListName;
}
/**
* 列出存储桶中的所有对象名称
*
* @param bucketName 存储桶名称
*/
@SneakyThrows
public List<String> listObjectNames(String bucketName) {
boolean exist = existBucket(bucketName);
if (!exist) return null;
List<String> listObjectNames = new ArrayList<>();
Iterable<Result<Item>> results =
minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
for (Result<Item> result : results) {
Item item = result.get();
listObjectNames.add(item.objectName());
}
return listObjectNames;
}
/**
* 查看文件对象
*
* @param bucketName 存储bucket名称
* @return 存储bucket内文件对象信息
*/
public Map<String, Object> listObjects(String bucketName) {
boolean exist = existBucket(bucketName);
if (!exist) return null;
Iterable<Result<Item>> results =
minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());
Map<String, Object> map = new HashMap<>();
try {
for (Result<Item> result : results) {
Item item = result.get();
map.put(item.objectName(), item);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
return map;
}
/**
* 文件访问路径
*
* @param bucketName 存储桶名称
* @param objectName 存储桶里的对象名称
*/
@SneakyThrows
public String getObjectUrl(String bucketName, String objectName) {
boolean exist = existBucket(bucketName);
if (!exist) return null;
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(2, TimeUnit.MINUTES)
.build());
}
/**
* 删除一个对象
*
* @param bucketName 存储桶名称
* @param objectName 存储桶里的对象名称
*/
@SneakyThrows
public boolean removeObject(String bucketName, String objectName) {
boolean exist = existBucket(bucketName);
if (!exist) return false;
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
return true;
}
/**
* 删除指定桶的多个文件对象
*
* @param bucketName 存储桶名称
* @param objectNames 含有要删除的多个object名称的迭代器对象
*/
@SneakyThrows
public boolean removeObject(String bucketName, List<String> objectNames) {
boolean exist = existBucket(bucketName);
if (!exist) return false;
List<DeleteObject> objects = new LinkedList<>();
for (String objectName : objectNames) {
objects.add(new DeleteObject(objectName));
}
minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objects).build());
return true;
}
/**
* 批量删除文件对象
*
* @param bucketName 存储bucket名称
* @param objects 对象名称集合
*/
public Iterable<Result<DeleteError>> removeObjects(String bucketName, List<String> objects) {
List<DeleteObject> dos = objects.stream().map(DeleteObject::new).collect(Collectors.toList());
return minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(dos).build());
}
/**
* 文件上传
*/
public String upload(MultipartFile multipartFile) {
String fileName = multipartFile.getOriginalFilename();
// 注意,这里需要加上 \\ 将 特殊字符 . 转意 \\. ,否则异常
assert fileName != null;
String[] fileArray = fileName.split("\\.");
// 获取当前日期
Date now = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String format = sdf.format(now);
fileName = format + "/" + fileArray[0] + System.currentTimeMillis() + "." + fileArray[1];
try {
InputStream inputStream = multipartFile.getInputStream();
// 上传到minio服务器
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(fileName)
.stream(inputStream, -1L, 10485760L)
.build());
} catch (Exception e) {
e.printStackTrace();
}
// 返回地址
return fileName;
}
/**
* 文件下载
*
* @param fileName 文件名
* @param delete 是否删除
*/
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
if (StringUtils.isBlank(fileName)) {
response.setHeader("Content-type", "text/html;charset=UTF-8");
String data = "文件下载失败";
OutputStream ps = response.getOutputStream();
ps.write(data.getBytes(StandardCharsets.UTF_8));
return;
}
outputStream = response.getOutputStream();
// 获取文件对象
inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(fileName).build());
byte[] buf = new byte[1024];
int length = 0;
response.reset();
response.setHeader("Content-Disposition", "attachment;filename=" +
URLEncoder.encode(fileName.substring(fileName.lastIndexOf("/") + 1), "UTF-8"));
response.setContentType("application/octet-stream");
response.setCharacterEncoding("UTF-8");
// 输出文件
while ((length = inputStream.read(buf)) > 0) {
outputStream.write(buf, 0, length);
}
inputStream.close();
// 判断:下载后是否同时删除minio上的存储文件
if (BooleanUtils.isTrue(delete)) {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(fileName).build());
}
} catch (Throwable ex) {
response.setHeader("Content-type", "text/html;charset=UTF-8");
String data = "文件下载失败";
try {
OutputStream ps = response.getOutputStream();
ps.write(data.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
} finally {
try {
outputStream.close();
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
在util中我们已经针对Minio的使用定义好了各种常用方法,如果有遗漏欢迎小伙伴们私信我做出补充。
四、方法测试
代码编写完成之后我们就开始测试我们代码的可用性,在这土豆简单编写了一个controller,主要用来测试我们常用的文件上传,文件下载方法。
代码如下:
import com.psy.common.core.controller.BaseController;
import com.psy.common.core.domain.AjaxResult;
import com.psy.common.utils.MinioUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
/**
* @author YHL
* @date 2024/4/24 8:54
* @description
*/
@RestController
@RequestMapping("/fileDemo")
public class FileDemoController extends BaseController {
@Resource
private MinioUtil minioUtil;
@RequestMapping("/upload")
public AjaxResult upload(MultipartFile multipartFile) {
return AjaxResult.success(minioUtil.upload(multipartFile));
}
@RequestMapping("/download")
public void download(String fileName, HttpServletResponse response) {
minioUtil.fileDownload(fileName, false, response);
}
}
然后我们可以使用测试工具对我们的代码进行测试,查看功能是否实现。
土豆这里使用的测试工具是ApiFox,我个人觉得是这款测试工具比传统的PostMan要美观要好用,有想去的小伙伴可以尝试下载使用一下。
好了,废话不多说,咱开测。
1、文件上传
首先查看我们的Minio指定的桶的状态是否正常,可以看到咱的桶中是没有文件的:
然后我们在ApiFox中输入接口地址以及选择要上传的文件
点击发送之后,返回值显示文件上传完成
然后我们再到Minio中查看
发现所上传的图片已经上传到Minio中指定的桶中了,并且文件的上级地址是当前的日期。
所以说我们的代码是没有问题的,在实际使用中,我们肯定会有将文件地址存储起来的操作
在这里我要提醒大家,
注意:
1)为了方便以后的维护或者是数据迁移等操作,存储时只需要存储文件的相对路径,如果有需要存储桶名或者其他相关信息的话,建议区分字段存储,以避免不必要的麻烦。
2)这里文件上传使用的文件名称采用了原文件名称,但是在实际场景中,这种操作并不可取,为了避免文件冲突以及方便区分文件,建议在文件上传时在数据库中保存文件原名称,并重命名上传文件的名称(可采用时间戳等方式确保文件名称唯一)
2、文件下载
上面完成了文件上传操作后Minio中已经存在一个文件了,现在我们需要将这个文件下载下来,我们继续使用ApiFox来测试:
点击发送:
到这里之后我们的文件下载和文件上传都是成功的。
结语
以上内容就是结合上篇Minio基于Window系统的安装与使用的分享整合的SpringBoot整合Minio的内容
最后还是希望本次分享对您能够有所帮助,如有不足还请各位多多指点,多多支持。
感激不尽,感激不尽。。。。。