一、导入依赖
<!-- MinIO 客户端 -->
<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.7</version>
</dependency><!-- OkHttp 是一个高效的网络库 -->
<dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.9.3</version>
</dependency>
二、添加yaml配置
server:port: 3333spring:servlet:multipart:max-request-size: 200MBmax-file-size: 200MBminio:url: http://127.0.0.1:19000 #换成自己的minio服务端地址accessKey: minioadmin # 用户名secretKey: minioadmin # 密码bucketName: demo # bucketName指的就是之前创建的MinIO桶Bucket
三、创建配置类
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @ClassName MinioConfig* @Description 文件* @Date 2024/9/21 10:21*/
@Data
@Configuration
public class MinioConfig {/** 访问地址 */@Value("${minio.url}")private String endpoint;/** 唯一标识账户 */@Value("${minio.accessKey}")private String accessKey;/** 账户密码 */@Value("${minio.secretKey}")private String secretKey;/** 默认储存桶 */@Value("${minio.bucketName}")private String bucketName;/*** 标记此方法为一个 Bean,Spring 会在上下文中管理这个 Bean*/@Beanpublic MinioClient minioClient(){使用 MinioClient 的构建器模式创建一个 MinioClient 实例return MinioClient.builder()//设置 Minio 服务的端点地址.endpoint(endpoint)// 设置访问 Minio 服务所需的访问密钥和秘密密钥.credentials(accessKey, secretKey)// 构建并返回 MinioClient 实例.build();}
}
四、创建工具类
java">import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Decoder;import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;/*** Minio工具类*/
@Log4j2
@Component
@RequiredArgsConstructor
public class MinioUtils {/*** 定义一个私有的、不可变的 MinioClient 实例变量 minioClient*/private final MinioClient minioClient;/*** 启动SpringBoot容器的时候初始化Bucket(桶)* 如果没有Bucket(桶)则创建** @param bucketName Bucket(桶)名称*/@SneakyThrows(Exception.class)private void createBucket(String bucketName) {if (!bucketExists(bucketName)) {//使用 minioClient 的 makeBucket 方法创建一个新的桶。//MakeBucketArgs.builder().bucket(bucketName).build() 创建了一个请求参数对象,指定了要创建的桶的名称。minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());}}/*** 判断Bucket(桶)是否存在** @param bucketName Bucket(桶)名称* @return true:存在 false:不存在*/@SneakyThrows(Exception.class)public boolean bucketExists(String bucketName) {//调用 minioClient 对象的 bucketExists 方法,检查指定的桶是否存在。//BucketExistsArgs.builder().bucket(bucketName).build() 创建了一个请求参数对象,用于传递桶名称。return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());}/*** 获得Bucket(桶)的策略** @param bucketName Bucket(桶)名称* @return 指定桶的访问策略*/@SneakyThrows(Exception.class)public String getBucketPolicy(String bucketName) {//通过 MinIO 客户端 API 获取指定桶的访问策略。return minioClient.getBucketPolicy(//使用构建者模式创建一个 GetBucketPolicyArgs 对象,以便设置请求参数。GetBucketPolicyArgs.builder()//设置要获取策略的桶的名称。.bucket(bucketName)//构建并返回 GetBucketPolicyArgs 对象,该对象将作为参数传递给 getBucketPolicy 方法。.build());}/*** 获得所有Bucket(桶)列表** @return 所有桶的列表*/@SneakyThrows(Exception.class)public List<Bucket> getAllBuckets() {//调用 MinIO 客户端的 listBuckets 方法,返回当前用户拥有的所有桶的列表。return minioClient.listBuckets();}/*** 根据bucketName获取其相关信息** @param bucketName Bucket(桶)名称* @return Optional<Bucket>*/@SneakyThrows(Exception.class)public Optional<Bucket> getBucket(String bucketName) {//查找并返回指定名称的桶,如果不存在,则返回 Optional.empty()。//调用 getAllBuckets() 方法获取所有桶,然后使用 Java 8 的 Stream API 进行处理。//filter 方法用于筛选出名称与 bucketName 匹配的桶。//findFirst 方法用于返回第一个匹配的桶,返回值是一个 Optional<Bucket>,表示可能存在的桶。return getAllBuckets().stream().filter(bucket -> bucket.name().equals(bucketName)).findFirst();}/*** 根据bucketName删除Bucket(桶) true:删除成功 false:删除失败** @param bucketName Bucket(桶)名称*/@SneakyThrows(Exception.class)public void removeBucket(String bucketName) {//调用 MinIO 客户端的 removeBucket 方法,通过构建 RemoveBucketArgs 对象来指定要删除的桶名。minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());}/*** 判断文件是否存在** @param bucketName Bucket(桶)名称* @param objectName 文件名称* @return true:存在 false:不存在*/public boolean isObjectExist(String bucketName, String objectName) {boolean exist = true;try {//调用 MinIO 客户端的 statObject 方法,构建 StatObjectArgs 对象以检查对象的状态。如果对象存在,该方法不会抛出异常。minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());} catch (Exception e) {log.error("【Minio工具类】>>>> 判断文件是否存在, 异常:", e);exist = false;}return exist;}/*** 判断文件夹是否存在** @param bucketName Bucket(桶)名称* @param objectName 文件夹名称* @return true:存在 false:不存在*/public boolean isFolderExit(String bucketName, String objectName) {boolean exist = true;try {//使用 listObjects 方法列出指定桶中以 objectName 为前缀的对象。recursive(false) 表示不进行递归查找。Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());//遍历结果,检查每个 Item 是否为目录且名称与 objectName 匹配。如果找到匹配的目录,将 exist 设为 true。for (Result<Item> result : results) {Item item = result.get();if (item.isDir() && objectName.equals(item.objectName())) {exist = true;}}} catch (Exception e) {log.error("【Minio工具类】>>>> 判断文件夹是否存在, 异常:", e);exist = false;}return exist;}/*** 根据文件前置查询文件** @param bucketName 存储桶* @param prefix 前缀* @param recursive 是否使用递归查询* @return MinioItem 列表*/@SneakyThrows(Exception.class)public List<Item> getAllObjectsByPrefix(String bucketName,String prefix,boolean recursive) {List<Item> list = new ArrayList<>();// 创建一个空的 Item 列表,用于存储结果Iterable<Result<Item>> objectsIterator = minioClient.listObjects(// 获取指定桶中以 prefix 开头的对象列表ListObjectsArgs.builder()// 构建 ListObjectsArgs 对象.bucket(bucketName)// 设置桶名.prefix(prefix)// 设置对象前缀.recursive(recursive)// 设置是否递归查找.build());// 构建参数if (objectsIterator != null) { // 检查对象迭代器是否不为 nullfor (Result<Item> o : objectsIterator) { // 遍历迭代器中的每个结果Item item = o.get();// 获取当前结果中的 Item 对象list.add(item);// 将 Item 添加到列表中}}return list;// 返回包含所有找到的 Item 的列表}/*** 获取文件流** @param bucketName 存储桶* @param objectName 文件名* @return 二进制流*/@SneakyThrows(Exception.class)public InputStream getObject(String bucketName, String objectName) {return minioClient.getObject(// 调用 MinIO 客户端的 getObject 方法,获取指定对象的输入流GetObjectArgs.builder()// 构建 GetObjectArgs 对象.bucket(bucketName)// 设置桶名.object(objectName)// 设置对象名.build()); // 构建参数并调用 getObject}/*** 断点下载** @param bucketName 存储桶* @param objectName 文件名称* @param offset 起始字节的位置* @param length 要读取的长度* @return 二进制流*/@SneakyThrows(Exception.class)public InputStream getObject(String bucketName, String objectName, long offset, long length) {return minioClient.getObject(// 调用 MinIO 客户端的 getObject 方法,获取指定对象的输入流GetObjectArgs.builder()// 构建 GetObjectArgs 对象.bucket(bucketName) // 设置桶名.object(objectName)// 设置对象名.offset(offset)// 设置读取的起始偏移量.length(length) // 设置要读取的字节长度.build());// 构建参数并调用 getObject}/*** 获取路径下文件列表** @param bucketName 存储桶* @param prefix 文件名称* @param recursive 是否递归查找,false:模拟文件夹结构查找* @return 二进制流*/public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) {return minioClient.listObjects(// 调用 MinIO 客户端的 listObjects 方法,返回对象列表ListObjectsArgs.builder() // 构建 ListObjectsArgs 对象.bucket(bucketName) // 设置桶名.prefix(prefix)// 设置对象前缀.recursive(recursive)// 设置是否递归查找.build());// 构建参数并调用 listObjects}/*** 使用MultipartFile进行文件上传** @param bucketName 存储桶* @param file 文件名* @param objectName 对象名* @param contentType 类型* @return*/@SneakyThrows(Exception.class)public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) {InputStream inputStream = file.getInputStream(); // 获取文件的输入流,以便上传return minioClient.putObject(// 调用 MinIO 客户端的 putObject 方法,上传文件PutObjectArgs.builder()// 构建 PutObjectArgs 对象.bucket(bucketName)// 设置桶名.object(objectName)// 设置对象名.contentType(contentType)// 设置文件的内容类型.stream(inputStream, inputStream.available(), -1) // 设置输入流及其可用字节数,-1 表示不限制.build());// 构建参数并调用 putObject}/*** 图片上传** @param bucketName 存储桶* @param imageBase64 图像的 Base64 编码字符串* @param imageName 接收图像的原始名称* @return*/public ObjectWriteResponse uploadImage(String bucketName, String imageBase64, String imageName) {if (!StringUtils.isEmpty(imageBase64)) {// 检查 Base64 字符串是否非空InputStream in = base64ToInputStream(imageBase64);// 将 Base64 字符串转换为输入流String newName = System.currentTimeMillis() + "_" + imageName + ".jpg";// 生成新文件名,包含当前时间戳和原始名称String year = String.valueOf(new Date().getYear());// 获取当前年份String month = String.valueOf(new Date().getMonth());// 获取当前月份return uploadFile(bucketName, year + "/" + month + "/" + newName, in); // 调用 uploadFile 方法上传文件,路径为 年/月/新文件名}return null;// 如果 Base64 字符串为空,返回 null}// BASE64Decoder在jdk8以上的版本移除了,报错最简单解决换成jdk8就行了public static InputStream base64ToInputStream(String base64) {ByteArrayInputStream stream = null;// 声明 ByteArrayInputStream 变量,用于保存字节流try {// 使用 BASE64Decoder 解码 Base64 字符串并去除首尾空格,得到字节数组byte[] bytes = new BASE64Decoder().decodeBuffer(base64.trim());// 将字节数组转换为 ByteArrayInputStream,以便返回stream = new ByteArrayInputStream(bytes);} catch (Exception e) {e.printStackTrace();}return stream;// 返回字节输入流,如果出错则返回 null}/*** 上传本地文件** @param bucketName 存储桶* @param objectName 对象名称* @param fileName 本地文件路径* @return*/@SneakyThrows(Exception.class)public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) {return minioClient.uploadObject(// 调用 MinIO 客户端的 uploadObject 方法进行文件上传UploadObjectArgs.builder()// 使用 UploadObjectArgs 构建上传参数.bucket(bucketName)// 设置存储桶名称.object(objectName)// 设置对象名称(在存储桶中的文件名).filename(fileName)// 设置要上传的本地文件名.build()); // 构建并返回 UploadObjectArgs 对象,然后执行上传}/*** 通过流上传文件** @param bucketName 存储桶* @param objectName 文件对象* @param inputStream 文件流* @return*/@SneakyThrows(Exception.class)public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) {return minioClient.putObject( // 调用 MinIO 客户端的 putObject 方法进行文件上传PutObjectArgs.builder()// 使用 PutObjectArgs 构建上传参数.bucket(bucketName)// 设置存储桶名称.object(objectName)// 设置对象名称(在存储桶中的文件名).stream(inputStream, inputStream.available(), -1)// 设置输入流及其大小,-1 表示使用默认值.build()); // 构建并返回 PutObjectArgs 对象,然后执行上传}/*** 创建文件夹或目录** @param bucketName 存储桶* @param objectName 目录路径* @return*/@SneakyThrows(Exception.class)public ObjectWriteResponse createDir(String bucketName, String objectName) {return minioClient.putObject(// 调用 MinIO 客户端的 putObject 方法进行上传PutObjectArgs.builder()// 使用 PutObjectArgs 构建上传参数.bucket(bucketName)// 设置存储桶名称.object(objectName) // 设置对象名称(在存储桶中的“目录”名称).stream(new ByteArrayInputStream(new byte[]{}), 0, -1)// 使用空的字节数组流,表示创建一个空对象,0 表示长度,-1 表示使用默认值.build());// 构建并返回 PutObjectArgs 对象,然后执行上传}/*** 获取文件信息, 如果抛出异常则说明文件不存在** @param bucketName 存储桶* @param objectName 文件名称* @return*/@SneakyThrows(Exception.class)public String getFileStatusInfo(String bucketName, String objectName) {return minioClient.statObject( // 调用 MinIO 客户端的 statObject 方法获取对象状态信息StatObjectArgs.builder() // 使用 StatObjectArgs 构建请求参数.bucket(bucketName) // 设置存储桶名称.object(objectName)// 设置对象名称.build() // 构建并返回 StatObjectArgs 对象).toString();// 调用 toString 方法,将获取的状态信息转换为字符串并返回}/*** 拷贝文件** @param bucketName 存储桶* @param objectName 文件名* @param srcBucketName 目标存储桶* @param srcObjectName 目标文件名*/@SneakyThrows(Exception.class)public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) {return minioClient.copyObject(// 调用 MinIO 客户端的 copyObject 方法进行对象复制CopyObjectArgs.builder()// 使用 CopyObjectArgs 构建复制参数.source(CopySource.builder()// 设置复制源.bucket(bucketName) // 源存储桶名称.object(objectName)// 源对象名称.build())// 构建并返回 CopySource 对象.bucket(srcBucketName)// 设置目标存储桶名称.object(srcObjectName) // 设置目标对象名称.build() // 构建并返回 CopyObjectArgs 对象);// 执行对象复制并返回 ObjectWriteResponse}/*** 删除文件** @param bucketName 存储桶* @param objectName 文件名称*/@SneakyThrows(Exception.class)public void removeFile(String bucketName, String objectName) {minioClient.removeObject(// 调用 MinIO 客户端的 removeObject 方法以删除指定对象RemoveObjectArgs.builder()// 使用 RemoveObjectArgs 构建删除参数.bucket(bucketName) // 设置要删除对象的存储桶名称.object(objectName)// 设置要删除的对象名称.build());// 构建并返回 RemoveObjectArgs 对象,并执行删除操作}/*** 批量删除文件** @param bucketName 存储桶* @param keys 需要删除的文件列表* @return*/public void removeFiles(String bucketName, List<String> keys) {List<DeleteObject> objects = new LinkedList<>();// 创建一个 LinkedList 用于存储待删除对象keys.forEach(s -> {// 遍历对象键列表objects.add(new DeleteObject(s)); // 将每个键封装成 DeleteObject 并添加到列表中try {// 尝试执行删除操作removeFile(bucketName, s);// 调用 removeFile 方法删除指定对象} catch (Exception e) {log.error("【Minio工具类】>>>> 批量删除文件,异常:", e);}});}/*** 获取文件外链* 使用了 .expiry(expires) 方法,指定了预签名 URL 的过期时间。* 适用于需要限制 URL 有效期的场景。* 适用于需要设定过期时间的情况** @param bucketName 存储桶* @param objectName 文件名* @param expires 过期时间 <=7 秒 (外链有效时间(单位:秒))* @return url*/@SneakyThrows(Exception.class)public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {// 构建获取预签名对象 URL 的参数GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()// 使用 GetPresignedObjectUrlArgs 的构建器.expiry(expires)// 设置 URL 的过期时间(以秒为单位).bucket(bucketName) // 设置存储桶名称.object(objectName)// 设置对象名称.build();// 构建并返回 GetPresignedObjectUrlArgs 对象return minioClient.getPresignedObjectUrl(args);// 调用 MinIO 客户端获取预签名 URL 并返回}/*** 获得文件外链* 使用了 .method(Method.GET) 方法,指定了 HTTP 请求方法为 GET。* 适用于需要明确请求类型的场景,但没有设置过期时间。* 适用于无需设定过期时间的访问** @param bucketName* @param objectName* @return url*/@SneakyThrows(Exception.class)public String getPresignedObjectUrl(String bucketName, String objectName) {// 构建获取预签名对象 URL 的参数GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()// 使用 GetPresignedObjectUrlArgs 的构建器.bucket(bucketName) // 设置存储桶名称.object(objectName)// 设置对象名称.method(Method.GET)// 设置对象名称.build();// 构建并返回 GetPresignedObjectUrlArgs 对象return minioClient.getPresignedObjectUrl(args);// 调用 MinIO 客户端获取预签名 URL 并返回}/*** 将URLDecoder编码转成UTF8** @param str* @return* @throws UnsupportedEncodingException*/public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {// 将字符串中的不合法 URL 编码转换为合法编码String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");// 使用正则表达式,将不符合 URL 编码规范的百分号(%)替换为 %25return URLDecoder.decode(url, "UTF-8");// 使用 URLDecoder 解码字符串,返回 UTF-8 编码的结果}}
一些解释
1.@Log4j2:这是一个 Lombok 提供的注解,用于自动生成一个 Log4j2 日志记录器。使用此注解后,你可以直接调用 log 对象来记录日志,而无需手动创建日志实例。
2.@Component:这是 Spring 框架的注解,表示该类是一个 Spring 组件,Spring 会自动检测并将其注册为一个 Bean。可以用于自动装配和依赖注入。
3.@RequiredArgsConstructor:也是 Lombok 提供的注解,自动生成一个构造函数,该构造函数接受所有被 final 修饰的字段或带有 @NonNull 注解的字段。这简化了依赖注入的过程,使代码更加简洁。
4.@SneakyThrows(Exception.class) :是 Lombok 提供的一个注解,用于在方法上处理检查性异常。
作用:
自动处理异常:在使用 @SneakyThrows 注解的方法中,如果抛出任何类型的检查性异常(即编译器要求显式捕获或声明的异常),Lombok 会自动将其包装成运行时异常(RuntimeException),从而避免了需要显式捕获或声明这些异常的麻烦。
注意事项:
尽管 @SneakyThrows 使代码更简洁,但在调试或异常处理时,要小心隐式处理异常可能导致的问题。如果可能,考虑在应用程序中适当处理异常,以提高代码的可读性和可维护性。
5.Optional是Java 8引入的一个新的容器对象,它提供了非常丰富的API,主要是为了解决空指针异常的问题。Optional类允许你创建一个可能为null的值的容器,从而避免了直接使用null值可能导致的空指针异常。它提供了多种方法来操作这个容器,包括判断值是否存在(isPresent())、获取值(get())、转换值(map())、过滤值(filter())等。
6.OkHttp 是一个高效的 HTTP 客户端库,用于发送和接收网络请求,它支持同步和异步调用,自动处理常见的网络问题如重定向、HTTPS握手和连接池。它广泛用于 Android 和 Java 应用程序中,以提高网络通信的效率和稳定性。
五、创建controller层
java">/*** Minio控制层*/
@Log4j2
@RestController
@RequestMapping("/minio")
public class MinioController {@Autowiredprivate MinioUtils minioUtils;@Autowiredprivate MinioConfig minioConfig;/*** 文件上传** @param file*/@PostMapping("/upload")public String upload(@RequestParam("file") MultipartFile file) {try {//文件名String fileName = file.getOriginalFilename();String newFileName = System.currentTimeMillis() + "." + StringUtils.substringAfterLast(fileName, ".");//类型String contentType = file.getContentType();minioUtils.uploadFile(minioConfig.getBucketName(), file, newFileName, contentType);return "上传成功";} catch (Exception e) {log.error("上传失败",e);return "上传失败";}}/*** 删除** @param fileName*/@DeleteMapping("/")public void delete(@RequestParam("fileName") String fileName) {minioUtils.removeFile(minioConfig.getBucketName(), fileName);}/*** 获取文件信息** @param fileName* @return*/@GetMapping("/info")public String getFileStatusInfo(@RequestParam("fileName") String fileName) {return minioUtils.getFileStatusInfo(minioConfig.getBucketName(), fileName);}/*** 获取文件外链** @param fileName* @return*/@GetMapping("/url")public String getPresignedObjectUrl(@RequestParam("fileName") String fileName) {return minioUtils.getPresignedObjectUrl(minioConfig.getBucketName(), fileName);}/*** 文件下载** @param fileName* @param response*/@GetMapping("/download")public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {try {// 从 MinIO 获取指定的对象(文件)的输入流InputStream fileInputStream = minioUtils.getObject(minioConfig.getBucketName(), fileName);// 设置响应头,指明这是一个附件下载,并指定下载文件的名称response.setHeader("Content-Disposition", "attachment;filename=" + fileName);// 设置响应的内容类型为强制下载response.setContentType("application/force-download");// 设置响应的字符编码为 UTF-8response.setCharacterEncoding("UTF-8");// 将输入流中的文件内容复制到响应输出流中IOUtils.copy(fileInputStream, response.getOutputStream());response.flushBuffer(); // 刷新响应输出流} catch (Exception e) {log.error("下载失败");System.out.println(e);}}