Java项目中使用minio存储服务
具体如下:
1️⃣导入依赖:
<!-- https://mvnrepository.com/artifact/io.minio/minio --><dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.5.17</version></dependency><!-- 工具类 --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.37</version></dependency>
2️⃣编辑配置文件:
# minio 服务
minio:#Minio服务所在地址endpoint: http://192.168.1.58:9000#存储桶名称 testdata bigfilesbucketName: testdata#访问的keyaccessKey: zItdf4kAbUVJgsFWNk#访问的秘钥secretKey: JlZp3WqOTG6PFEckFW7UHXqXSzGu1ICwMXQ6Ep
3️⃣设置配置文件:
import io.minio.MinioClient;
import io.minio.errors.ServerException;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** minio 配置** @author xjs* @date 2025/04*/
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {/*** 端点*/private String endpoint;/*** 访问密钥*/private String accessKey;/*** 密钥*/private String secretKey;/*** 存储桶名称*/private String bucketName;/*** 注入 Minio 客户端** @return {@link MinioClient }*/@Beanpublic MinioClient minioClient() throws ServerException {try {return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();} catch (Exception e) {throw new ServerException("-----创建Minio客户端失败-----" + e.getMessage() , 202, "未处理");}//return MinioClient.builder()// .endpoint(endpoint)// .credentials(accessKey, secretKey)// .build();}
}
4️⃣编写MinioUtil工具类
import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.lang.UUID;
import com.ev.common.utils.StringUtils;
import com.ev.web.config.MinioConfig;
import com.ev.windfarm.utils.DateUtils;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.Item;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** Minio 工具类** @author xjs* @date 2025/04*/
@Component
public class MinioUtil {private static final Logger log = LoggerFactory.getLogger(MinioUtil.class);@Resourceprivate MinioClient minioClient;@Autowiredprivate MinioConfig prop;/*** 最大文件大小* 100 MB 最大文件大小*/private static final long MAX_FILE_SIZE = 100 * 1024 * 1024;/*** 检查存储bucket是否存在** @param bucketName Bucket 名称* @return boolean 存在返回 true,否则返回 false*/public boolean bucketExists(String bucketName) {try {return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());} catch (Exception e) {log.error("检查 bucket 是否存在失败,bucketName: {}, 错误信息: {}", bucketName, e.getMessage());return false;}}/*** 创建存储bucket** @param bucketName Bucket 名称* @return boolean 创建成功返回 true,否则返回 false*/public boolean makeBucket(String bucketName) {try {if (!bucketExists(bucketName)) {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());log.info("Bucket 创建成功,bucketName: {}", bucketName);return true;} else {log.warn("Bucket 已经存在,bucketName: {}", bucketName);}} catch (Exception e) {log.error("创建 bucket 失败,bucketName: {}, 错误信息: {}", bucketName, e.getMessage());}return false;}/*** 删除存储bucket** @param bucketName Bucket 名称* @return boolean 删除成功返回 true,否则返回 false*/public boolean removeBucket(String bucketName) {try {if (bucketExists(bucketName)) {minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());log.info("Bucket 删除成功,bucketName: {}", bucketName);return true;} else {log.warn("Bucket 不存在,bucketName: {}", bucketName);}} catch (Exception e) {log.error("删除 bucket 失败,bucketName: {}, 错误信息: {}", bucketName, e.getMessage());}return false;}/*** 获取所有 bucket 列表** @return List<Bucket> 所有的 Bucket 列表*/public List<Bucket> getAllBuckets() {try {return minioClient.listBuckets();} catch (Exception e) {log.error("获取所有 buckets 失败,错误信息: {}", e.getMessage());}return Collections.emptyList();}/*** 文件上传** @param file 文件* @return String 上传后的文件名,上传失败返回 null*/public String upload(MultipartFile file) {String originalFilename = file.getOriginalFilename();if (StringUtils.isBlank(originalFilename)) {throw new IllegalArgumentException("文件名不能为空");}String fileName = UUID.randomUUID() + getFileExtension(originalFilename);String objectName = DateUtils.getNowYyyyMmdd() + "/" + fileName;log.info("上传文件,原文件名:{},上传后文件名:{}", originalFilename, fileName);try (InputStream fileInputStream = file.getInputStream()) {PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(prop.getBucketName()).object(objectName).stream(fileInputStream, file.getSize(), -1).contentType(file.getContentType()).build();minioClient.putObject(objectArgs);log.info("文件上传成功,objectName: {}", objectName);return objectName;} catch (Exception e) {log.error("文件上传失败,文件名:{},错误信息: {}", originalFilename, e.getMessage());}return null;}/*** 优化文件上传* 文件上传:优化版** @param file* @return {@link String }*/public String optimizedFileUpload(MultipartFile file) {String originalFilename = file.getOriginalFilename();if (StringUtils.isBlank(originalFilename)) {throw new IllegalArgumentException("文件名不能为空");}if (file.getSize() > MAX_FILE_SIZE) {throw new IllegalArgumentException("文件大小超出限制,最大允许上传 100 MB");}String fileName = UUID.randomUUID() + getFileExtension(originalFilename);// yyyy/MM/dd/fileName.xxxString objectName = DateUtils.getNowYyyyMmdd() + "/" + fileName;log.info("准备上传文件,原文件名:{},上传后文件名:{}", originalFilename, fileName);try (InputStream fileInputStream = file.getInputStream()) {PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(prop.getBucketName()).object(objectName).stream(fileInputStream, file.getSize(), -1).contentType(file.getContentType()).build();// 使用重试机制进行上传uploadWithRetry(objectArgs, 3);log.info("文件上传成功,objectName: {}", objectName);return objectName;} catch (Exception e) {log.error("文件上传失败,文件名:{},错误信息: {}", originalFilename, e.getMessage());}return null;}/*** // 上传重试机制** @param objectArgs 对象参数* @param maxRetries 最大重试次数* @throws Exception 例外*/private void uploadWithRetry(PutObjectArgs objectArgs, int maxRetries) throws Exception {int attempt = 0;while (attempt < maxRetries) {try {minioClient.putObject(objectArgs);return; // 上传成功,返回} catch (Exception e) {attempt++;log.warn("上传失败,第 {} 次尝试,错误信息: {}", attempt, e.getMessage());if (attempt == maxRetries) {throw new RuntimeException("上传失败,已达到最大重试次数");}// 等待一段时间后重试Thread.sleep(2000);}}}/*** 获取文件扩展名** @param fileName 文件名* @return String 文件扩展名*/private String getFileExtension(String fileName) {int lastIndex = fileName.lastIndexOf(".");return lastIndex == -1 ? "" : fileName.substring(lastIndex);}/*** 验证文件大小* 限制上传的文件大小** @param file 文件* @return boolean*/public boolean validateFileSize(MultipartFile file) {if (file.getSize() > MAX_FILE_SIZE) {log.warn("文件超出大小限制,最大允许上传 100 MB,当前文件大小: {}", file.getSize());return false;}return true;}/*** 生成文件的预签名 URL,用于文件预览** @param fullFileName 文件完整路径* @return String 预签名 URL*/public String generatePreviewUrl(String fullFileName) {if (StringUtils.isBlank(fullFileName)) {log.warn("文件名为空,无法生成预签名 URL");return null;}try {GetPresignedObjectUrlArgs presignedUrlArgs = GetPresignedObjectUrlArgs.builder().bucket(prop.getBucketName()).object(fullFileName).method(Method.GET)// 设置 URL 过期时间为 1 小时.expiry(3600).build();String url = minioClient.getPresignedObjectUrl(presignedUrlArgs);log.info("生成预签名 URL 成功,fileName: {}, url: {}", fullFileName, url);return url;} catch (Exception e) {log.error("生成预签名 URL 失败,fileName: {}", fullFileName, e);}return null;}/*** 下载文件** @param fileName 文件名称* @param res HTTP 响应*/public void download(String fileName, HttpServletResponse res) {GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(prop.getBucketName()).object(fileName).build();try (GetObjectResponse response = minioClient.getObject(objectArgs);FastByteArrayOutputStream os = new FastByteArrayOutputStream()) {byte[] buf = new byte[1024];int len;while ((len = response.read(buf)) != -1) {os.write(buf, 0, len);}os.flush();byte[] bytes = os.toByteArray();res.setCharacterEncoding("utf-8");res.addHeader("Content-Disposition", "attachment;fileName=" + fileName);try (ServletOutputStream stream = res.getOutputStream()) {stream.write(bytes);stream.flush();}} catch (Exception e) {log.error("下载文件失败,fileName: {}, 错误信息: {}", fileName, e.getMessage());}}/*** 文件下载:优化版** @param fileName 文件名称* @param res HTTP 响应*/public void downloadOptimizedVersion(String fileName, HttpServletResponse res) {GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(prop.getBucketName()).object(fileName).build();try (GetObjectResponse response = minioClient.getObject(objectArgs)) {// 增大缓冲区byte[] buf = new byte[1024 * 4];int len;// 分块下载并输出到响应流try (ServletOutputStream stream = res.getOutputStream()) {res.setCharacterEncoding("utf-8");res.addHeader("Content-Disposition", "attachment;fileName=" + fileName);res.setHeader("Cache-Control", "no-store");res.setHeader("Pragma", "no-cache");res.setHeader("Expires", "0");while ((len = response.read(buf)) != -1) {stream.write(buf, 0, len);// 确保实时写入stream.flush();}log.info("文件下载成功,fileName: {}", fileName);}} catch (Exception e) {log.error("下载文件失败,fileName: {}, 错误信息: {}", fileName, e.getMessage());}}/*** 查看文件对象列表** @return List<Item> 文件对象列表*/public List<Item> listObjects() {try {Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(prop.getBucketName()).build());List<Item> items = new ArrayList<>();for (Result<Item> result : results) {items.add(result.get());}return items;} catch (Exception e) {log.error("列出文件对象失败,错误信息: {}", e.getMessage());}return Collections.emptyList();}/*** 删除文件** @param fileName 文件名* @return boolean 删除成功返回 true,否则返回 false*/public boolean remove(String fileName) {try {minioClient.removeObject(RemoveObjectArgs.builder().bucket(prop.getBucketName()).object(fileName).build());log.info("删除文件成功,fileName: {}", fileName);return true;} catch (Exception e) {log.error("删除文件失败,fileName: {}, 错误信息: {}", fileName, e.getMessage());}return false;}
}
5️⃣调用示例
@Autowiredprivate MinioUtil minioUtil;@Autowiredprivate MinioConfig minioConfig;@Resourceprivate MinioClient minioClient;/*** 存储桶存在** @param bucketName 存储桶名称* @return {@link ResponseEntity }<{@link Object }>*/@ApiOperation(value = "查看存储bucket是否存在")@GetMapping("/bucketExists")public ResponseEntity<Object> bucketExists(@RequestParam("bucketName") String bucketName) {return ResponseEntity.status(HttpStatus.OK).body("查看存储bucket是否存在 bucketName : " + minioUtil.bucketExists(bucketName));}/*** 制作桶** @param bucketName 存储桶名称* @return {@link ResponseEntity }<{@link Object }>*/@ApiOperation(value = "创建存储bucket")@GetMapping("/makeBucket")public ResponseEntity<Object> makeBucket(String bucketName) {return ResponseEntity.status(HttpStatus.OK).body("创建存储bucket bucketName : " + minioUtil.makeBucket(bucketName));}/*** 删除存储桶** @param bucketName 存储桶名称* @return {@link ResponseEntity }<{@link Object }>*/@ApiOperation(value = "删除存储bucket")@GetMapping("/removeBucket")public ResponseEntity<Object> removeBucket(String bucketName) {return ResponseEntity.status(HttpStatus.OK).body("删除存储bucket bucketName : " + minioUtil.removeBucket(bucketName));}/*** 获取所有存储桶** @return {@link ResponseEntity }<{@link Object }>*/@ApiOperation(value = "获取全部bucket")@GetMapping("/getAllBuckets")public ResponseEntity<Object> getAllBuckets() {List<Bucket> allBuckets = minioUtil.getAllBuckets();// 获取名称 allBuckets.forEach(bucket -> log.info(bucket.name()));List<String> bucketNames = allBuckets.stream().map(Bucket::name).collect(Collectors.toList());HashMap<String, Object> response = new HashMap<>();response.put("data", bucketNames);response.put("code", HttpStatus.OK.value());response.put("msg", "获取全部bucket成功");return ResponseEntity.status(HttpStatus.OK).body(response);}/*** 上传** @param file 文件* @return {@link ResponseEntity }<{@link Object }>*/@ApiOperation(value = "文件上传返回url")@PostMapping("/upload")public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file) throws ServerException {if (file.isEmpty()) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("文件不能为空");}String objectName = minioUtil.upload(file);if (null != objectName) {HashMap<String, Object> response = new HashMap<>();response.put("data", (minioConfig.getEndpoint() + "/" + minioConfig.getBucketName() + "/" + objectName));response.put("code", HttpStatus.OK.value());response.put("msg", "上传成功");return ResponseEntity.status(HttpStatus.OK).body(response);}return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件上传失败");}
此外,也可以不使用工具类直接进行操作,如文件上传如下:
/*** 文件上传** @param file 文件* @return 文件在Minio中的路径*/@ApiOperation(value = "文件上传返回url")@PostMapping("/upload1")public ResponseEntity<Object> upload1(MultipartFile file) {String originalFilename = file.getOriginalFilename();if (StringUtils.isBlank(originalFilename)) {throw new RuntimeException("文件名为空");}String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));// 使用UUID确保文件名唯一String fileName = UUID.randomUUID() + fileExtension;// 使用日期加上文件名,方便分类String objectName = DateUtils.getNowYyyyMmdd() + "/" + fileName;log.info("准备上传文件,objectName: {}", objectName);// 通过更大缓冲区和流优化上传文件try (InputStream fileInputStream = file.getInputStream()) {// 设置缓冲区大小,增加上传效率 // 缓冲区大小可以根据实际情况调整int bufferSize = 8192;byte[] buffer = new byte[bufferSize];// 上传前记录文件大小和类型long fileSize = file.getSize();log.info("上传文件大小:{} 字节, 文件类型:{}", fileSize, file.getContentType());PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(minioConfig.getBucketName()).object(objectName)// 使用流式上传.stream(fileInputStream, fileSize, -1).contentType(file.getContentType()).build();minioClient.putObject(objectArgs);log.info("文件上传成功,objectName: {}", objectName);HashMap<String, Object> response = new HashMap<>();response.put("data", (minioConfig.getEndpoint() + "/" + minioConfig.getBucketName() + "/" + objectName));response.put("code", HttpStatus.OK.value());response.put("msg", "上传成功");return ResponseEntity.status(HttpStatus.OK).body(response);} catch (IOException e) {log.error("上传文件失败,objectName: {}", objectName, e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件上传失败");} catch (ServerException | InsufficientDataException | ErrorResponseException | NoSuchAlgorithmException |InvalidKeyException | InvalidResponseException | XmlParserException | InternalException e) {throw new RuntimeException(e);}}
获取文件预览链接
@ApiOperation(value = "图片/视频预览")@GetMapping("/preview-file")public ResponseEntity<Object> previewFile(@RequestParam("fileName") String fileName) {// 校验文件名是否有效if (StringUtils.isBlank(fileName)) {log.warn("文件名为空,无法生成预签名 URL");return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("文件名为空,无法生成预签名 URL");}// 确保文件名包含完整的路径(例如:2025/04/28/78bda88f-ff2d-4c4f-9756-397c62c3771f.jpg)if (!fileName.contains("/")) {log.warn("请检查文件名是否正确,无法生成预签名 URL");return null;}// 创建返回 mapHashMap<String, Object> res = new HashMap<>();// 生成 1 小时有效的预签名 URLtry {GetPresignedObjectUrlArgs presignedUrlArgs = GetPresignedObjectUrlArgs.builder()// 获取 bucket 名称.bucket(minioConfig.getBucketName())// 使用完整路径.object(fileName)// 设置方法为 GET(读取文件).method(Method.GET)// 设置 URL 过期时间为 1 小时 1*60*60.expiry(3600).build();// 获取预签名 URLString url = minioClient.getPresignedObjectUrl(presignedUrlArgs);res.put("data", url);// 记录生成的 URLlog.info("生成预签名 URL 成功,fileName: {}, url: {}", fileName, url);} catch (Exception e) {log.error("生成预签名 URL 失败,fileName: {}", fileName, e);}res.put("code", HttpStatus.OK.value());res.put("msg", "上传成功");return ResponseEntity.status(HttpStatus.OK).body(res);}
好了!这样就是在项目中调用或者使用minio服务了!