自己实现一个文件存储(本地存储,阿里云OSS,FastDFS)的Spring Boot的starter
一、了解SpringBoot starter机制SpringBoot中的starter是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进starter,应用者只需要在maven中引入starter依赖,SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置。starter让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。SpringBoot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。SpringBoot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。
命名规范
官方命名:
- 前缀:spring-boot-starter-xxx
- 比如:spring-boot-starter-web
自定义命名:
- xxx-spring-boot-starter
- 比如:mybatis-spring-boot-starter
参照官方约定的命名规则:file-storage-spring-boot-starter
项目工程目录
pom.xml
4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.11.RELEASE com.feige file-storage-spring-boot-starter 1.0 8 8 3.13.1 1.27.2 1.4 5.7.13 org.springframework.boot spring-boot-configuration-processor true org.springframework.boot spring-boot-starter com.github.tobato fastdfs-client ${fastdfs.version} com.aliyun.oss aliyun-sdk-oss ${aliyun-oss.version} commons-fileupload commons-fileupload ${commons-fileupload.version} cn.hutool hutool-all ${hutool.version}
package com.feige.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOssProperties {
private String bucketName;
private String endpoint;
public String bucketUrl;
private String accessKeyId;
private String accessKeySecret;
public String getBucketName() {
return bucketName;
}
public void setBucketName(String bucketName) {
this.bucketName = bucketName;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getBucketUrl() {
return bucketUrl;
}
public void setBucketUrl(String bucketUrl) {
this.bucketUrl = bucketUrl;
}
public String getAccessKeyId() {
return accessKeyId;
}
public void setAccessKeyId(String accessKeyId) {
this.accessKeyId = accessKeyId;
}
public String getAccessKeySecret() {
return accessKeySecret;
}
public void setAccessKeySecret(String accessKeySecret) {
this.accessKeySecret = accessKeySecret;
}
}
package com.feige.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "feige.storage.local")
public class LocalStorageProperties {
private String path;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
package com.feige.service;
import com.feige.pojo.FSInfo;
import com.feige.pojo.UploadInfo;
import javax.validation.constraints.NotNull;
import java.io.InputStream;
public interface FileStorage {
FSInfo upload(@NotNull UploadInfo uploadInfo);
default InputStream getFileInputStream(@NotNull String objectName) {
return null;
}
}
package com.feige.service.impl;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.OSSObject;
import com.feige.service.FileStorage;
import com.feige.pojo.FSInfo;
import com.feige.pojo.UploadInfo;
import com.feige.properties.AliyunOssProperties;
import com.feige.utils.FSUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
public class AliyunOssFileStorageImpl implements FileStorage {
Logger logger = LoggerFactory.getLogger(AliyunOssFileStorageImpl.class);
private final AliyunOssProperties aliyunOssProperties;
public AliyunOssFileStorageImpl(AliyunOssProperties aliyunOssProperties) {
this.aliyunOssProperties = aliyunOssProperties;
}
@Override
public FSInfo upload(UploadInfo uploadInfo) {
OSS ossClient = null;
try {
ossClient = new OSSClientBuilder()
.build(aliyunOssProperties.getEndpoint(), aliyunOssProperties.getAccessKeyId(), aliyunOssProperties.getAccessKeySecret());
// 上传文件到指定的存储空间(bucketName)并将其保存为指定的文件名称(objectName)。
String group = uploadInfo.isEnableDatePath() ? FSUtils.joinFileName() : "";
ossClient.putObject(aliyunOssProperties.getBucketName(), group + File.separator + uploadInfo.getFileName(), uploadInfo.getInputStream());
FSInfo fsInfo = new FSInfo();
fsInfo.setPath(uploadInfo.getFileName());
fsInfo.setGroup(group);
logger.info("{}文件上传成功",uploadInfo.getFileName());
return fsInfo;
// 关闭OSSClient。
} catch (Exception e) {
logger.error(e.getMessage());
e.printStackTrace();
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return null;
}
@Override
public InputStream getFileInputStream(String objectName) {
OSS ossClient = new OSSClientBuilder().build(aliyunOssProperties.getEndpoint(), aliyunOssProperties.getAccessKeyId(), aliyunOssProperties.getAccessKeySecret());
try {
OSSObject object = ossClient.getObject(aliyunOssProperties.getBucketName(), objectName);
return object.getObjectContent();
} catch (Exception e) {
e.printStackTrace();
}finally {
ossClient.shutdown();
}
throw new RuntimeException("下载文件出错");
}
}
package com.feige.service.impl;
import com.feige.service.FileStorage;
import com.feige.pojo.FSInfo;
import com.feige.pojo.UploadInfo;
import com.github.tobato.fastdfs.domain.fdfs.metaData;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.domain.proto.storage.DownloadByteArray;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.HashSet;
import java.util.Set;
public class FastDfsFileStorageImpl implements FileStorage {
Logger logger = LoggerFactory.getLogger(FastDfsFileStorageImpl.class);
private final FastFileStorageClient fastFileStorageClient;
public FastDfsFileStorageImpl(FastFileStorageClient fastFileStorageClient) {
this.fastFileStorageClient = fastFileStorageClient;
}
@Override
public FSInfo upload(UploadInfo uploadInfo) {
Set metaData = uploadInfo.getmetaDataSet();
if (metaData == null){
metaData = new HashSet<>();
}
String objectName = uploadInfo.getFileName() + "." + uploadInfo.getFileExtName();
metaData.add(new metaData("fileName", objectName));
StorePath storePath = fastFileStorageClient.uploadFile(uploadInfo.getInputStream(), uploadInfo.getFileSize(), uploadInfo.getFileExtName(), metaData);
FSInfo fsInfo = new FSInfo();
fsInfo.setPath(storePath.getPath());
fsInfo.setGroup(storePath.getGroup());
logger.info("{}文件上传成功",uploadInfo.getFileName());
return fsInfo;
}
@Override
public InputStream getFileInputStream(String objectName) {
int indexOf = objectName.lastIndexOf("/");
if (indexOf != -1){
String group = objectName.substring(indexOf + 1);
String path = objectName.substring(0,indexOf + 1);
byte[] bytes = fastFileStorageClient.downloadFile(group, path, new DownloadByteArray());
return new ByteArrayInputStream(bytes);
}
throw new IllegalArgumentException("FastDFS下载文件名必须含group和文件名");
}
}
package com.feige.service.impl;
import com.feige.service.FileStorage;
import com.feige.pojo.FSInfo;
import com.feige.pojo.UploadInfo;
import com.feige.properties.LocalStorageProperties;
import com.feige.utils.FSUtils;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
public class LocalFileStorageImpl implements FileStorage {
Logger logger = LoggerFactory.getLogger(LocalFileStorageImpl.class);
private final LocalStorageProperties localStorageProperties;
public LocalFileStorageImpl(LocalStorageProperties localStorageProperties) {
this.localStorageProperties = localStorageProperties;
}
@Override
public FSInfo upload(UploadInfo uploadInfo) {
String group = uploadInfo.isEnableDatePath() ? File.separator + FSUtils.joinFileName() : "";
try {
FileUtils.copyInputStreamToFile(uploadInfo.getInputStream(),new File(getPath() + group + File.separator + uploadInfo.getFileName()));
} catch (IOException e) {
e.printStackTrace();
}
FSInfo fsInfo = new FSInfo();
fsInfo.setPath(uploadInfo.getFileName());
fsInfo.setGroup(group);
logger.info("{}文件上传成功",uploadInfo.getFileName());
return fsInfo;
}
@Override
public InputStream getFileInputStream(String objectName) {
String path = getPath() + objectName;
try {
return new ByteArrayInputStream(FileUtils.readFileToByteArray(new File(path)));
} catch (IOException e) {
e.printStackTrace();
}
throw new RuntimeException("下载文件出错");
}
private String getPath(){
File file = new File("");
String path = localStorageProperties.getPath();
if (!path.isEmpty()){
if (path.charAt(path.length() - 1) != '/'){
path = path + File.separator;
}
}else {
path = file.getAbsolutePath();
}
return path;
}
}
package com.feige.utils;
import java.io.File;
import java.util.Calendar;
public class FSUtils {
public static String joinFileName(){
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
return year + File.separator + month + File.separator + day;
}
}
package com.feige.pojo;
import com.github.tobato.fastdfs.domain.fdfs.metaData;
import java.io.InputStream;
import java.util.Calendar;
import java.util.Set;
public class UploadInfo {
private InputStream inputStream;
private Long fileSize;
private String fileName;
private boolean enableDatePath;
private Set metaDataSet;
public UploadInfo(InputStream inputStream, Long fileSize, String fileName, boolean enableDatePath, Set metaDataSet) {
this.inputStream = inputStream;
this.fileSize = fileSize;
this.fileName = fileName;
this.enableDatePath = enableDatePath;
this.metaDataSet = metaDataSet;
}
public UploadInfo(InputStream inputStream, Long fileSize, String fileName, boolean enableDatePath) {
this.inputStream = inputStream;
this.fileSize = fileSize;
this.fileName = fileName;
this.enableDatePath = enableDatePath;
}
public UploadInfo() {
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public Long getFileSize() {
return fileSize;
}
public void setFileSize(Long fileSize) {
this.fileSize = fileSize;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFileExtName() {
int lastIndexOf = this.fileName.lastIndexOf('.');
if (lastIndexOf != -1){
return this.fileName.substring(lastIndexOf + 1);
}
return "";
}
public Set getmetaDataSet() {
return metaDataSet;
}
public void setmetaDataSet(Set metaDataSet) {
this.metaDataSet = metaDataSet;
}
public boolean isEnableDatePath() {
return enableDatePath;
}
public void setEnableDatePath(boolean enableDatePath) {
this.enableDatePath = enableDatePath;
}
}
package com.feige.pojo;
public class FSInfo {
private String group;
private String path;
public String getGroup() {
return this.group;
}
public void setGroup(String group) {
this.group = group;
}
public String getPath() {
return this.path;
}
public void setPath(String path) {
this.path = path;
}
public String getFullPath() {
return this.group.concat("/").concat(this.path);
}
}
package com.feige.conf;
import com.feige.service.FileStorage;
import com.feige.service.impl.AliyunOssFileStorageImpl;
import com.feige.service.impl.FastDfsFileStorageImpl;
import com.feige.service.impl.LocalFileStorageImpl;
import com.feige.properties.AliyunOssProperties;
import com.feige.properties.LocalStorageProperties;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class FileStorageAutoConfiguration {
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
prefix = "feige.storage",
name = "type",
havingValue = "oss"
)
@EnableConfigurationProperties(AliyunOssProperties.class)
public static class EnableAliyunOssConfiguration{
private final AliyunOssProperties aliyunOssProperties;
@Autowired
public EnableAliyunOssConfiguration(AliyunOssProperties aliyunOssProperties) {
this.aliyunOssProperties = aliyunOssProperties;
}
@Bean
@ConditionalOnMissingBean({FileStorage.class})
public FileStorage FileStorage(){
return new AliyunOssFileStorageImpl(this.aliyunOssProperties);
}
}
@Configuration(
proxyBeanMethods = false
)
public static class EnableFastDfsConfiguration{
private final FastFileStorageClient fastFileStorageClient;
@Autowired
public EnableFastDfsConfiguration(FastFileStorageClient fastFileStorageClient) {
this.fastFileStorageClient = fastFileStorageClient;
}
@Bean
@ConditionalOnMissingBean({FileStorage.class})
@ConditionalOnProperty(
prefix = "feige.storage",
name = "type",
havingValue = "fastdfs"
)
public FileStorage FileStorage(){
return new FastDfsFileStorageImpl(fastFileStorageClient);
}
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty(
prefix = "feige.storage",
name = "type",
havingValue = "local"
)
@EnableConfigurationProperties(LocalStorageProperties.class)
public static class EnableLocalStoreConfiguration{
private final LocalStorageProperties localStorageProperties;
@Autowired
public EnableLocalStoreConfiguration(LocalStorageProperties localStorageProperties) {
this.localStorageProperties = localStorageProperties;
}
@Bean
@ConditionalOnMissingBean({FileStorage.class})
public FileStorage FileStorage(){
return new LocalFileStorageImpl(localStorageProperties);
}
}
}
resourcesmeta-INFadditional-spring-configuration-metadata.json
{
"properties": [
{
"name": "aliyun.oss.bucket-name",
"type": "java.lang.String",
"description": "阿里云OSS的bucketName.",
"defaultValue": ""
},
{
"name": "aliyun.oss.endpoint",
"type": "java.lang.String",
"description": "阿里云OSS的endpoint.",
"defaultValue": ""
},
{
"name": "aliyun.oss.bucket-url",
"type": "java.lang.String",
"description": "阿里云OSS的bucketUrl.",
"defaultValue": ""
},
{
"name": "aliyun.oss.access-key-id",
"type": "java.lang.String",
"description": "阿里云OSS的accessKeyId.",
"defaultValue": ""
},
{
"name": "aliyun.oss.access-key-secret",
"type": "java.lang.String",
"description": "阿里云OSS的accessKeySecret.",
"defaultValue": ""
},{
"name": "feige.storage.type",
"type": "java.lang.String",
"description": "文件存储类型(oss,fastdfs,local),默认为local." ,
"defaultValue": "local"
},{
"name": "feige.storage.local.path",
"type": "java.lang.String",
"description": "本地文件保存路径,默认为项目根路径." ,
"defaultValue": ""
}
]
}
resourcesmeta-INFspring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.feige.conf.FileStorageAutoConfiguration
打jar包
新建一个SpringBoot项目,导入即可测试
com.feige file-storage-spring-boot-starter 1.0



