黑马头条日记 | 微服务项目MinIO与业务代码耦合度过高?耐心看完这篇你就知道如何从零构建MinIO起步依赖!

发布时间:2026/5/20 7:50:54

黑马头条日记 | 微服务项目MinIO与业务代码耦合度过高?耐心看完这篇你就知道如何从零构建MinIO起步依赖! 一、问题分析如果不构建MinIO起步依赖就会导致一旦某个微服务需要用到MinIO都需要重复在这个微服务下重复引入minio依赖、创建配置类和业务类看下方各个微服务下的分配就可以发现这简直就是纯正的屎山结构代码重复率高达100%不仅维护起来困难还特别容易出现不一致的情况浪费开发时间。这还只是仅仅从目录结构来看每个重复类下的代码还不少尤其是MinIOFileStorageService的代码里面每个方法的实现都是一大坨这几个微服务加一块可就真成Java人自嘲的屎山代码了。heima-leadnews-article/ ├── pom.xml (引入 minio 依赖) ├── config/ │ ├── MinIOConfig.java │ └── MinIOConfigProperties.java ├── service/ │ ├── FileStorageService.java │ └── impl/ │ └── MinIOFileStorageService.java └── application.yml (配置 minio 参数) heima-leadnews-comment/ ├── pom.xml (引入 minio 依赖) ├── config/ │ ├── MinIOConfig.java ← 重复 │ └── MinIOConfigProperties.java ← 重复 ├── service/ │ ├── FileStorageService.java ← 重复 │ └── impl/ │ └── MinIOFileStorageService.java ← 重复 └── application.yml (配置 minio 参数) heima-leadnews-user/ ├── pom.xml (引入 minio 依赖) ├── config/ │ ├── MinIOConfig.java ← 重复 │ └── MinIOConfigProperties.java ← 重复 ├── service/ │ ├── FileStorageService.java ← 重复 │ └── impl/ │ └── MinIOFileStorageService.java ← 重复 └── application.yml (配置 minio 参数)但是如果我告诉你只需要构建MinIO的起步依赖就可以将上述屎山代码一扫而光极大降低MinIO和业务代码的耦合度展示专属于Java人的优雅代码呢不多说了直接上成品heima-leadnews-basic/ └── heima-file-starter/ ├── pom.xml ├── config/ │ ├── MinIOConfig.java │ └── MinIOConfigProperties.java ├── service/ │ ├── FileStorageService.java │ └── impl/ │ └── MinIOFileStorageService.java └── resources/ └── META-INF/ └── spring.factories heima-leadnews-article/ ├── pom.xml (只需引入 heima-file-starter) └── application.yml (只需配置 minio 参数) heima-leadnews-comment/ ├── pom.xml (只需引入 heima-file-starter) └── application.yml (只需配置 minio 参数) heima-leadnews-user/ ├── pom.xml (只需引入 heima-file-starter) └── application.yml (只需配置 minio 参数)怎么样还是刚刚的道理别看目录结构就少了一部分之前重复的代码可都全部消失了成功实现代码复用率100%之后如果还有其他的微服务想要调用MinIO功能直接导入我们封装好的MinIO起步依赖在Nacos中配置minIO参数就可以直接使用了那么废话不多说我们立刻开始从零构建MinIO起步依赖的教学吧。二、MinIO起步依赖实现讲解在讲解内部实现之前呢我们先看看起步依赖的实际组成经过分析可以发现大致可以分为接口、实现、配置三层架构以及我们的pom.xml文件做依赖管理。需要用到的依赖如下?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd parent artifactIdheima-leadnews-basic/artifactId groupIdcom.heima/groupId version1.0-SNAPSHOT/version /parent modelVersion4.0.0/modelVersion artifactIdheima-file-starter/artifactId properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-autoconfigure/artifactId /dependency dependency groupIdio.minio/groupId artifactIdminio/artifactId version7.1.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-configuration-processor/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency /dependencies /project关键依赖说明spring-boot-autoconfigure - 提供自动配置能力minio 7.1.0 - MinIO 官方 Java 客户端库spring-boot-configuration-processor - 生成配置元数据IDE 能识别自定义配置属性spring-boot-starter-actuator - 应用监控和管理前置准备做完了接下来我们就逐一来讲解每层架构里面的代码。1.接口层package com.heima.file.service; import java.io.InputStream; /** * author itheima */ public interface FileStorageService { /** * 上传图片文件 * param prefix 文件前缀 * param filename 文件名 * param inputStream 文件流 * return 文件全路径 */ public String uploadImgFile(String prefix, String filename,InputStream inputStream); /** * 上传html文件 * param prefix 文件前缀 * param filename 文件名 * param inputStream 文件流 * return 文件全路径 */ public String uploadHtmlFile(String prefix, String filename,InputStream inputStream); /** * 删除文件 * param pathUrl 文件全路径 */ public void delete(String pathUrl); /** * 下载文件 * param pathUrl 文件全路径 * return * */ public byte[] downLoadFile(String pathUrl); }我们的接口层其实就只有一个FileStorageService文件存储接口里面主要是定义了上传图片或html文件、删除文件、下载文件四个方法。至于方法的具体逻辑留到后面实现类再说这么做其实非常符合我们的开发习惯并且通过接口隐藏方法具体实现逻辑也方便了后续对文件存储方法进行扩展说不准还能支持除了MinIO以外的文件存储方法呢2.配置层1配置属性类package com.heima.file.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import java.io.Serializable; Data ConfigurationProperties(prefix minio) // 文件上传 配置前缀file.oss public class MinIOConfigProperties implements Serializable { private String accessKey; private String secretKey; private String bucket; private String endpoint; private String readPath; }MinIOConfigProperties这个配置类存在的原因就是为了绑定我们MinIO的一些信息。具体实现逻辑就是通过ConfigurationProperties(prefix minio) 注解来锁定该配置类下的属性都需要绑定application.yml文件中 minio.* 的配置。示例如下minio: endpoint: http://192.168.1.100:9000 # 内网地址后端用 readPath: http://minio.example.com # 公网地址前端用 bucket: leadnews # minIO桶的名称 accessKey: minioadmin # minIO访问密钥 secretKey: minioadmin # minIO秘密密钥需要额外说明的是endpoint和readPath这两个属性的区别endpoint本质上是我们后端内网中访问MinIO的地址而readPath是我们后面调用文件上传方法时返回给前端公网访问我们MinIO刚刚上传的文件的地址。在后续讲解实现层的时候会再次提到这两个属性的区别。2自动配置类package com.heima.file.config; import com.heima.file.service.FileStorageService; import io.minio.MinioClient; import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Data Configuration EnableConfigurationProperties({MinIOConfigProperties.class}) //当引入FileStorageService接口时 ConditionalOnClass(FileStorageService.class) public class MinIOConfig { Autowired private MinIOConfigProperties minIOConfigProperties; Bean public MinioClient buildMinioClient() { return MinioClient .builder() .credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey()) .endpoint(minIOConfigProperties.getEndpoint()) .build(); } }我们的自动配置类MinIOConfig的核心作用就是生产我们微服务调用MinIO功能需要的MinioClient客户端Bean对象。所以就是经典的Configuration Bean的组合拳前者用来标记MinIOConfig为Spring配置类Bean则是将方法里return的MinioClient放入容器中注册为Bean对象。第二个注解EnableConfigurationProperties({MinIOConfigProperties.class})则是启用我们刚刚编写的配置属性类MinIOConfigProperties配置属性绑定因此我们在代码块内可以Autowired注入MinIOConfigProperties。第三个注解ConditionalOnClass(FileStorageService.class)则是进行条件装配只有在当前类路径下存在FileStorageService才会加载此配置这样一来不仅可以避免不必要的Bean创建同时提高启动速度和内存效率。如果不加这个注解就意味着MinioClient一直会被创建即使在某个不使用文件存储的微服务中由于在applicatio.yml文件里未配置各种属性导致调用buildMinioClient方法创建MinioClient时就会报空指针异常。关于buildMinioClient方法调用构建MinIO客户端实例使用配置属性初始化连接credentials里面传参就是类似我们MinIO的账号和密码endpoint里面传参我们事先配置好的后端内网访问MinIO地址方便我们后端通过MinioClient与我们的MinIO服务器进行通信。3.实现层我们的实现层就是对接口层方法的实现编写具体的逻辑即我们的MinIOFileStorageService类。package com.heima.file.service.impl; import com.heima.file.config.MinIOConfig; import com.heima.file.config.MinIOConfigProperties; import com.heima.file.service.FileStorageService; import io.minio.GetObjectArgs; import io.minio.MinioClient; import io.minio.PutObjectArgs; import io.minio.RemoveObjectArgs; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Import; import org.springframework.util.StringUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.Date; Slf4j EnableConfigurationProperties(MinIOConfigProperties.class) Import(MinIOConfig.class) public class MinIOFileStorageService implements FileStorageService { Autowired private MinioClient minioClient; Autowired private MinIOConfigProperties minIOConfigProperties; private final static String separator /; /** * param 文件前缀 * param 文件名 file.jpg * return */ public String builderFilePath(String dirPath,String filename) { StringBuilder stringBuilder new StringBuilder(50); if(!StringUtils.isEmpty(dirPath)){ stringBuilder.append(dirPath).append(separator); } SimpleDateFormat sdf new SimpleDateFormat(yyyy/MM/dd); String todayStr sdf.format(new Date()); stringBuilder.append(todayStr).append(separator); stringBuilder.append(filename); return stringBuilder.toString(); } /** * 上传图片文件 * param prefix 文件前缀 * param filename 文件名 * param inputStream 文件流 * return 文件全路径 */ Override public String uploadImgFile(String prefix, String filename,InputStream inputStream) { String filePath builderFilePath(prefix, filename); try { PutObjectArgs putObjectArgs PutObjectArgs.builder() .object(filePath) .contentType(image/jpg) .bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1) .build(); minioClient.putObject(putObjectArgs); StringBuilder urlPath new StringBuilder(minIOConfigProperties.getReadPath()); urlPath.append(separatorminIOConfigProperties.getBucket()); urlPath.append(separator); urlPath.append(filePath); return urlPath.toString(); }catch (Exception ex){ log.error(minio put file error.,ex); throw new RuntimeException(上传文件失败); } } /** * 上传html文件 * param prefix 文件前缀 * param filename 文件名 * param inputStream 文件流 * return 文件全路径 */ Override public String uploadHtmlFile(String prefix, String filename,InputStream inputStream) { String filePath builderFilePath(prefix, filename); try { PutObjectArgs putObjectArgs PutObjectArgs.builder() .object(filePath) .contentType(text/html) .bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1) .build(); minioClient.putObject(putObjectArgs); StringBuilder urlPath new StringBuilder(minIOConfigProperties.getReadPath()); urlPath.append(separatorminIOConfigProperties.getBucket()); urlPath.append(separator); urlPath.append(filePath); return urlPath.toString(); }catch (Exception ex){ log.error(minio put file error.,ex); ex.printStackTrace(); throw new RuntimeException(上传文件失败); } } /** * 删除文件 * param pathUrl 文件全路径 */ Override public void delete(String pathUrl) { String key pathUrl.replace(minIOConfigProperties.getEndpoint()/,); int index key.indexOf(separator); String bucket key.substring(0,index); String filePath key.substring(index1); // 删除Objects RemoveObjectArgs removeObjectArgs RemoveObjectArgs.builder().bucket(bucket).object(filePath).build(); try { minioClient.removeObject(removeObjectArgs); } catch (Exception e) { log.error(minio remove file error. pathUrl:{},pathUrl); e.printStackTrace(); } } /** * 下载文件 * param pathUrl 文件全路径 * return 文件流 * */ Override public byte[] downLoadFile(String pathUrl) { String key pathUrl.replace(minIOConfigProperties.getEndpoint()/,); int index key.indexOf(separator); String bucket key.substring(0,index); String filePath key.substring(index1); InputStream inputStream null; try { inputStream minioClient.getObject(GetObjectArgs.builder().bucket(minIOConfigProperties.getBucket()).object(filePath).build()); } catch (Exception e) { log.error(minio down file error. pathUrl:{},pathUrl); e.printStackTrace(); } ByteArrayOutputStream byteArrayOutputStream new ByteArrayOutputStream(); byte[] buff new byte[100]; int rc 0; while (true) { try { if (!((rc inputStream.read(buff, 0, 100)) 0)) break; } catch (IOException e) { e.printStackTrace(); } byteArrayOutputStream.write(buff, 0, rc); } return byteArrayOutputStream.toByteArray(); } }第一个注解EnableConfigurationProperties(MinIOConfigProperties.class)跟上面自动配置类一样都是为了使用配置属性类MinIOConfigProperties里面的各种属性。第二个注解Import(MinIOConfig.class)则是显示标明并导入依赖MinIOConfig类确保注入MinioClient时一定能成功。第一个方法builderFilePath主要是抽取后两个上传文件方法的共同部分即构建文件前缀年月日分类文件名这么一个待上传MinIO文件的Object。第二个方法uploadImgFile需要传入文件前缀、文件名、文件输入流前两个参数就是用在开头复用第一个方法生成MinIO中的object名字filePath随后构建PutObjectArgs上传参数设置object、文件类型contentType、桶名称、文件输入流参数构建完就可以调用minioClient客户端的putObject方法成功上传文件到MinIO中最后我们需要构建一个能够返回给前端访问MinIO中该文件的地址urlPath注意是给前端通过公网访问的而不是后端通过内网访问所以前面的地址我们应该用配置属性类里面的readPath属性然后拼接桶名称最后拼接我们的object文件路径再返回这个地址即可后续我们可以把这个地址存入数据库后续如果想获取该文件数据库查这个url去MinIO里面访问就可以了性能非常搞。第三个方法uploadHtmlFile跟第二个方法本质上都是上传文件除了设置contenType有所区别外其他都是一样的。第四个方法delete删除文件传参pathUrl注意这个路径参数对应的是后端内网访问MinIO的地址因此我们对其拆分时调用replace方法里面传的是endPoint而不是readPath经过拆分后得到桶名bucket以及文件路径filePath然后拿着这两个参数构建RemoveObjectArgs删除参数即可最后调用minioClient的removeObject方法。第五个方法downLoadFile开始跟第四个方法一样先拆分pathUrl得到桶名bucket以及文件路径filePath接着就是拿着这俩参数调用minioClient的getObject方法得到文件输入流。然后就是经典的IO操作先创建ByteArrayOutputStream输出流用来写再创建一个字节数组buff作批量写加快速度最后把写好的ByteArrayOutputStream调用toByteArray方法转为字节数组返回。4.自动配置注册org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.heima.file.service.impl.MinIOFileStorageServiceSpring Boot 启动时自动扫描 META-INF/spring.factories将 MinIOFileStorageService 注册为自动配置类当项目引入此 starter 依赖时自动装配文件存储功能三、微服务集成MinIO至此我们已经完成了MinIO起步依赖的构建那么具体在其他微服务中如何实现MinIO功能呢请看下述步骤1.在微服务的pom.xml中引入依赖dependencies !-- 引入 MinIO Starter 依赖 -- dependency groupIdcom.heima/groupId artifactIdheima-file-starter/artifactId version1.0-SNAPSHOT/version /dependency !-- 其他依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies2.在application.yml中配置 MinIO 参数server: port: 8003 spring: application: name: leadnews-article # MinIO 配置与 heima-file-starter 中的前缀对应 minio: accessKey: minioadmin secretKey: minioadmin bucket: leadnews endpoint: http://192.168.1.100:9000 # 后端内网地址 readPath: http://minio.example.com # 前端公网地址3.在业务代码中注入使用你可以选择在Controller层或者Service层注入。1Controller层注入package com.heima.article.service.impl; import com.heima.file.service.FileStorageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.InputStream; Service public class ArticleService { Autowired private FileStorageService fileStorageService; // ← 直接注入 /** * 上传文章封面图 */ public String uploadArticleCover(String filename, InputStream inputStream) { // 使用已装配好的 MinIO 功能 String fileUrl fileStorageService.uploadImgFile(article/cover, filename, inputStream); return fileUrl; } /** * 上传文章内容HTML */ public String uploadArticleContent(String filename, InputStream inputStream) { String fileUrl fileStorageService.uploadHtmlFile(article/content, filename, inputStream); return fileUrl; } /** * 删除文章文件 */ public void deleteArticleFile(String fileUrl) { fileStorageService.delete(fileUrl); } }2Service层注入package com.heima.article.controller; import com.heima.file.service.FileStorageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; RestController RequestMapping(/api/article) public class ArticleController { Autowired private FileStorageService fileStorageService; /** * 上传文章图片 */ PostMapping(/upload/image) public String uploadImage(RequestParam(file) MultipartFile file) throws Exception { String filename file.getOriginalFilename(); String fileUrl fileStorageService.uploadImgFile( article/images, filename, file.getInputStream() ); return fileUrl; } /** * 删除文件 */ DeleteMapping(/delete) public void deleteFile(RequestParam(fileUrl) String fileUrl) { fileStorageService.delete(fileUrl); } }四、总结恭喜你你已经成功学会了如何封装MinIO起步依赖并将其集成到微服务中这是一个极具复用价值的实践经验在之后的学习中一定会多次使用到就像 Spring Boot 提供的 spring-boot-starter-web 一样它封装了 Web 开发的常用配置让开发者只需引入依赖就能快速开发 Web 应用。heima-file-starter 也是同样的思想封装了文件存储的常用配置让所有微服务都能快速使用文件存储功能。这就是 Spring Boot Starter 的核心价值

相关新闻