在前面的一篇文章中,详细的给大家介绍了如何在Centos7.x环境下从零开始搭建分布式文件系统MinIo,错过的朋友可以点这里:从零搭建分布式文件系统MinIO比FastDFS要更合适
那么继上一篇文章之后,本篇文章将带领大家开始实战,选用当下主流的SpringBoot2.x框架来整合分布式文件系统MinIo,关于MinIo的使用,接下来我们一探究竟。
关于SpringBoot整合MinIo 以及Minio相关操作的API可参考官方文档,官方文档给出了详细的API解释:
参考: http://docs.minio.org.cn/docs/master/java-client-quickstart-guide
参考: https://docs.min.io/cn/java-client-api-reference.html
Minio支持接入JavaScript、Java、Python、Golang等多种语言,这里我们选择最熟悉的Java语言,使用最流行的框架 SpringBoot 2.x。
完整项目结构图
io.minio minio ${minio.version}
4.0.0 org.springframework.boot spring-boot-starter-parent 2.4.0 com.example springboot-minio 0.0.1-SNAPSHOT springboot-minio SpringBoot 系列教程(一百零四):SpringBoot 整合 MinIo 文件服务 1.8 org.springframework.boot spring-boot-starter-web org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test io.minio minio 7.0.2 com.alibaba fastjson 1.2.7 org.springframework.boot spring-boot-starter-thymeleaf org.apache.commons commons-lang3 3.9 cn.hutool hutool-core 5.4.4 org.springframework.boot spring-boot-maven-plugin
这时候需要在application.yml文件中配置关于minio文件服务器的相关配置参数
#服务端口server: port: 8899#minio配置minio: #minio的服务端访问地址 endpoint: http://192.168.15.135:9000 #账号 accessKey: minioadmin #密码 secretKey: minioadmin #桶名称(根目录文件夹名称) bucketName: dev
将minio的属性配置信息映射到Java配置类,并注册成Bean, 如下:
package com.example.minio.properties;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration;/** * @desc: minio的属性配置信息映射到Java配置类,并注册成Bean * @author: cao_wencao * @date: 2020-11-19 17:13 */@Data@Configuration@ConfigurationProperties(prefix = "minio")public class MinIoProperties { /** * 服务地址 */ private String endpoint; /** * 用户名 */ private String accessKey; /** * 密码 */ private String secretKey; /** * 桶名称 */ private String bucketName;}
根据Minio官方API文档封装MinIo工具类,如下:
package com.example.minio.config;import com.example.minio.properties.MinIoProperties;import io.minio.MinioClient;import io.minio.PutObjectOptions;import io.minio.Result;import io.minio.messages.Bucket;import io.minio.messages.Item;import lombok.SneakyThrows;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import java.io.InputStream;import java.util.List;/** * http://docs.minio.org.cn/docs/master/java-client-api-reference * 参考:https://www.jianshu.com/p/7f493105b2b2 * * @desc: 根据Minio官方API文档封装MinIo工具类 * @author: cao_wencao * @date: 2020-11-19 17:16 */@Component@EnableConfigurationProperties(value = {MinIoProperties.class})@ConditionalOnProperty(prefix = "minio", name = {"endpoint", "accessKey", "secretKey", "bucketName"})public class MinIoUtils { private final MinioClient minioClient; private final String bucketName; @Autowired private MinIoProperties minIoProperties; /** * 初始化MinioClient */ @SneakyThrows public MinIoUtils(MinIoProperties minIoProperties) { if (!StringUtils.hasText(minIoProperties.getBucketName())) { throw new RuntimeException("bucket name can not be empty or null"); } this.bucketName = StringUtils.trimTrailingCharacter(minIoProperties.getBucketName().trim(), '/'); minioClient = new MinioClient(minIoProperties.getEndpoint(), minIoProperties.getAccessKey(), minIoProperties.getSecretKey()); } /** * 判断bucket是否存在 * * @param bucketName bucket名称 * @return */ @SneakyThrows public boolean bucketExists(String bucketName) { return minioClient.bucketExists(bucketName); } /** * 创建bucket * * @param bucketName bucket名称 */ @SneakyThrows public void makeBucket(String bucketName) { boolean isExist = minioClient.bucketExists(bucketName); if (!isExist) { minioClient.makeBucket(bucketName); } } /** * 获取全部bucket */ @SneakyThrows public List getAllBuckets() { return minioClient.listBuckets(); } /** * 判断文件是否存在 * * @param objectName 文件名称 * @return */ public boolean objectExists(String objectName) { boolean isExists = false; try { InputStream inputStream = minioClient.getObject(bucketName, objectName); if (null != inputStream) { isExists = true; } return isExists; } catch (Exception e) { throw new RuntimeException(e); } } /** * 获取单个文件 * * @param objectName 文件名称 * @return */ @SneakyThrows public InputStream getObject(String objectName) { return minioClient.getObject(bucketName, objectName); } /** * 获取文件集合 * * @param bucketN bucket名称 * @return */ @SneakyThrows public Iterable> listObjects(String bucketN) { if (StringUtils.hasText(bucketN)) { bucketN = bucketName; } return minioClient.listObjects(bucketN); } /** * 根据prefix获取object集合 * * @param prefix * @return */ @SneakyThrows public Iterable> listObjectsByPrefix(String prefix) { return minioClient.listObjects(bucketName, prefix); } /** * 获取文件外链 * * @param objectName 文件名称 * @return */ @SneakyThrows public String buildObjectUrl(String objectName) { if (!StringUtils.hasText(objectName)) { throw new RuntimeException("object name can not be empty or null"); } objectName = StringUtils.trimLeadingCharacter(StringUtils.trimTrailingCharacter(objectName.trim(), '/'), '/'); String objectUrl = String.format("%s/%s", bucketName, objectName); if (StringUtils.hasText(minIoProperties.getEndpoint())) { objectUrl = String.format("%s/%s", minIoProperties.getEndpoint(), objectUrl); } return objectUrl; } /** * 上传文件-InputStream * * @param bucketName bucket名称 * @param objectName 文件名称 * @param stream 文件流 */ @SneakyThrows public void putObject(String bucketName, String objectName, InputStream stream) { minioClient.putObject(bucketName, objectName, stream, new PutObjectOptions(stream.available(), -1)); } /** * 上传文件-InputStream * * @param objectName 文件名称 * @param stream 文件流 */ @SneakyThrows public void putObject(String objectName, InputStream stream) { minioClient.putObject(bucketName, objectName, stream, new PutObjectOptions(stream.available(), -1)); } /** * 上传文件 * * @param objectName 文件名称 * @param stream 文件流 * @param size 大小 * @param contentType 类型 * @return the object url string */ @SneakyThrows public String putObject(String objectName, InputStream stream, Long size, String contentType) { if (!StringUtils.hasText(objectName)) { throw new RuntimeException("object name can not be empty or null"); } minioClient.putObject(bucketName, objectName, stream, new PutObjectOptions(stream.available(), -1)); return buildObjectUrl(objectName); } /** * 删除文件 * * @param bucketName bucket名称 * @param objectName 文件名称 */ @SneakyThrows public void removeObject(String bucketName, String objectName) { minioClient.removeObject(bucketName, objectName); } /** * 删除文件 * * @param objectName 文件名称 */ @SneakyThrows public void removeObject(String objectName) { minioClient.removeObject(bucketName, objectName); }}
根据已经封装好的MinioUtils工具类,我们写几个简单的图片上传、图片删除、图片下载、获取图片信息、图片预览功能,如下Api接口所示:
package com.example.minio.controller;import cn.hutool.core.date.DateUtil;import com.example.minio.config.MinIoUtils;import com.example.minio.result.RestResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;import javax.servlet.ServletOutputStream;import javax.servlet.http.HttpServletResponse;import java.io.ByteArrayOutputStream;import java.io.InputStream;/** * @desc: 文件上传工具类 * @author: cao_wencao * @date: 2020-11-19 22:00 */@RestController@Slf4jpublic class MinioFileController { @Autowired private MinIoUtils minIoUtils; /** * 创建资源桶 * @param bucketName * @return */ @GetMapping(value = "/createBucket") public RestResponse createBucket(String bucketName){ if (!minIoUtils.bucketExists(bucketName)) { minIoUtils.makeBucket(bucketName); return RestResponse.success("桶创建成功"); } return RestResponse.fail("该桶已存在,请勿重复创建"); } /** * @desc: 上传图片 * @auth: cao_wencao * @date: 2020/11/19 22:22 */ @PostMapping(value = "uploadImg") public RestResponse uploadImage(@RequestParam("file") MultipartFile file) throws Exception { if (file.isEmpty()) { throw new RuntimeException("上传文件不能为空"); } else { // 得到文件流 final InputStream inputStream = file.getInputStream(); // 文件名 final String originalFilename = file.getOriginalFilename(); String filelName = "bucketName" + "_" + System.currentTimeMillis() + "wechat.jpg".substring("wechat.jpg".lastIndexOf(".")); String objectName = String.format("images/%s/%s", DateUtil.today(),+ System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf("."))); log.info("【objectName的名称】:{}", objectName); //String objectName = String.format("upload/images/%s/%s/%s", DateUtil.today(), // UUID.randomUUID().toString().replace("-", "").substring(0, 10), // originalFilename); // 把文件放到minio的boots桶里面 minIoUtils.putObject(objectName, inputStream); // 关闭输入流 inputStream.close(); return RestResponse.success("上传成功"); } } /** * 获取图片的URL访问地址 * @param objectName * @return */ @RequestMapping("/getObjectURL") public RestResponse getObjectURL(String objectName){ String url = minIoUtils.buildObjectUrl(objectName); return RestResponse.success(url); } /** * 删除文件 * @param objectName * @return */ @GetMapping(value = "removeImg") public RestResponse removeImage(@RequestParam("objectName") String objectName) { if (!StringUtils.hasText(objectName)){ throw new RuntimeException("资源名称不能为空"); } minIoUtils.removeObject(objectName); return RestResponse.success("删除成功"); } /** * 下载文件到本地 * @param objectName * @param response * @return * @throws Exception */ @GetMapping("/downloadImage") public RestResponse downloadImage(@RequestParam("objectName") String objectName, HttpServletResponse response) throws Exception { ResponseEntity responseEntity = null; InputStream stream = null; ByteArrayOutputStream output = null; try { // 获取"myobject"的输入流。 stream = minIoUtils.getObject(objectName); if (stream == null) { System.out.println("文件不存在"); } //用于转换byte output = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int n = 0; while (-1 != (n = stream.read(buffer))) { output.write(buffer, 0, n); } byte[] bytes = output.toByteArray(); //设置header HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("Accept-Ranges", "bytes"); httpHeaders.add("Content-Length", bytes.length + "");// objectName = new String(objectName.getBytes("UTF-8"), "ISO8859-1"); //把文件名按UTF-8取出并按ISO8859-1编码,保证弹出窗口中的文件名中文不乱码,中文不要太多,最多支持17个中文,因为header有150个字节限制。 httpHeaders.add("Content-disposition", "attachment; filename=" + objectName); httpHeaders.add("Content-Type", "text/plain;charset=utf-8");// httpHeaders.add("Content-Type", "image/jpeg"); responseEntity = new ResponseEntity(bytes, httpHeaders, HttpStatus.CREATED); } catch (Exception e) { e.printStackTrace(); } finally { if (stream != null) { stream.close(); } if (output != null) { output.close(); } } return RestResponse.success(responseEntity); } /** * 在浏览器预览图片,预览功能,需要设置contextType = “image/jpeg” * @param objectName * @param response * @throws Exception */ @RequestMapping("/preViewPicture") public void preViewPicture(@RequestParam("objectName") String objectName, HttpServletResponse response) throws Exception{ response.setContentType("image/jpeg"); ServletOutputStream out = null; try { out = response.getOutputStream(); InputStream stream = minIoUtils.getObject(objectName); ByteArrayOutputStream output = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; int n = 0; while (-1 != (n = stream.read(buffer))) { output.write(buffer, 0, n); } byte[] bytes = output.toByteArray(); out.write(bytes == null ? new byte[0]:bytes); out.flush(); }finally { if (out != null) { out.close(); } } }}
总所周知,Rest风格的API接口在返回数据到客户端时,需要统一返回数据格式,这既是规范也是基本要求,封装的 REST接口工具类如下:
package com.example.minio.result;import java.io.Serializable;/** * REST接口统一返回数据工具类封装RestResponse * @param */public class RestResponse implements Serializable { private static final long serialVersionUID = 3728877563912075885L; private int code; private String msg; private T data; public RestResponse(){ } public RestResponse(int code, String message, T data) { this.code = code; this.setMsg(message); this.data = data; } public RestResponse(int code, T data) { this.code = code; this.data = data; } public RestResponse(int code, String message) { this.code = code; this.setMsg(message); } /** * 成功时-返回data * @param * @return */ public static RestResponse success(T data){ return new RestResponse(200, null, data); } /** * 成功-不返回data * @param * @return */ public static RestResponse success(String msg){ return new RestResponse(200, msg); } /** * 成功-返回data+msg * @param * @return */ public static RestResponse success(String msg, T data){ return new RestResponse(200, msg, data); } /** * 失败 * @param * @return */ public static RestResponse fail(String msg){ return new RestResponse(500, msg,null); } /** * 失败-code * @param * @return */ public static RestResponse fail(int code, String msg){ return new RestResponse(code, msg,null); } public int getCode() { return code; } public String getMsg() { return msg; } public T getData() { return data; } public void setCode(int code) { this.code = code; } public void setMsg(String msg) { this.msg = msg; } public void setData(T data) { this.data = data; } @Override public String toString() { return "RestResponse{" + "code=" + code + ", msg='" + msg + ''' +", data=" + data +'}'; }}
html> 测试minio上传 文件:
import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;/** * @desc: 页面跳转 * @author: cao_wencao * @date: 2020-11-20 10:53 */@Controllerpublic class PageController { @RequestMapping("/toPage") public String toPage(){ return "upload"; }}
在这里插入图片描述
访问上传图片的页面后,选择一张图片进行上传,上传完成后,登录控制台查看上传成功的文件如下所示:
在这里插入图片描述
https://docs.min.io/cn/java-client-quickstart-guide.html
https://docs.min.io/cn/java-client-api-reference.html
https://github.com/Thinkingcao/SpringBootLearning/tree/master/springboot-minio