在上一篇文章中,Markdown编辑器还没有实现图片上传的功能,要实现图片上传,那么后端服务就需要支持文件上传,文件上传是很多后端服务需要的功能,可以实现一个SpringBootStarter来支持文件的相关功能,比如文件上传、预览、下载、删除等。
实现原理:CreatingYourOwnAuto-configuration
开源组件版本SpringBoot:2.6.3
Minio:8.2.1
安装MinioMinio简介:/
运行minio服务dockerrun-nminio-dev-p7000:9000-p7001:9001minio/minioserver/data--console-address":9001"复制代码
启动完成后,本地访问:http://localhost:7001/
项目依赖因为是嵌入一个文件服务,在平常的SpringBoot项目中可以查看项目的健康状况,那么Minio服务的状态也添加进项目健康状况中,这样就能监控Minio的服务状况了,所以需要添加依赖:spring-boot-starter-actuator,还需要支持文件相关接口,需要添加:spring-boot-starter-web和:minio:8.2.1依赖。
Starter实现自定义SpringBootStarter是SpringBoot一个比较常用的扩展点,可以利用这个扩展点为应用提供默认配置或者自定义功能等。
准备新建一个quiet-minio-spring-boot-starter项目,同时新建一个文件。

classifications:图片分类,一个项目中大部分不止一个地方用到文件上传,为了更好管理文件资源,后端可以限制可以上传的文件分类,以/分割可以在Minio中创建文件夹,实现分文件夹管理文件。
objectPrefix:访问文件时的前缀,不同的服务,它有不同的URL,不同的端口号,这个不是必须的,但是在后端统一配置更方便统一管理,这个可以根据团队的规范自行决定是否使用。
/***@authorahref="mailto:lin-mt@"lin-mt/a*/@Slf4j@Getter@Setter@ConfigurationProperties(prefix="")publicclassMinioConfigurationPropertiesimplementsInitializingBean{privateStringurl="http://localhost:7000";privateStringaccessKey;privateStringsecretKey;privateStringbucketName;privateSetStringclassifications;privateStringobjectPrefix;privateDurationconnectTimeout=(10);privateDurationwriteTimeout=(60);privateDurationreadTimeout=(10);privatebooleancheckBucket=true;privatebooleancreateBucketIfNotExist=true;@OverridepublicvoidafterPropertiesSet(){(accessKey,"accessKeymustnotbeempty.");(secretKey,"secretKeymustnotbeempty.");(bucketName,"bucketNamemustnotbeempty.");(objectPrefix,"objectPrefixmustnotbeempty.");}}复制代码Configuration创建一个配置类,在这个类中,使用我们上一步提供的配置信息,注入一个Bean实例MinioClient,所有文件的相关操作都可以通过这个Bean实现。
在文件中需要添加:=
该步骤是实现项目嵌入Minio服务的关键,具体的原理可以看源码和。
/***@authorahref="mailto:lin-mt@"lin-mt/a*/@Slf4j@Configuration@AllArgsConstructor@ComponentScan("")@EnableConfigurationProperties()publicclassQuietMinioConfiguration{privatefinalMinioConfigurationPropertiesproperties;@BeanpublicMinioClientminioClient()throwsServerException,InsufficientDataException,ErrorResponseException,IOException,NoSuchAlgorithmException,InvalidKeyException,InvalidResponseException,XmlParserException,InternalException{MinioClientminioClient=().point(()).credentials((),()).build();(().toMillis(),().toMillis(),().toMillis());if(()){StringbucketName=();BucketExistsArgsexistsArgs=().bucket(bucketName).build();booleanbucketExists=(existsArgs);if(!bucketExists){if(()){MakeBucketArgsmakeBucketArgs=().bucket(bucketName).build();(makeBucketArgs);}else{thrownewIllegalStateException("Bucketdoesnotexist:"+bucketName);}}}returnminioClient;}}复制代码Controller提供文件相关操作的接口,比如文件上传、下载、删除、预览等。
/***@authorahref="mailto:lin-mt@"lin-mt/a*/@Slf4j@RestController@AllArgsConstructor@RequestMapping("/minio")publicclassMinioController{privatefinalMinioServiceminioService;privatefinalMinioConfigurationPropertiesproperties;privatefinalOptionalMinioHandlerminioHandler;privateStringgetFileName(Stringobject){if((object)){().toString().replace("-","");}if(!("/")||("/")){returnobject;}(("/")+1);}privateFileResponsebuildFileResponse(StatObjectResponsemetadata,Tagstags){=();Stringobject=();StringobjectPrefix=();if(!("/")){objectPrefix=objectPrefix+"/";}objectPrefix=objectPrefix+"minio/";(object).detailPath(objectPrefix+"detail/"+object).viewPath(objectPrefix+"view/"+object).downloadPath(objectPrefix+"download/"+object).deletePath(objectPrefix+"delete/"+object).lastModified(().toLocalDateTime()).fileSize(()).filename(getFileName(())).contentType(()).userMetadata(()).headers(());if(tags!=null){(());}();}@SneakyThrows@PostMapping(consumes=_FORM_DATA_VALUE)@ResponseStatus()publicResponseEntityListFileResponsefileUpload(@RequestParam("classification")Stringclassification,@RequestPart("files")ListMultipartFilefiles){((classification,files));if((files)){(_CONTENT).build();}if(!().contains(classification)){thrownewIllegalArgumentException("classificationisnotconfig.");}ListFileResponseresponses=newArrayList(());for(MultipartFilefile:files){StringfileId=().toString().replace("-","");StringoriginalFilename=();if(originalFilename==null){originalFilename=fileId;}StringBuilderfileName=newStringBuilder(fileId);if((".")){(((".")));}Pathsource=(classification,());MultimapString,StringuserMetadata=(1,1);("original_file_name",originalFilename);(source,(),null,userMetadata);(buildFileResponse((source),(source)));}AtomicReferenceListFileResponsereference=newAtomicReference(responses);(((responses)));().contentType(_JSON).body(());}@GetMapping("/view/**")@ResponseStatus()publicResponseEntityInputStreamResourceviewFile(HttpServletRequestrequest){Stringobject=().toString().split("/view/")[1];((object));PathobjectPath=(object);InputStreaminputStream=(objectPath);StatObjectResponsemetadata=(objectPath);().contentType((())).contentLength(()).header("Content-disposition","attachment;filename="+getFileName(())).body(newInputStreamResource(inputStream));}@GetMapping("/download/**")@ResponseStatus()publicResponseEntityInputStreamResourcedownloadFile(HttpServletRequestrequest){Stringobject=().toString().split("/download/")[1];((object));PathobjectPath=(object);InputStreaminputStream=(objectPath);StatObjectResponsemetadata=(objectPath);AtomicReferenceStatObjectResponseref=newAtomicReference(metadata);(handler-{StatObjectResponseresponse=(metadata);if(response==null){("responsecannotbenull.");}else{(response);}});().contentType(_OCTET_STREAM).contentLength(().size()).header("Content-disposition","attachment;filename="+getFileName(().object())).body(newInputStreamResource(inputStream));}@DeleteMapping("/delete/**")@ResponseStatus(_CONTENT)publicResponseEntityObjectremoveFile(HttpServletRequestrequest){Stringobject=().toString().split("/delete/")[1];((object));PathobjectPath=(object);(objectPath);((object));().build();}@GetMapping("/detail/**")@ResponseStatus()publicResponseEntityFileResponsegetFileDetail(HttpServletRequestrequest){Stringobject=().toString().split("/detail/")[1];((object));PathobjectPath=(object);StatObjectResponsemetadata=(objectPath);FileResponseresponse=buildFileResponse(metadata,(objectPath));AtomicReferenceFileResponsereference=newAtomicReference(response);(((response)));().contentType(_JSON).body(());}}复制代码MinioHnadler因为这是一个嵌入式的文件服务,在进行文件操作的时候,不同的项目可能需要做一些自定义操作,那么我们需要提供一些扩展点,这也是软件设计的原则之一:对扩展开放,对修改关闭。当然,这个扩展点可提供也可不提供,具体实现可以根据自己的团队规范进行设计。
/***@authorahref="mailto:lin-mt@"lin-mt/a*/publicinterfaceMinioHandler{defaultvoidbeforeUpload(Stringclassification,ListMultipartFilefiles){}defaultListFileResponseafterUpload(ListFileResponseresponses){returnresponses;}defaultvoidbeforeView(Stringobject){}defaultvoidbeforeDownloadGetObject(Stringobject){}defaultStatObjectResponsebeforeDownload(StatObjectResponseresponse){returnresponse;}defaultvoidbeforeDelete(Stringobject){}defaultvoidafterDelete(Stringobject){}defaultvoidbeforeGetDetail(Stringobject){}defaultFileResponseafterGetDetail(FileResponseresponse){returnresponse;}}复制代码健康状态检查这个Starter提供了一个文件上传的服务,我们需要提供监控该服务的健康状态的信息,这部分可以自己增加健康状态的详细信息。
/***@authorahref="mailto:lin-mt@"lin-mt/a*/@Component@AllArgsConstructor@ConditionalOnClass()publicclassMinioHealthIndicatorimplementsHealthIndicator{privatefinalMinioClientminioClient;privatefinalMinioConfigurationPropertiesproperties;@OverridepublicHealthhealth(){if(minioClient==null){().build();}StringbucketName=();try{BucketExistsArgsargs=().bucket(()).build();if((args)){().withDetail("bucketName",bucketName).build();}else{().withDetail("bucketName",bucketName).build();}}catch(Exceptione){(e).withDetail("bucketName",bucketName).build();}}}复制代码至此,一个简易的开箱即用的文件服务插件就完成了。
示例创建accessKey项目中引入Starterapiproject(path:":quiet-spring-boot-starters:quiet-minio-spring-boot-starter",configuration:"default")
配置Minioquiet:minio:bucket-name:${}access-key:65mtumFyO3xMpUyPsecret-key:sXBTjKmCtWf8iwOiy8Uw3fCOhe8ibuGVobject-prefix:http://localhost:8080/docclassifications:-api/remark复制代码效果图文件上传服务状态





