MinIo使用二 SpringBoot集成MinIo+Mybatis-plus 实现文件增删改查
- 1. 引入SpringBoot+MinIo+Mybatis-plus依赖
<dependencies> <!--spring集成使用--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.3.0.RELEASE</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.20</version> </dependency> <!--Spring 单元测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>2.3.0.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>RELEASE</version> </dependency> <!-- MinIO 客户端 https://min.io/download#/windows--> <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.3.3</version> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.8.1</version> </dependency> <!-- mybatis-plus依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> <scope>runtime</scope> </dependency> <!--数据库连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!--Swagger 相关--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <!-- 协助@EnableAspectJAutoProxy(proxyTargetClass = true) 用来处理Service层直接使用类注入到Spring容器 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.1</version> </dependency> <dependency> <groupId>org.apache.skywalking</groupId> <artifactId>apm-toolkit-logback-1.x</artifactId> <version>8.8.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
- 2. application.yml 配置文件
server: port: 6011 # 配置存储MinIO minio: minioUrl: http://127.0.0.1:9000/ accessKey: root secretKey: root123456 passagerBucket: passager driverBucket: driver systemBucket: system downLoadPath: /data/minIo/tmp/downLoad spring: servlet: multipart: max-file-size: 5MB max-request-size: 15MB datasource: url: jdbc:mysql://127.0.0.1:3306/file?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT username: root password: sjftSQL@123456 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
- 3. sql建表语句+实体类+mapper
create DATABASE `file`; use `file`; drop table IF EXISTS `file_info`; CREATE TABLE `file_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(500) NOT NULL COMMENT '文件名称', `minio_url` varchar(500) NOT NULL COMMENT 'MinIO存储url', `bucket_name` varchar(200) NOT NULL COMMENT 'MinIO桶名称', `file_size` int(11) DEFAULT NULL COMMENT '文件大小 --字节单位', `upload_time` datetime DEFAULT NOW() COMMENT '上传时间', `upload_phone` varchar(200) DEFAULT NULL, `is_deleted` int(11) NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '文件管理表';
/** * @description: 文件信息 * @author: dyg * @date: 2022/4/14 19:19 */ @Data @NoArgsConstructor @AllArgsConstructor @Builder @TableName(value = "file_info") public class FileInfo implements Serializable { private static final long serialVersionUID = 6943641372262086829L; @TableId(type = IdType.AUTO) private Integer id; /** * 文件名称 */ @NotNull(message = "文件名称不能为空") @NotEmpty(message = "文件名称不能为空") private String name; /** * MinIo中访问Url */ private String minioUrl; /** * 桶名称 */ @NotNull(message = "桶名称不能为空") @NotEmpty(message = "桶名称不能为空") private String bucketName; /** * 文件大小 */ private Long fileSize; /** * 上传时间 */ private Date uploadTime; /** * 是否删除 * 1-否 0-是 */ @TableLogic private Integer isDeleted; /** * 上传人 */ private String uploadPhone; }
@Repository public interface FileInfoMapper extends BaseMapper<FileInfo> {}
- 4. MinIo+Mybatis-Plus+跨域+Swagger配置文件
/** * @description: MinIO配置 * @author: dyg * @date: 2022/4/14 18:59 */ @Data @Component @ConfigurationProperties(prefix = "minio") @Slf4j public class MinIoConfig { /** * 服务器地址 * -- 代理地址 使用nginx代理 */ private String minioUrl; /** * 用户名 */ private String accessKey; /** * 密码 */ private String secretKey; /** * 乘客端文件上传保存桶名 */ private String passagerBucket; /** * 司机端文件上传保存桶名 */ private String driverBucket; /** * 后端营销系统上传文件保存桶名 */ private String systemBucket; @Bean public MinioClient minioClient(){ log.info("初始化MinioClient客户端:minioUrl:" + minioUrl + ",accessKey:" + accessKey + ",secretKey:" + secretKey); MinioClient minioClient = MinioClient.builder() .endpoint(minioUrl) .credentials(accessKey, secretKey) .build(); return minioClient; } @PostConstruct public void createBucket(){ MinioClient minioClient = minioClient(); try{ if(!minioClient.bucketExists(BucketExistsArgs.builder().bucket(passagerBucket).build())){ minioClient.makeBucket(MakeBucketArgs.builder().bucket(passagerBucket).build()); log.info("----成功创建MinIO-乘客端的桶{}",passagerBucket); } if(!minioClient.bucketExists(BucketExistsArgs.builder().bucket(driverBucket).build())){ minioClient.makeBucket(MakeBucketArgs.builder().bucket(driverBucket).build()); log.info("----成功创建MinIO-司机端的桶{}",driverBucket); } if(!minioClient.bucketExists(BucketExistsArgs.builder().bucket(systemBucket).build())){ minioClient.makeBucket(MakeBucketArgs.builder().bucket(systemBucket).build()); log.info("----成功创建MinIO-后台营销系统的桶{}",systemBucket); } }catch (Exception e){ log.error("-----初始化创建MinIo桶、创建临时下载目录 发生异常{} ",e); } } }
/** * @description: MyBatisPlus配置类 * @author: dyg * @date: 2021/8/14 13:44 */ @Configuration @EnableTransactionManagement public class MyBatisConfig { /** * 汇总版--分页插件+乐观锁插件 * @return */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }
/** * 整合SpringBoot Swagger2 不用在手写接口文档 * http://localhost:6011/swagger-ui.html */ @Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() // 你需要生成文档所在的包 .apis(RequestHandlerSelectors.withClassAnnotation(Api.class)) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() // 文档标题 .title("minio文件服务器-接口平台") // 描述信息 .description("web接口") .version("1.0") .build(); } }
/** * Web 配置 * * @EnableAspectJAutoProxy(proxyTargetClass = true) 用来处理Service层直接使用类注入到Spring容器 */ @Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) @Slf4j public class WebMvcConfig implements WebMvcConfigurer {/** * 跨域允许处理 * * @param registry */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600) .allowedHeaders("*"); } }
- 5. 接口返回结果封装类 Result
/** * @description: Web接口返回信息 * @author: dyg * @date: 2022/3/22 17:39 */ @Data @Builder @ApiModel(value = "接口返回参数") @AllArgsConstructor @NoArgsConstructor public class Result implements Serializable { public static final int SUCC_CODE = 200; public static final int FAIL_CODE = 9999; public static final String SUCC_STATUS = "succ"; public static final String FAIL_STATUS = "fail"; /** * 返回码 * - 200 正常返回 * - 9999 异常返回 */ @ApiModelProperty(name = "返回码", dataType = "int", notes = "200 正常返回", required = false) private int code; /** * 返回状态 * - succ : 成功 * - fail : 失败 */ @ApiModelProperty(name = "返回状态", dataType = "string", notes = "返回状态", required = false) private String status; /** * 返回异常信息 * - 200 为空 * - 9999 异常信息 */ @ApiModelProperty(name = "返回异常信息", dataType = "string", notes = "200 为空 非200-异常信息 ", required = false) private String msg; /** * 返回数据 */ @ApiModelProperty(name = "返回数据", dataType = "object", notes = "返回数据", required = false) private Object data; @ApiModelProperty(name = "返回分页数据",dataType = "list",notes = "返回分页数据", required = false) private List pageData; /** * 分页数据相关 */ @ApiModelProperty(name = "总条数", dataType = "int", notes = "总条数", required = false) private Integer count; @ApiModelProperty(name = "每页条数", dataType = "int", notes = "每页条数", required = false) private Integer pageSize; @ApiModelProperty(name = "页数", dataType = "int", notes = "页数", required = false) private Integer pageIndex; /** * 不带数据的成功调用 * * @return */ public static Result okWithOutData() { return Result.builder() .code(SUCC_CODE) .status(SUCC_STATUS) .msg("") .build(); } /** * 不带数据的异常调用 * * @param message * @return */ public static Result errorWithOutData(String message) { return Result.builder() .code(FAIL_CODE) .status(FAIL_STATUS) .msg(message) .build(); } /** * 带数据不支持分页的成功调用 * * @param val * @return */ public static Result okWithDataNoPage(Object val) { return Result.builder() .code(SUCC_CODE) .status(SUCC_STATUS) .data(val) .build(); } public static Result okWithDataNoPage(String message,Object val) { return Result.builder() .code(SUCC_CODE) .status(SUCC_STATUS) .data(val) .msg(message) .build(); } }
- 6. Service 层代码
/** * @description: 文件处理相关 * @author: dyg * @date: 2022/4/15 8:59 */ @Service @Slf4j public class FileInfoService extends ServiceImpl<FileInfoMapper, FileInfo> { @Autowired MinioClient minioClient; @Autowired private MinIoConfig minIoConfig; /** * 上传文件保存信息 * * @param file * @param type * @param phone * @return 文件在MinIO访问地址 */ @Transactional(rollbackFor = Exception.class) public Result uploadFileAndSaveInfoIntoDb(MultipartFile file, Integer type, String phone) { try { String bucketName = ""; if (1 == type.intValue()) { bucketName = minIoConfig.getPassagerBucket(); } else if ("2 == type.intValue())) { bucketName = minIoConfig.getDriverBucket(); } else { return Result.errorWithOutData("请选择 上传文件类型 客户文件-passager 司机文件-driver"); } String originalFilename = file.getOriginalFilename(); String name = file.getName(); long size = file.getSize(); // 是否要有文件大小限制 String fileName = System.currentTimeMillis() + "-" + originalFilename; String url = uploadFile(minioClient, bucketName, file, fileName); FileInfo fileInfo = FileInfo.builder() .name(fileName) .uploadTime(new Date()) .bucketName(bucketName) .fileSize(size) .minioUrl(url) .uploadPhone(phone) .build(); this.save(fileInfo); return Result.okWithDataNoPage(fileInfo); } catch (Exception e) { log.error("----调用上传文件接口发生异常 {} ", e); return Result.errorWithOutData(e.toString()); } } /** * 预览文件 * * @param id * @return */ public Result viewFileById(Integer id) { FileInfo fileInfo = this.getById(id); if (Objects.isNull(fileInfo)) { return Result.errorWithOutData("当前Id未查询到对应的文件"); } String minIoUrl = fileInfo.getMinioUrl(); if (StringUtils.isEmpty(minIoUrl)) { return Result.errorWithOutData("当前Id查询到文件获取到的url为空"); } return Result.okWithDataNoPage(minIoUrl); } /** * 删除文件 * * @param id * @return */ public Result removeFileById(Integer id) { try { FileInfo fileInfo = this.getById(id); if (Objects.isNull(fileInfo)) { return Result.errorWithOutData("当前Id未查询到对应的文件"); } String bucketName = fileInfo.getBucketName(); String name = fileInfo.getName(); deleteFile(minioClient, bucketName, name); this.removeById(id); return Result.okWithOutData(); } catch (Exception e) { log.error("---删除文件{} 发生异常{} ", id, e); return Result.errorWithOutData(e.getMessage()); } } /** * 文件下载 * * @param response * @param id */ public void downLoadFile(HttpServletResponse response, Integer id) throws Exception { FileInfo fileInfo = this.getById(id); if (Objects.isNull(fileInfo)) { throw new Exception("当前Id未查询到对应的文件"); } String minIoUrl = fileInfo.getMinioUrl(); if (StringUtils.isEmpty(minIoUrl)) { throw new Exception("当前Id查询到文件获取到的url为空"); } String name = fileInfo.getName(); String bucketName = fileInfo.getBucketName(); try (InputStream ism = new BufferedInputStream(minioClient.getObject(GetObjectArgs.builder() .bucket(bucketName) .object(name).build()))) { /** * 调用statObject()来判断对象是否存在。 - 如果不存在, statObject()抛出异常, - 否则则代表对象存在 */ minioClient.statObject(StatObjectArgs.builder() .bucket(bucketName) .object(name).build()); byte buf[] = new byte[1024]; int length = 0; response.reset(); /** * Content-disposition 是 MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件。 - Content-disposition其实可以控制用户请求所得的内容存为一个文件的时候提供一个默认的文件名, - 文件直接在浏览器上显示或者在访问时弹出文件下载对话框。 */ response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(name, "UTF-8")); response.setContentType("application/x-msdownload"); response.setCharacterEncoding("utf-8"); OutputStream osm = new BufferedOutputStream(response.getOutputStream()); while ((length = ism.read(buf)) > 0) { osm.write(buf, 0, length); } osm.close(); } catch (Exception ex) { ex.printStackTrace(); } } }
- 7. 工具类
/** * @description: MinIO文件服务器工具类 * @author: dyg * @date: 2022/4/14 19:03 */ @Slf4j public class FileServerOptUtil { /** * 判断MinIO中桶是否存在 * * @param bucketName * @return */ public static boolean bucketExist(MinioClient minioClient, String bucketName) throws Exception { try { return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); } catch (Exception e) { log.error("---判断桶{}在MinIo服务器是否存在发生异常 {}", bucketName, e); throw new Exception(e); } } /** * 创建桶 * * @param bucketName * @return */ public static boolean createBucket(MinioClient minioClient, String bucketName) throws Exception { try { if (!bucketExist(minioClient, bucketName)) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); return true; } log.error("----MinIo上当前桶{}存在", bucketName); return false; } catch (Exception e) { log.error("---在MinIo上创建桶{}发生异常{}", bucketName, e); throw new Exception(e); } } /** * 指定桶中上传文件 * * @param bucketName * @param file * @return */ public static String uploadFile(MinioClient minioClient, String bucketName, MultipartFile file, String fileName) throws Exception { try { InputStream inputStream = file.getInputStream(); PutObjectArgs putObjectArgs = PutObjectArgs.builder() .bucket(bucketName) .contentType(file.getContentType()) .stream(inputStream, file.getSize(), -1) .object(fileName) .build(); minioClient.putObject(putObjectArgs); String fileUrl = getFileUrl(minioClient, bucketName, fileName); return fileUrl; } catch (Exception e) { log.error("---在往MinIo的桶{}上传文件{}时发生错误{}", bucketName, file.getName(), e); throw new Exception(e); } } /** * 获取上传文件的访问URL * * @param objectFile * @return */ public static String getFileUrl(MinioClient minioClient, String bucketName, String objectFile) throws Exception { try { return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(objectFile) .build() ); } catch (Exception e) { log.error("---获取上传文件的url发生异常 {}", e); throw new Exception(e); } } /** * 删除指定文件 * * @param bucketName * @param fileName * @return */ public static boolean deleteFile(MinioClient minioClient, String bucketName, String fileName) throws Exception { try { minioClient.removeObject(RemoveObjectArgs.builder() .bucket(bucketName) .object(fileName) .build()); return true; } catch (Exception e) { log.error("---获取上传文件的url发生异常 {}", e); throw new Exception(e); } } /** * 下载文件 * * @param bucketName * @param fileName * @param toPath * @return */ public static boolean downloadFile(MinioClient minioClient, String bucketName, String fileName, String toPath) throws Exception { try { minioClient.downloadObject(DownloadObjectArgs.builder() .bucket(bucketName) .object(fileName) .filename(toPath) .build()); return true; } catch (Exception e) { log.error("---下载桶{}文件的{}发生异常 {}", bucketName, fileName, e); throw new Exception(e); } } /** * 设置浏览器下载响应头 * * @param response * @param fileName */ public static void setResponseHeader(HttpServletRequest request, HttpServletResponse response, String fileName) { try { fileName = new String(fileName.getBytes(), "ISO8859-1"); response.addHeader("Content-Disposition", String.format("attachment; filename=\"%s\"", fileName)); response.setContentType("application/octet-stream"); // 这后面可以设置导出Excel的名称,此例中名为student.xls response.setHeader("Content-Disposition", "attachment;filename=" + fileName); } catch (Exception ex) { log.error(ex.getLocalizedMessage(), ex); } } }
- 8. Controller层代码
/** * @description: 文件相关数据 * @author: dyg * @date: 2022/4/15 8:58 */ @RestController @Slf4j @RequestMapping(value = "/com/dyg/file") @Api(value = "文件相关数据相关接口", tags = {"v1.0"}) public class MinIoFileController { @Autowired private FileInfoService fileInfoService; @PostMapping(value = "/upload") @ApiOperation(value = "上传文件") public Result uploadFile(@RequestBody MultipartFile file, @RequestParam(value = "userType") @ApiParam(value = "userType" ,example = "1") Integer userType, @RequestParam("uid") String uid) { if(null == file){ return Result.errorWithOutData("上传文件不能为空"); } log.info("----uid为 {}的{}用户 上传文件{}",uid,userType,file.getOriginalFilename()); return fileInfoService.uploadFileAndSaveInfoIntoDb(file, userType, uid); } @GetMapping(value = "/view") @ApiOperation(value = "预览文件") public Result viewFile(@RequestParam(value = "id") Integer id) { return fileInfoService.viewFileById(id); } @GetMapping(value = "/downLoad") @ApiOperation(value = "下载文件") public void downLoad(@RequestParam(value = "id") Integer id, HttpServletResponse resp) throws Exception { fileInfoService.downLoadFile(resp, id); } @GetMapping("/delete") @ApiOperation(value = "删除文件") public Result removeFileById(@RequestParam(value = "id") Integer id) { return fileInfoService.removeFileById(id); } }
- 9.Swagger UI 界面
- 10.接口测试
- 上传文件
{ "code": 200, "status": "succ", "msg": null, "data": { "id": 1, "name": "1654766892078-车辆图标.PNG", "minioUrl": "http://127.0.0.1:9000/driver/1654766892078-%E8%BD%A6%E8%BE%86%E5%9B%BE%E6%A0%87.PNG?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=root%2F20220609%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220609T092813Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=797e7cb16fb657f2792da370627662cdf0b141c5943f27defab56913e3a44122", "bucketName": "driver", "fileSize": 210984, "uploadTime": "2022-06-09T09:28:13.812+00:00", "isDeleted": null, "uploadPhone": "11" }, "pageData": null, "count": null, "pageSize": null, "pageIndex": null }
-
- 预览文件
{ "code": 200, "status": "succ", "msg": null, "data": "http://127.0.0.1:9000/driver/1654766892078-%E8%BD%A6%E8%BE%86%E5%9B%BE%E6%A0%87.PNG?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=root%2F20220609%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220609T092813Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=797e7cb16fb657f2792da370627662cdf0b141c5943f27defab56913e3a44122", "pageData": null, "count": null, "pageSize": null, "pageIndex": null }
-
- 下载文件
-
- 删除文件