在Web开发中,大文件的上传是必不可少的功能之一。本文将介绍如何使用SpringBoot整合minio实现一个简单的大文件上传网站。
项目下载
gitee:https://gitee.com/wusupweilgy/springboot-vue.git
前端:vue2、element-ui组件、axios
后端:springboot、minio、mybatis-plus、redis
基本分成3步完成上传,检查是否上传、获取分片上传的url列表和分片上传合并,所以这里我把文件上传分成了三张流程图,就是为了详细点,其实是一个流程。
检查是否上传
获取上传url
分片上传合并
redis主要作用是缓存上传任务,因为如果是断点续传,需要查询上次上传的信息去查询已上传的分片信息,只要上传没有上传的分片即可。
mysql主要作用是存储已完成上传的文件信息。如果下次上传的是同一个文件,数据库中存在就不用上传,直接返回数据库中的文件信息。
npm install spark-md5 --S
上传示例
选择文件 上传 清空
文件名 文件大小 上传进度 状态 {{ item.name }} {{ item.size | transformByte }} 等待上传 校验MD5 正在上传 上传完成 正在上传
/** * @param: fileName - 文件名称 * @param: 数据返回 1) 无后缀匹配 - false * @param: 数据返回 2) 匹配图片 - image * @param: 数据返回 3) 匹配 txt - txt * @param: 数据返回 4) 匹配 excel - excel * @param: 数据返回 5) 匹配 word - word * @param: 数据返回 6) 匹配 pdf - pdf * @param: 数据返回 7) 匹配 ppt - ppt * @param: 数据返回 8) 匹配 视频 - video * @param: 数据返回 9) 匹配 音频 - radio * @param: 数据返回 10) 其他匹配项 - other * @author: ljw **/ export function fileSuffixTypeUtil(fileName){ // 后缀获取 var suffix = ""; // 获取类型结果 var result = ""; try { var flieArr = fileName.split("."); suffix = flieArr[flieArr.length - 1]; } catch (err) { suffix = ""; } // fileName无后缀返回 false if (!suffix) { result = false; return result; } // 图片格式 var imglist = ["png", "jpg", "jpeg", "bmp", "gif"]; // 进行图片匹配 result = imglist.some(function (item) { return item == suffix; }); if (result) { result = "image"; return result; } // 匹配txt var txtlist = ["txt"]; result = txtlist.some(function (item) { return item == suffix; }); if (result) { result = "txt"; return result; } // 匹配 excel var excelist = ["xls", "xlsx"]; result = excelist.some(function (item) { return item == suffix; }); if (result) { result = "excel"; return result; } // 匹配 word var wordlist = ["doc", "docx"]; result = wordlist.some(function (item) { return item == suffix; }); if (result) { result = "word"; return result; } // 匹配 pdf var pdflist = ["pdf"]; result = pdflist.some(function (item) { return item == suffix; }); if (result) { result = "pdf"; return result; } // 匹配 ppt var pptlist = ["ppt"]; result = pptlist.some(function (item) { return item == suffix; }); if (result) { result = "ppt"; return result; } // 匹配 视频 var videolist = ["mp4", "m2v", "mkv","ogg", "flv", "avi", "wmv", "rmvb"]; result = videolist.some(function (item) { return item == suffix; }); if (result) { result = "video"; return result; } // 匹配 音频 var radiolist = ["mp3", "wav", "wmv"]; result = radiolist.some(function (item) { return item == suffix; }); if (result) { result = "radio"; return result; } // 其他 文件类型 result = "other"; return result; };
import request from '@/utils/request' //上传信息 export function uploadScreenshot(data){ return request({ url:'upload/multipart/uploadScreenshot', method:'post', data }) } //上传信息 export function uploadFileInfo(data){ return request({ url:'upload/multipart/uploadFileInfo', method:'post', data }) } // 上传校验 export function checkUpload(MD5) { return request({ url: `upload/multipart/check?md5=${MD5}`, method: 'get', }) }; // 初始化上传 export function initUpload(data) { return request({ url: `upload/multipart/init`, method: 'post', data }) }; // 初始化上传 export function mergeUpload(data) { return request({ url: `upload/multipart/merge`, method: 'post', data }) };
import axios from 'axios' import router from "@/router"; import ElementUI from "element-ui"; const request = axios.create({ baseURL: `http://localhost:9090`, timeout: 30000 }) // request 拦截器 // 可以自请求发送前对请求做一些处理 // 比如统一加token,对请求参数统一加密 request.interceptors.request.use(config => { config.headers['Content-Type'] = 'application/json;charset=utf-8'; return config }, error => { return Promise.reject(error) }); // response 拦截器 // 可以在接口响应后统一处理结果 request.interceptors.response.use( response => { let res = response.data; // 如果是返回的文件 if (response.headers === 'blob') { return res } // 兼容服务端返回的字符串数据 if (typeof res === 'string') { res = res ? JSON.parse(res) : res console.log(res) } return res; }, error => { console.log('err' + error) // for debug return Promise.reject(error) } ) export default request
/* Navicat Premium Data Transfer Source Server : local Source Server Type : MySQL Source Server Version : 80028 Source Host : localhost:3306 Source Schema : shiro_jwt_vue_file Target Server Type : MySQL Target Server Version : 80028 File Encoding : 65001 Date: 01/05/2023 13:52:06 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for files -- ---------------------------- DROP TABLE IF EXISTS `files`; CREATE TABLE `files` ( `id` int(0) NOT NULL AUTO_INCREMENT COMMENT 'id', `upload_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '分片上传uploadId', `file_md5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件md5', `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '下载链接', `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件名称', `bucket_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '桶名', `file_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件类型', `file_size` bigint(0) NULL DEFAULT NULL COMMENT '文件大小(byte)', `chunk_size` bigint(0) NULL DEFAULT NULL COMMENT '每个分片的大小(byte)', `chunk_num` int(0) NULL DEFAULT NULL COMMENT '分片数量', `is_delete` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除', `enable` tinyint(1) NULL DEFAULT 1 COMMENT '是否禁用链接', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 262 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
org.springframework.boot spring-boot-starter-weborg.projectlombok lomboktrue com.alibaba fastjson1.2.50 com.baomidou mybatis-plus-boot-starter3.5.2 mysql mysql-connector-java8.0.30 io.minio minio8.3.1 com.squareup.okhttp3 okhttp4.9.2 org.springframework.boot spring-boot-starter-data-rediscn.hutool hutool-all5.7.20 org.springframework.boot spring-boot-starter-validationorg.apache.commons commons-lang3org.springframework.boot spring-boot-starter-testtest org.springframework.boot spring-boot-dependencies${spring-boot.version} pom import
server: port: 9090 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/shiro_jwt_vue_file?serverTimezone=Asia/Shanghai&userUnicode=true&useSSL=false username: root password: mysql redis: # Redis服务器地址 host: 127.0.0.1 # Redis服务器端口号 port: 6379 # 使用的数据库索引,默认是0 database: 0 # 连接超时时间 timeout: 1800000 # 设置密码 #password: "123456" lettuce: pool: # 最大阻塞等待时间,负数表示没有限制 max-wait: -1 # 连接池中的最大空闲连接 max-idle: 5 # 连接池中的最小空闲连接 min-idle: 0 # 连接池中最大连接数,负数表示没有限制 max-active: 20 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl minio: endpoint: http://localhost:9000 accesskey: minioadmin secretkey: minioadmin expiry: 1 #分片对象过期时间 单位(天) breakpoint-time: 1 #断点续传有效时间,在redis存储任务的时间 单位(天)
package com.wusuowei.miniouploadfile.controller; import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo; import com.wusuowei.miniouploadfile.service.UploadService; import com.wusuowei.miniouploadfile.utils.MinioUtils; import com.wusuowei.miniouploadfile.utils.R; import com.wusuowei.miniouploadfile.utils.RedisUtil; import com.wusuowei.miniouploadfile.utils.RespEnum; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; /** * minio上传流程 * * 1.检查数据库中是否存在上传文件 * * 2.根据文件信息初始化,获取分片预签名url地址,前端根据url地址上传文件 * * 3.上传完成后,将分片上传的文件进行合并 * * 4.保存文件信息到数据库 */ @RestController @Slf4j @RequestMapping("upload") public class FileMinioController { @Resource private UploadService uploadService; @Resource private RedisUtil redisUtil; @Resource private MinioUtils minioUtils; /** * @description 获取上传文件 * @param fileMD5 文件md5 * @return {@link R } * @author LGY * @date 2023/04/26 16:00 */ @GetMapping("/getUploadingFile/{fileMD5}") public R getUploadingFile(@PathVariable String fileMD5) { if (StringUtils.isBlank(fileMD5)) { return R.error(); } FileUploadInfo fileUploadInfo = (FileUploadInfo) redisUtil.get(fileMD5); if (fileUploadInfo != null) { // 查询上传后的分片数据 fileUploadInfo.setChunkUploadedList(minioUtils.getChunkByFileMD5(fileUploadInfo.getFileName(), fileUploadInfo.getUploadId(), fileUploadInfo.getFileType())); return R.ok().setData(fileUploadInfo); } return R.error(); } /** * 校验文件是否存在 * * @param md5 String * @return ResponseResult
package com.wusuowei.miniouploadfile.service; import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo; import com.wusuowei.miniouploadfile.utils.R; import org.springframework.web.multipart.MultipartFile; import java.util.Map; public interface UploadService { /** * 分片上传初始化 * * @param fileUploadInfo * @return Map*/ Map initMultiPartUpload(FileUploadInfo fileUploadInfo); /** * 完成分片上传 * * @param fileUploadInfo * @return String */ String mergeMultipartUpload(FileUploadInfo fileUploadInfo); /** * 通过 md5 获取已上传的数据 * @param md5 String * @return Mono
package com.wusuowei.miniouploadfile.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.wusuowei.miniouploadfile.mapper.FilesMapper; import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo; import com.wusuowei.miniouploadfile.model.po.Files; import com.wusuowei.miniouploadfile.service.UploadService; import com.wusuowei.miniouploadfile.utils.MinioUtils; import com.wusuowei.miniouploadfile.utils.R; import com.wusuowei.miniouploadfile.utils.RedisUtil; import com.wusuowei.miniouploadfile.utils.RespEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.util.List; import java.util.Map; @Slf4j @Service public class UploadServiceImpl implements UploadService { @Resource private FilesMapper filesMapper; @Resource private MinioUtils minioUtils; @Resource private RedisUtil redisUtil; @Value("${minio.breakpoint-time}") private Integer breakpointTime; /** * 通过 md5 获取已上传的数据(断点续传) * * @param md5 String * @return Mono
package com.wusuowei.miniouploadfile.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.wusuowei.miniouploadfile.model.po.Files; /** ** Mapper 接口 *
* * @author LGY */ public interface FilesMapper extends BaseMapper{ }
package com.wusuowei.miniouploadfile.model.po; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.io.Serializable; import java.time.LocalDateTime; /** ** *
* * @author LGY */ @Data @TableName("files") public class Files implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Integer id; @TableField("upload_id") private String uploadId; @TableField("file_md5") private String fileMd5; @TableField("url") private String url; @TableField("file_name") private String fileName; @TableField("bucket_name") private String bucketName; @TableField("file_type") private String fileType; @TableField("file_size") private Long fileSize; @TableField("chunk_size") private Long chunkSize; @TableField("chunk_num") private Integer chunkNum; @TableField("is_delete") @TableLogic(value = "0",delval = "1") private Boolean isDelete; @TableField("enable") private Boolean enable; @TableField(value = "create_time", fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }
package com.wusuowei.miniouploadfile.model.vo; import lombok.Data; import lombok.experimental.Accessors; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.util.List; @Data @Accessors(chain = true) public class FileUploadInfo { @NotBlank(message = "文件名不能为空") private String fileName; @NotNull(message = "文件大小不能为空") private Long fileSize; @NotBlank(message = "Content-Type不能为空") private String contentType; @NotNull(message = "分片数量不能为空") private Integer chunkNum; @NotBlank(message = "uploadId 不能为空") private String uploadId; private Long chunkSize; // 桶名称 //private String bucketName; //md5 private String fileMd5; //文件类型 private String fileType; //已上传的分片索引+1 private ListchunkUploadedList; }
package com.wusuowei.miniouploadfile.utils; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.StrUtil; import com.google.common.collect.HashMultimap; import com.wusuowei.miniouploadfile.config.CustomMinioClient; import com.wusuowei.miniouploadfile.model.vo.FileUploadInfo; import io.minio.*; import io.minio.errors.*; import io.minio.http.Method; import io.minio.messages.Part; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import javax.annotation.PostConstruct; import java.io.IOException; import java.io.InputStream; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @Slf4j @Component public class MinioUtils { @Value(value = "${minio.endpoint}") private String endpoint; @Value(value = "${minio.accesskey}") private String accesskey; @Value(value = "${minio.secretkey}") private String secretkey; @Value(value = "${minio.expiry}") private Integer expiry; private CustomMinioClient customMinioClient; /** * 用spring的自动注入会注入失败 */ @PostConstruct public void init() { MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accesskey, secretkey) .build(); customMinioClient = new CustomMinioClient(minioClient); } /** * 单文件签名上传 * * @param objectName 文件全路径名称 * @param bucketName 桶名称 * @return / */ public MapgetUploadObjectUrl(String objectName, String bucketName) { try { log.info("tip message: 通过 <{}-{}> 开始单文件上传 ", objectName, bucketName); Map resMap = new HashMap(); List partList = new ArrayList<>(); String url = customMinioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(bucketName) .object(objectName) .expiry(expiry, TimeUnit.DAYS) .build()); log.info("tip message: 单个文件上传、成功"); partList.add(url); resMap.put("uploadId", "SingleFileUpload"); resMap.put("urlList", partList); return resMap; } catch (Exception e) { log.error("error message: 单个文件上传失败、原因:", e); // 返回 文件上传失败 return null; } } /** * 初始化分片上传 * * @param fileUploadInfo * @param objectName 文件全路径名称 * @param chunkNum 分片数量 * @param contentType 类型,如果类型使用默认流会导致无法预览 * @param bucketName 桶名称 * @return Mono > */ public Map initMultiPartUpload(FileUploadInfo fileUploadInfo, String objectName, int chunkNum, String contentType, String bucketName) { log.info("tip message: 通过 <{}-{}-{}-{}> 开始初始化<分片上传>数据", objectName, chunkNum, contentType, bucketName); Map resMap = new HashMap<>(); try { if (CharSequenceUtil.isBlank(contentType)) { contentType = "application/octet-stream"; } HashMultimap headers = HashMultimap.create(); headers.put("Content-Type", contentType); //获取uploadId String uploadId = null; if(StringUtils.isBlank(fileUploadInfo.getUploadId())){ uploadId = customMinioClient.initMultiPartUpload(bucketName, null, objectName, headers, null); }else{ uploadId = fileUploadInfo.getUploadId(); } resMap.put("uploadId", uploadId); fileUploadInfo.setUploadId(uploadId); fileUploadInfo.setChunkNum(chunkNum); List partList = new ArrayList<>(); Map reqParams = new HashMap<>(); reqParams.put("uploadId", uploadId); for (int i = 1; i <= chunkNum; i++) { reqParams.put("partNumber", String.valueOf(i)); String uploadUrl = customMinioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket(bucketName) .object(objectName) .expiry(1, TimeUnit.DAYS) .extraQueryParams(reqParams) .build()); partList.add(uploadUrl); } log.info("tip message: 文件初始化<分片上传>、成功"); resMap.put("urlList", partList); return resMap; } catch (Exception e) { log.error("error message: 初始化分片上传失败、原因:", e); // 返回 文件上传失败 return R.error(RespEnum.UPLOAD_FILE_FAILED); } } /** * 分片上传完后合并 * * @param objectName 文件全路径名称 * @param uploadId 返回的uploadId * @param bucketName 桶名称 * @return boolean */ public boolean mergeMultipartUpload(String objectName, String uploadId, String bucketName) { try { log.info("tip message: 通过 <{}-{}-{}> 合并<分片上传>数据", objectName, uploadId, bucketName); //目前仅做了最大1000分片 Part[] parts = new Part[1000]; // 查询上传后的分片数据 ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null); int partNumber = 1; for (Part part : partResult.result().partList()) { parts[partNumber - 1] = new Part(partNumber, part.etag()); partNumber++; } // 合并分片 customMinioClient.mergeMultipartUpload(bucketName, null,objectName, uploadId, parts, null, null); } catch (Exception e) { log.error("error message: 合并失败、原因:", e); //TODO删除redis的数据 return false; } return true; } /** * 通过 sha256 获取上传中的分片信息 * * @param objectName 文件全路径名称 * @param uploadId 返回的uploadId * @param bucketName 桶名称 * @return Mono > */ public List getChunkByFileMD5(String objectName, String uploadId, String bucketName) { log.info("通过 <{}-{}-{}> 查询 上传分片数据", objectName, uploadId, bucketName); try { // 查询上传后的分片数据 ListPartsResponse partResult = customMinioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null); return partResult.result().partList().stream().map(Part::partNumber).collect(Collectors.toList()); } catch (Exception e) { log.error("error message: 查询上传后的分片信息失败、原因:", e); return null; } } /** * 获取文件下载地址 * * @param bucketName 桶名称 * @param fileName 文件名 * @return */ public String getFliePath(String bucketName, String fileName) { return StrUtil.format("{}/{}/{}", endpoint, bucketName, fileName);//文件访问路径 } /** * 创建一个桶 * * @return */ public String createBucket(String bucketName) { try { BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build(); //如果桶存在 if (customMinioClient.bucketExists(bucketExistsArgs)) { return bucketName; } MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build(); customMinioClient.makeBucket(makeBucketArgs); return bucketName; } catch (Exception e) { log.error("创建桶失败:{}", e.getMessage()); throw new RuntimeException(e); } } /** * 根据文件类型获取minio桶名称 * * @param fileType * @return */ public String getBucketName(String fileType) { try { //String bucketName = getProperty(fileType.toLowerCase()); if (fileType != null && !fileType.equals("")) { //判断桶是否存在 String bucketName2 = createBucket(fileType.toLowerCase()); if (bucketName2 != null && !bucketName2.equals("")) { return bucketName2; }else{ return fileType; } } } catch (Exception e) { log.error("Error reading bucket name "); } return fileType; } /** * 读取配置文件 * * @param fileType * @return * @throws IOException */ private String getProperty(String fileType) throws IOException { Properties SysLocalPropObject = new Properties(); //判断桶关系配置文件是否为空 if (SysLocalPropObject.isEmpty()) { InputStream is = getClass().getResourceAsStream("/BucketRelation.properties"); SysLocalPropObject.load(is); is.close(); } return SysLocalPropObject.getProperty("bucket." + fileType); } /** * 文件上传 * * @param file 文件 * @return Boolean */ public String upload(MultipartFile file, String bucketName) { String originalFilename = file.getOriginalFilename(); if (StringUtils.isBlank(originalFilename)) { throw new RuntimeException(); } String objectName = file.getName(); try { PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(objectName) .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build(); //文件名称相同会覆盖 customMinioClient.putObject(objectArgs); } catch (Exception e) { e.printStackTrace(); return null; } // 查看文件地址 GetPresignedObjectUrlArgs build = new GetPresignedObjectUrlArgs().builder().bucket(bucketName).object(objectName).method(Method.GET).build(); String url = null; try { url = customMinioClient.getPresignedObjectUrl(build); } catch (ErrorResponseException e) { e.printStackTrace(); } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } return url; } // /** // * 写入配置文件 // */ // public void setProperty(String bucketName) { // String tempPath = Objects.requireNonNull(getClass().getResource("/BucketRelation.properties")).getPath(); // OutputStream os; // try { // os = new FileOutputStream(tempPath); // SysLocalPropObject.setProperty(bucketName, bucketName); // SysLocalPropObject.store(os, "Update " + bucketName + " " + bucketName); // os.close(); // } catch (IOException e) { // } // } } // @Autowired // private MinioProp minioProp; // // // @Autowired // private MinioClient minioClient; // // // // /** // * 列出所有的桶 // */ // public List listBuckets() throws Exception { // List list = minioClient.listBuckets(); // List names = new ArrayList<>(); // list.forEach(b -> { // names.add(b.name()); // }); // return names; // } // // /** // * 列出一个桶中的所有文件和目录 // */ // public List listFiles(String bucket) throws Exception { // Iterable > results = minioClient.listObjects( // ListObjectsArgs.builder().bucket(bucket).recursive(true).build()); // // List infos = new ArrayList<>(); // results.forEach(r->{ // Fileinfo info = new Fileinfo(); // try { // Item item = r.get(); // info.setFilename(item.objectName()); // info.setDirectory(item.isDir()); // infos.add(info); // } catch (Exception e) { // e.printStackTrace(); // } // }); // return infos; // } // // /** // * 下载一个文件 // */ // public InputStream download(String bucket, String objectName) throws Exception { // InputStream stream = minioClient.getObject( // GetObjectArgs.builder().bucket(bucket).object(objectName).build()); // return stream; // } // // /** // * 删除一个桶 // */ // public void deleteBucket(String bucket) throws Exception { // minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build()); // } // // /** // * 删除一个对象 // */ // public void deleteObject(String bucket, String objectName) throws Exception { // minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build()); // } // // // /** // * 创建一个桶 // */ // public void createBucket(String bucketName) { // BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build(); // MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build(); // try { // if (minioClient.bucketExists(bucketExistsArgs)) // return; // minioClient.makeBucket(makeBucketArgs); // } catch (Exception e) { // log.error("创建桶失败:{}", e.getMessage()); // throw new RuntimeException(e); // } // } // // /** // * 上传一个文件 // * @param file 文件 // * @param bucketName 存储桶 // * @return // */ // public JSONObject uploadFile(MultipartFile file, String bucketName) throws Exception { // JSONObject res = new JSONObject(); // res.put("code", 0); // // 判断上传文件是否为空 // if (null == file || 0 == file.getSize()) { // res.put("msg", "上传文件不能为空"); // return res; // } // // 判断存储桶是否存在 // createBucket(bucketName); // // 文件名 // String originalFilename = file.getOriginalFilename(); // // 新的文件名 = 存储桶名称_时间戳.后缀名 // String fileName = bucketName + "_" + System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf(".")); // // 开始上传 // InputStream inputStream = file.getInputStream(); // PutObjectArgs args = PutObjectArgs.builder().bucket(bucketName).object(fileName) // .stream(inputStream,inputStream.available(),-1).build(); // minioClient.putObject(args); // res.put("code", 1); // res.put("msg", minioProp.getEndpoint() + "/" + bucketName + "/" + fileName); // return res; // }
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @Component public class RedisUtil { @Autowired private RedisTemplate redisTemplate; /** * 指定缓存失效时间 * @param key 键 * @param time 时间(秒) * @return */ public boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据key 获取过期时间 * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效 */ public long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * @param key 键 * @return true 存在 false不存在 */ public boolean hasKey(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除缓存 * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete((Collection) CollectionUtils.arrayToList(key)); } } } // ============================String============================= /** * 普通缓存获取 * @param key 键 * @return 值 */ public Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } /** * 普通缓存放入 * @param key 键 * @param value 值 * @return true成功 false失败 */ public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 普通缓存放入并设置时间 * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * @param key 键 * @param delta 要增加几(大于0) * @return */ public long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * @param key 键 * @param delta 要减少几(小于0) * @return */ public long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } // ================================Map================================= /** * 获取list缓存的长度 * @param key 键 * @return */ public long hGetMapSize(String key) { try { return redisTemplate.opsForHash().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * HashGet * @param key 键 不能为null * @param item 项 不能为null * @return 值 */ public Object hget(String key, String item) { return redisTemplate.opsForHash().get(key, item); } /** * 获取hashKey对应的所有键值 * @param key 键 * @return 对应的多个键值 */ public Map hmget(String key) { return redisTemplate.opsForHash().entries(key); } /** * HashSet * @param key 键 * @param map 对应多个键值 * @return true 成功 false 失败 */ public boolean hmset(String key, Map map) { try { redisTemplate.opsForHash().putAll(key, map); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * HashSet 并设置时间 * @param key 键 * @param map 对应多个键值 * @param time 时间(秒) * @return true成功 false失败 */ public boolean hmset(String key, Map map, long time) { try { redisTemplate.opsForHash().putAll(key, map); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * @param key 键 * @param item 项 * @param value 值 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value) { try { redisTemplate.opsForHash().put(key, item, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 向一张hash表中放入数据,如果不存在将创建 * @param key 键 * @param item 项 * @param value 值 * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 * @return true 成功 false失败 */ public boolean hset(String key, String item, Object value, long time) { try { redisTemplate.opsForHash().put(key, item, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 删除hash表中的值 * @param key 键 不能为null * @param item 项 可以使多个 不能为null */ public void hdel(String key, Object... item) { redisTemplate.opsForHash().delete(key, item); } /** * 判断hash表中是否有该项的值 * @param key 键 不能为null * @param item 项 不能为null * @return true 存在 false不存在 */ public boolean hHasKey(String key, String item) { return redisTemplate.opsForHash().hasKey(key, item); } /** * hash递增 如果不存在,就会创建一个 并把新增后的值返回 * @param key 键 * @param item 项 * @param by 要增加几(大于0) * @return */ public double hincr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, by); } /** * hash递减 * @param key 键 * @param item 项 * @param by 要减少记(小于0) * @return */ public double hdecr(String key, String item, double by) { return redisTemplate.opsForHash().increment(key, item, -by); } // ============================set============================= /** * 根据key获取Set中的所有值 * @param key 键 * @return */ public Set sGet(String key) { try { return redisTemplate.opsForSet().members(key); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 根据value从一个set中查询,是否存在 * @param key 键 * @param value 值 * @return true 存在 false不存在 */ public boolean sHasKey(String key, Object value) { try { return redisTemplate.opsForSet().isMember(key, value); } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将数据放入set缓存 * @param key 键 * @param values 值 可以是多个 * @return 成功个数 */ public long sSet(String key, Object... values) { try { return redisTemplate.opsForSet().add(key, values); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 将set数据放入缓存 * @param key 键 * @param time 时间(秒) * @param values 值 可以是多个 * @return 成功个数 */ public long sSetAndTime(String key, long time, Object... values) { try { Long count = redisTemplate.opsForSet().add(key, values); if (time > 0) { expire(key, time); } return count; } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 获取set缓存的长度 * @param key 键 * @return */ public long sGetSetSize(String key) { try { return redisTemplate.opsForSet().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 移除值为value的 * @param key 键 * @param values 值 可以是多个 * @return 移除的个数 */ public long setRemove(String key, Object... values) { try { Long count = redisTemplate.opsForSet().remove(key, values); return count; } catch (Exception e) { e.printStackTrace(); return 0; } } // ===============================list================================= /** * 获取list缓存的内容 * @param key 键 * @param start 开始 * @param end 结束 0 到 -1代表所有值 * @return */ public List lGet(String key, long start, long end) { try { return redisTemplate.opsForList().range(key, start, end); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取list缓存的长度 * @param key 键 * @return */ public long lGetListSize(String key) { try { return redisTemplate.opsForList().size(key); } catch (Exception e) { e.printStackTrace(); return 0; } } /** * 通过索引 获取list中的值 * @param key 键 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 * @return */ public Object lGetIndex(String key, long index) { try { return redisTemplate.opsForList().index(key, index); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, Object value) { try { redisTemplate.opsForList().rightPush(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, Object value, long time) { try { redisTemplate.opsForList().rightPush(key, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * @param key 键 * @param value 值 * @return */ public boolean lSet(String key, List value) { try { redisTemplate.opsForList().rightPushAll(key, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 将list放入缓存 * * @param key 键 * @param value 值 * @param time 时间(秒) * @return */ public boolean lSet(String key, List value, long time) { try { redisTemplate.opsForList().rightPushAll(key, value); if (time > 0) { expire(key, time); } return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 根据索引修改list中的某条数据 * @param key 键 * @param index 索引 * @param value 值 * @return */ public boolean lUpdateIndex(String key, long index, Object value) { try { redisTemplate.opsForList().set(key, index, value); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 移除N个值为value * @param key 键 * @param count 移除多少个 * @param value 值 * @return 移除的个数 */ public long lRemove(String key, long count, Object value) { try { Long remove = redisTemplate.opsForList().remove(key, count, value); return remove; } catch (Exception e) { e.printStackTrace(); return 0; } } }
import com.google.common.collect.Multimap; import io.minio.CreateMultipartUploadResponse; import io.minio.ListPartsResponse; import io.minio.MinioClient; import io.minio.ObjectWriteResponse; import io.minio.errors.*; import io.minio.messages.Part; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; public class CustomMinioClient extends MinioClient { /** * 继承父类 * @param client */ public CustomMinioClient(MinioClient client) { super(client); } /** * 初始化分片上传、获取 uploadId * * @param bucket String 存储桶名称 * @param region String * @param object String 文件名称 * @param headers Multimap请求头 * @param extraQueryParams Multimap * @return String */ public String initMultiPartUpload(String bucket, String region, String object, Multimap headers, Multimap extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException { CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams); return response.result().uploadId(); } /** * 合并分片 * * @param bucketName String 桶名称 * @param region String * @param objectName String 文件名称 * @param uploadId String 上传的 uploadId * @param parts Part[] 分片集合 * @param extraHeaders Multimap * @param extraQueryParams Multimap * @return ObjectWriteResponse */ public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap extraHeaders, Multimap extraQueryParams) throws IOException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException, ServerException, InvalidKeyException { return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams); } /** * 查询当前上传后的分片信息 * * @param bucketName String 桶名称 * @param region String * @param objectName String 文件名称 * @param maxParts Integer 分片数量 * @param partNumberMarker Integer 分片起始值 * @param uploadId String 上传的 uploadId * @param extraHeaders Multimap * @param extraQueryParams Multimap * @return ListPartsResponse */ public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap extraHeaders, Multimap extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException { return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams); } }
import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @description Web应用程序配置 * @author LGY * @date 2023/03/14 20:09 * @version 1.0.0 */ @Configuration public class WebAppConfigurer implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowCredentials(true) .allowedOriginPatterns("*") .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE") .allowedHeaders("*"); } //实体类属性为空时不进行序列化返回给前端 @Bean public HttpMessageConverters fastJsonHttpMessageConverters() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat); fastConverter.setFastJsonConfig(fastJsonConfig); HttpMessageConverter> converter = fastConverter; return new HttpMessageConverters(converter); } }
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.time.LocalDateTime; /** * MybatisPlus配置类 * */ @Slf4j @Configuration @MapperScan("com.wusuowei.miniouploadfile.mapper") public class MybatisPlusConfig implements MetaObjectHandler { /** * 新的分页插件 * 需要设置 MybatisConfiguration#useDeprecatedExecutor = false * 避免缓存出现问题(该属性会在旧插件移除后一同移除) */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } @Override public void insertFill(MetaObject metaObject) { log.info("start insert fill ...."); this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用) this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`) /* 上面选其一使用,下面的已过时(注意 strictInsertFill 有多个方法,详细查看源码) */ } @Override public void updateFill(MetaObject metaObject) { log.info("start update fill ...."); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用) this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`) /* 上面选其一使用,下面的已过时(注意 strictUpdateFill 有多个方法,详细查看源码) */ }
package com.wusuowei.miniouploadfile.utils; import com.alibaba.fastjson.JSON; import java.util.HashMap; import java.util.Map; /** * 返回数据 * * @author Mark sunlightcs@gmail.com */ public class R extends HashMap{ private static final long serialVersionUID = 1L; public R setData(Object data) { put("data",data); return this; } //利用fastjson进行反序列化 public T getData(Class typeReference) { Object data = get("data"); //默认是map String jsonString = JSON.toJSONString(data); T t = JSON.parseObject(jsonString, typeReference); return t; } public R() { put("code", 200); put("msg", "success"); } public static R error() { return error(5000, "未知异常,请联系管理员"); } public static R error(String msg) { return error(500, msg); } public static R error(RespEnum respEnum) { return error(respEnum.getCode(), respEnum.getMessage()); } public static R error(int code, String msg) { R r = new R(); r.put("code", code); r.put("msg", msg); return r; } public static R ok(RespEnum respEnum) { return ok(respEnum.getCode(), respEnum.getMessage()); } public static R ok(String msg) { R r = new R(); r.put("msg", msg); return r; } public static R ok(int code, String msg) { R r = new R(); r.put("code", code); r.put("msg", msg); return r; } public static R ok(Map map) { R r = new R(); r.putAll(map); return r; } public static R ok() { return new R(); } public R put(String key, Object value) { super.put(key, value); return this; } public Integer getCode() { return (Integer) this.get("code"); } }
package com.wusuowei.miniouploadfile.utils; public enum RespEnum { UPLOADSUCCESSFUL(1, "上传成功"), UPLOADING(2, "上传中"), NOT_UPLOADED(3, "未上传"), ACCESS_PARAMETER_INVALID(1001,"访问参数无效"), UPLOAD_FILE_FAILED(1002,"文件上传失败"), DATA_NOT_EXISTS(1003,"数据不存在"), ; private final Integer code; private final String message; public Integer getCode() { return code; } public String getMessage() { return message; } RespEnum(Integer code, String message) { this.code = code; this.message = message; } }
文章贴的代码有点多了,大家可以去我的gitee上下载下来运行。项目的功能我测试优化了很多次,如果代码有何异常或者不完整欢迎在评论区留言。如果这篇文章有幸帮助到你,希望读者大大们可以给作者点个赞呀😶🌫️😶🌫️😶🌫️