闽公网安备 35020302035485号
SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要用于框架中开发,例如Dubbo、Spring、Common-Logging,JDBC等都是采用SPI机制,针对同一接口采用不同的实现提供给不同的用户,从而提高了框架的扩展性。
其实现原理是ServiceLoader是Java内置的用于查找服务提供接口的工具类,通过调用load()方法实现对服务提供接口的查找,最后遍历来逐个访问服务提供接口的实现类。ServiceLoader类本身实现了Iterable接口并实现了其中的iterator方法,iterator方法的实现中调用了LazyIterator这个内部类中的方法,迭代器创建实例。所有服务提供接口的对应文件都是放置在META-INF/services/目录下,final类型决定了PREFIX目录不可变更。
以mysql为例,实现类是com.mysql.jdbc.Driver,在mysql-connector-java-5.1.48.jar中,我们可以看到有一个META-INF/services目录,目录下有一个文件名为java.sql.Driver的文件,其中的内容是com.mysql.jdbc.Driver
在我们使用JDBC获取连接时,我们通常会调用DriverManager.getConnection()方法获取连接对象,而在Driver类初始化时会使用ServiceLoader动态获取classpath下注册的驱动实现:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
// 堆代码 duidaima.com
private static void loadInitialDrivers() {
.....
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//使用ServiceLoader动态加载具体的驱动实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
.....
}
public final class SpringFactoriesLoader {
/**
* 堆代码 duidaima.com
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
private static final Map < ClassLoader, MultiValueMap < String, String >> cache = new ConcurrentReferenceHashMap < > ();
private SpringFactoriesLoader() {}
/**
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>If a custom instantiation strategy is required, use {@link #loadFactoryNames}
* to obtain all registered factory names.
* @param factoryClass the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading (can be {@code null} to use the default)
* @throws IllegalArgumentException if any factory implementation class cannot
* be loaded or if an error occurs while instantiating any factory
* @see #loadFactoryNames
*/
public static < T > List < T > loadFactories(Class < T > factoryClass, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryClass, "'factoryClass' must not be null");
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
List < String > factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
}
List < T > result = new ArrayList < > (factoryNames.size());
for (String factoryName: factoryNames) {
result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
}
AnnotationAwareOrderComparator.sort(result);
return result;
}
/**
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* @param factoryClass the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
* @throws IllegalArgumentException if an error occurs while loading factory names
* @see #loadFactories
*/
public static List < String > loadFactoryNames(Class <? > factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
private static Map < String, List < String >> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap < String, String > result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration < URL > urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap < > ();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry <? , ?> entry: properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName: StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
@
SuppressWarnings("unchecked")
private static < T > T instantiateFactory(String instanceClassName, Class < T > factoryClass, ClassLoader classLoader) {
try {
Class <? > instanceClass = ClassUtils.forName(instanceClassName, classLoader);
if (!factoryClass.isAssignableFrom(instanceClass)) {
throw new IllegalArgumentException(
"Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
}
return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance();
} catch (Throwable ex) {
throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
}
}
}
loadFactoryNames():解析spring.factories文件中指定接口的实现类的全限定名instantiateFactory():实例化是通过反射来实现对应的初始化
本次案例以Minio,OSS以及Fastdfs上传下载文件为基础,springboot版本2.2.1.RELEASE,目录结构如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.yian</groupId>
<artifactId>file-store-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-boot.version>2.2.1.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.1</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- spring boot 版本控制 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<!--解决maven install 生成jar含有BOOT-INF ,导致被其他工程依赖时找不到类-->
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
定义propertiespackage org.yian.config.properties;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author: duidaima.com
* @create: 2023/3/13
**/
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
/**
* 是一个URL,域名,IPv4或者IPv6地址
*/
private String endpoint;
/**
* TCP/IP端口号
*/
private Integer port;
/**
* accessKey类似于用户ID,用于唯一标识你的账户
*/
private String accessKey;
/**
* secretKey是你账户的密码
*/
private String secretKey;
/**
* 如果是true,则用的是https而不是http,默认值是true
*/
private boolean secure;
/**
* 默认存储桶
*/
private String bucketName;
/**
* 图片的最大大小
*/
private long imageSize;
/**
* 其他文件的最大大小
*/
private long fileSize;
}
OssProperties:package org.yian.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author: duidaima.com
* @create: 2023/3/13
**/
@Data
@Configuration
@ConfigurationProperties(prefix = "oss")
public class OssProperties {
private String accessKey;
private String secret;
private String bucketName;
private String endpoint;
}
工具类package org.yian.utils.minio;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.springframework.web.multipart.MultipartFile;
import org.yian.config.properties.MinioProperties;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: 堆代码
* @create: 2023/3/13
**/
public class MinioUtil {
private final MinioClient minioClient;
private final MinioProperties minioProperties;
public MinioUtil(MinioClient minioClient, MinioProperties minioProperties) {
this.minioClient = minioClient;
this.minioProperties = minioProperties;
}
/**
* 检查存储桶是否存在
*
* @param bucketName 存储桶名称
* @return
*/
@SneakyThrows
public boolean bucketExists(String bucketName) {
boolean found =
minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (found) {
System.out.println(bucketName + " exists");
} else {
System.out.println(bucketName + " does not exist");
}
return found;
}
/**
* 创建存储桶
*
* @param bucketName 存储桶名称
*/
@SneakyThrows
public boolean makeBucket(String bucketName) {
boolean flag = bucketExists(bucketName);
if (!flag) {
minioClient.makeBucket(
MakeBucketArgs.builder()
.bucket(bucketName)
.build());
return true;
} else {
return false;
}
}
/**
* 列出所有存储桶名称
*
* @return
*/
@SneakyThrows
public List<String> listBucketNames() {
List<Bucket> bucketList = listBuckets();
List<String> bucketListName = new ArrayList<>();
for (Bucket bucket : bucketList) {
bucketListName.add(bucket.name());
}
return bucketListName;
}
/**
* 列出所有存储桶
*
* @return
*/
@SneakyThrows
public List<Bucket> listBuckets() {
return minioClient.listBuckets();
}
/**
* 删除存储桶
*
* @param bucketName 存储桶名称
* @return
*/
@SneakyThrows
public boolean removeBucket(String bucketName) {
boolean flag = bucketExists(bucketName);
if (flag) {
Iterable<Result<Item>> myObjects = listObjects(bucketName);
for (Result<Item> result : myObjects) {
Item item = result.get();
// 有对象文件,则删除失败
if (item.size() > 0) {
return false;
}
}
// 删除存储桶,注意,只有存储桶为空时才能删除成功。
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
flag = bucketExists(bucketName);
if (!flag) {
return true;
}
}
return false;
}
/**
* 列出存储桶中的所有对象名称
*
* @param bucketName 存储桶名称
* @return
*/
@SneakyThrows
public List<String> listObjectNames(String bucketName) {
List<String> listObjectNames = new ArrayList<>();
boolean flag = bucketExists(bucketName);
if (flag) {
Iterable<Result<Item>> myObjects = listObjects(bucketName);
for (Result<Item> result : myObjects) {
Item item = result.get();
listObjectNames.add(item.objectName());
}
}else{
listObjectNames.add("存储桶不存在");
}
return listObjectNames;
}
/**
* 列出存储桶中的所有对象
*
* @param bucketName 存储桶名称
* @return
*/
@SneakyThrows
public Iterable<Result<Item>> listObjects(String bucketName) {
boolean flag = bucketExists(bucketName);
if (flag) {
return minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).build());
}
return null;
}
/**
* 文件上传
*
* @param bucketName
* @param multipartFile
*/
@SneakyThrows
public void putObject(String bucketName, MultipartFile multipartFile, String filename, String fileType) {
InputStream inputStream = new ByteArrayInputStream(multipartFile.getBytes());
minioClient.putObject(
PutObjectArgs.builder().bucket(bucketName).object(filename).stream(
inputStream, -1, minioProperties.getFileSize())
.contentType(fileType)
.build());
}
/**
* 文件访问路径
*
* @param bucketName 存储桶名称
* @param objectName 存储桶里的对象名称
* @return
*/
@SneakyThrows
public String getObjectUrl(String bucketName, String objectName) {
boolean flag = bucketExists(bucketName);
String url = "";
if (flag) {
url = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.expiry(2, TimeUnit.MINUTES)
.build());
System.out.println(url);
}
return url;
}
/**
* 删除一个对象
*
* @param bucketName 存储桶名称
* @param objectName 存储桶里的对象名称
*/
@SneakyThrows
public boolean removeObject(String bucketName, String objectName) {
boolean flag = bucketExists(bucketName);
if (flag) {
minioClient.removeObject(
RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
return true;
}
return false;
}
/**
* 以流的形式获取一个文件对象
*
* @param bucketName 存储桶名称
* @param objectName 存储桶里的对象名称
* @return
*/
@SneakyThrows
public InputStream getObject(String bucketName, String objectName) {
boolean flag = bucketExists(bucketName);
if (flag) {
StatObjectResponse statObject = statObject(bucketName, objectName);
if (statObject != null && statObject.size() > 0) {
InputStream stream =
minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
return stream;
}
}
return null;
}
/**
* 获取对象的元数据
*
* @param bucketName 存储桶名称
* @param objectName 存储桶里的对象名称
* @return
*/
@SneakyThrows
public StatObjectResponse statObject(String bucketName, String objectName) {
boolean flag = bucketExists(bucketName);
if (flag) {
StatObjectResponse stat =
minioClient.statObject(
StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
return stat;
}
return null;
}
/**
* 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表
*
* @param bucketName 存储桶名称
* @param objectNames 含有要删除的多个object名称的迭代器对象
* @return
*/
@SneakyThrows
public boolean removeObject(String bucketName, List<String> objectNames) {
boolean flag = bucketExists(bucketName);
if (flag) {
List<DeleteObject> objects = new LinkedList<>();
for (int i = 0; i < objectNames.size(); i++) {
objects.add(new DeleteObject(objectNames.get(i)));
}
Iterable<Result<DeleteError>> results =
minioClient.removeObjects(
RemoveObjectsArgs.builder().bucket(bucketName).objects(objects).build());
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
System.out.println(
"Error in deleting object " + error.objectName() + "; " + error.message());
return false;
}
}
return true;
}
/**
* 以流的形式获取一个文件对象(断点下载)
*
* @param bucketName 存储桶名称
* @param objectName 存储桶里的对象名称
* @param offset 起始字节的位置
* @param length 要读取的长度 (可选,如果无值则代表读到文件结尾)
* @return
*/
@SneakyThrows
public InputStream getObject(String bucketName, String objectName, long offset, Long length) {
boolean flag = bucketExists(bucketName);
if (flag) {
StatObjectResponse statObject = statObject(bucketName, objectName);
if (statObject != null && statObject.size() > 0) {
InputStream stream =
minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.offset(offset)
.length(length)
.build());
return stream;
}
}
return null;
}
/**
* 通过InputStream上传对象
*
* @param bucketName 存储桶名称
* @param objectName 存储桶里的对象名称
* @param inputStream 要上传的流
* @param contentType 要上传的文件类型 MimeTypeUtils.IMAGE_JPEG_VALUE
* @return
*/
@SneakyThrows
public boolean putObject(String bucketName, String objectName, InputStream inputStream, String contentType) {
boolean flag = bucketExists(bucketName);
if (flag) {
minioClient.putObject(
PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(
inputStream, -1, minioProperties.getFileSize())
.contentType(contentType)
.build());
StatObjectResponse statObject = statObject(bucketName, objectName);
if (statObject != null && statObject.size() > 0) {
return true;
}
}
return false;
}
}
FileTypeUtils:package org.yian.utils.minio;
import cn.hutool.core.io.FileTypeUtil;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
/**
* @author: duidaima.com
* @create: 2023/3/13
**/
public class FileTypeUtils {
private final static String IMAGE_TYPE = "image/";
private final static String AUDIO_TYPE = "audio/";
private final static String VIDEO_TYPE = "video/";
private final static String APPLICATION_TYPE = "application/";
private final static String TXT_TYPE = "text/";
public static String getFileType(MultipartFile multipartFile) {
InputStream inputStream = null;
String type = null;
try {
inputStream = multipartFile.getInputStream();
type = FileTypeUtil.getType(inputStream,multipartFile.getOriginalFilename());
if (type.equalsIgnoreCase("JPG") || type.equalsIgnoreCase("JPEG")
|| type.equalsIgnoreCase("GIF") || type.equalsIgnoreCase("PNG")
|| type.equalsIgnoreCase("BMP") || type.equalsIgnoreCase("PCX")
|| type.equalsIgnoreCase("TGA") || type.equalsIgnoreCase("PSD")
|| type.equalsIgnoreCase("TIFF")) {
return IMAGE_TYPE+type;
}
if (type.equalsIgnoreCase("mp3") || type.equalsIgnoreCase("OGG")
|| type.equalsIgnoreCase("WAV") || type.equalsIgnoreCase("REAL")
|| type.equalsIgnoreCase("APE") || type.equalsIgnoreCase("MODULE")
|| type.equalsIgnoreCase("MIDI") || type.equalsIgnoreCase("VQF")
|| type.equalsIgnoreCase("CD")) {
return AUDIO_TYPE+type;
}
if (type.equalsIgnoreCase("mp4") || type.equalsIgnoreCase("avi")
|| type.equalsIgnoreCase("MPEG-1") || type.equalsIgnoreCase("RM")
|| type.equalsIgnoreCase("ASF") || type.equalsIgnoreCase("WMV")
|| type.equalsIgnoreCase("qlv") || type.equalsIgnoreCase("MPEG-2")
|| type.equalsIgnoreCase("MPEG4") || type.equalsIgnoreCase("mov")
|| type.equalsIgnoreCase("3gp")) {
return VIDEO_TYPE+type;
}
if (type.equalsIgnoreCase("doc") || type.equalsIgnoreCase("docx")
|| type.equalsIgnoreCase("ppt") || type.equalsIgnoreCase("pptx")
|| type.equalsIgnoreCase("xls") || type.equalsIgnoreCase("xlsx")
|| type.equalsIgnoreCase("zip")||type.equalsIgnoreCase("jar")
||type.equalsIgnoreCase("java")) {
return APPLICATION_TYPE+type;
}
if (type.equalsIgnoreCase("txt")||type.equalsIgnoreCase("csv")) {
return TXT_TYPE+type;
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
OssUtil:package org.yian.utils.oss;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.net.URL;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author: duidaima.com
* @create: 2023/3/13
**/
public class OssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
public OssUtil(String endpoint,String accessKeyId,String accessKeySecret,String bucketName){
this.endpoint = endpoint;
this.accessKeyId = accessKeyId;
this.accessKeySecret = accessKeySecret;
this.bucketName = bucketName;
}
/**
* 将文件上传到阿里OSS
*
* @param sourceFilePathName 本地文件
* @param aimFilePathName 在阿里OSS中保存的可以包含路径的文件名
* @return 返回上传后文件的访问路径
* @throws FileNotFoundException
*/
public String upload(String sourceFilePathName, String aimFilePathName) throws FileNotFoundException {
FileInputStream is = new FileInputStream(sourceFilePathName);
if (aimFilePathName.startsWith("/")) {
aimFilePathName = aimFilePathName.substring(1);
}
// 如果需要上传时设置存储类型与访问权限,请参考以下示例代码。
ObjectMetadata metadata = new ObjectMetadata();
int indexOfLastDot = aimFilePathName.lastIndexOf(".");
String suffix = aimFilePathName.substring(indexOfLastDot);
metadata.setContentType(getContentType(suffix));
//避免文件覆盖
aimFilePathName = aimFilePathName.substring(0, indexOfLastDot) + System.currentTimeMillis() + suffix;
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, aimFilePathName, is);
//避免访问时将图片下载下来
putObjectRequest.setMetadata(metadata);
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(putObjectRequest);
Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 100);
URL url = ossClient.generatePresignedUrl(bucketName, aimFilePathName, expiration);
// 关闭ossClient
ossClient.shutdown();
return url.toString();
}
/**
* 网络实现上传头像到OSS
*
* @param multipartFile
* @return
*/
public String upload(MultipartFile multipartFile) throws IOException {
// 获取上传的文件的输入流
InputStream inputStream = multipartFile.getInputStream();
// 获取文件名称
String fileName = multipartFile.getOriginalFilename();
// 避免文件覆盖
int i = fileName.lastIndexOf(".");
String suffix = fileName.substring(i);
fileName = fileName.substring(0, i) + System.currentTimeMillis() + suffix;
// 把文件按照日期进行分类
// 获取当前日期
String datePath = DateTimeFormatter.ISO_DATE.format(LocalDate.now());
// 拼接fileName
fileName = datePath + "/" + fileName;
// 如果需要上传时设置存储类型与访问权限
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(getContentType(fileName.substring(fileName.lastIndexOf("."))));
// 上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream);
putObjectRequest.setMetadata(metadata);
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.putObject(putObjectRequest);
//文件访问路径
Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 100);
URL url = ossClient.generatePresignedUrl(bucketName, fileName, expiration);
// 关闭ossClient
ossClient.shutdown();
// 把上传到oss的路径返回
return url.toString();
}
/**
* 返回contentType
*
* @param FileNameExtension
* @return
*/
private String getContentType(String FileNameExtension) {
if (FileNameExtension.equalsIgnoreCase(".bmp")) {
return "image/bmp";
}
if (FileNameExtension.equalsIgnoreCase(".gif")) {
return "image/gif";
}
if (FileNameExtension.equalsIgnoreCase(".jpeg") ||
FileNameExtension.equalsIgnoreCase(".jpg") ||
FileNameExtension.equalsIgnoreCase(".png")
) {
return "image/jpg";
}
return "image/jpg";
}
/**
* 列举 指定路径下所有的文件的文件名
* 如果要列出根路径下的所有文件,path= ""
*
* @param path
* @return
*/
public List<String> listFileName(String path) {
List<String> res = new ArrayList<>();
// 构造ListObjectsRequest请求。
ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName);
// 设置prefix参数来获取fun目录下的所有文件。
listObjectsRequest.setPrefix(path);
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 列出文件。
ObjectListing listing = ossClient.listObjects(listObjectsRequest);
// 遍历所有文件
for (OSSObjectSummary objectSummary : listing.getObjectSummaries()) {
System.out.println(objectSummary.getKey());
}
// 关闭OSSClient。
ossClient.shutdown();
return res;
}
/**
* 列举文件下所有的文件url信息
*/
public List<String> listFileUrl(String path) {
List<String> res = new ArrayList<>();
// 构造ListObjectsRequest请求
ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName);
// 设置prefix参数来获取fun目录下的所有文件。
listObjectsRequest.setPrefix(path);
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 列出文件。
ObjectListing listing = ossClient.listObjects(listObjectsRequest);
// 遍历所有文件。
for (OSSObjectSummary objectSummary : listing.getObjectSummaries()) {
//文件访问路径
Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 100);
URL url = ossClient.generatePresignedUrl(bucketName, objectSummary.getKey(), expiration);
res.add(url.toString());
}
// 关闭OSSClient。
ossClient.shutdown();
return res;
}
/**
* 判断文件是否存在
*
* @param objectName
* @return
*/
public boolean isFileExist(String objectName) {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
boolean res = ossClient.doesObjectExist(bucketName, objectName);
return res;
}
/**
* 通过文件名下载文件
*
* @param objectName 要下载的文件名
* @param localFileName 本地要创建的文件名
*/
public void downloadFile(String objectName, String localFileName) {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 下载OSS文件到本地文件。如果指定的本地文件存在会覆盖,不存在则新建。
ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File(localFileName));
// 关闭OSSClient。
ossClient.shutdown();
}
/**
* 删除文件或目录
*
* @param objectName
*/
public void delelteFile(String objectName) {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ossClient.deleteObject(bucketName, objectName);
ossClient.shutdown();
}
/**
* 批量删除文件或目录
*
* @param keys
*/
public void deleteFiles(List<String> keys) {
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 删除文件。
DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(new DeleteObjectsRequest(bucketName).withKeys(keys));
List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
ossClient.shutdown();
}
/**
* 创建文件夹
*
* @param folder
* @return
*/
public String createFolder(String folder) {
// 文件夹名
final String keySuffixWithSlash = folder;
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 判断文件夹是否存在,不存在则创建
if (!ossClient.doesObjectExist(bucketName, keySuffixWithSlash)) {
// 创建文件夹
ossClient.putObject(bucketName, keySuffixWithSlash, new ByteArrayInputStream(new byte[0]));
// 得到文件夹名
OSSObject object = ossClient.getObject(bucketName, keySuffixWithSlash);
String fileDir = object.getKey();
ossClient.shutdown();
return fileDir;
}
return keySuffixWithSlash;
}
}
FdfsUtil:package org.yian.utils.fdfs;
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.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.charset.Charset;
import java.util.Set;
/**
* @author: duidaima.com
* @create: 2023/3/13
**/
public class FdfsUtil {
private final FastFileStorageClient storageClient;
public FdfsUtil(FastFileStorageClient storageClient){
this.storageClient = storageClient;
}
/**
* MultipartFile类型的文件上传ַ
* @param file
* @return
* @throws IOException
*/
public String uploadFile(MultipartFile file) throws IOException {
StorePath storePath = storageClient.uploadFile(file.getInputStream(), file.getSize(),
FilenameUtils.getExtension(file.getOriginalFilename()), null);
return getResAccessUrl(storePath);
}
/**
* 普通的文件上传
*
* @param file
* @return
* @throws IOException
*/
public String uploadFile(File file) throws IOException {
FileInputStream inputStream = new FileInputStream(file);
StorePath path = storageClient.uploadFile(inputStream, file.length(),
FilenameUtils.getExtension(file.getName()), null);
return getResAccessUrl(path);
}
/**
* 带输入流形式的文件上传
*
* @param is
* @param size
* @param fileName
* @return
*/
public String uploadFileStream(InputStream is, long size, String fileName) {
StorePath path = storageClient.uploadFile(is, size, fileName, null);
return getResAccessUrl(path);
}
/**
* 下载文件,返回字节数组
* @param group fastdfs分组名
* @param path 文件路径
* @return
*/
public byte[] downloadFile(String group, String path) {
byte[] bytes = storageClient.downloadFile(group, path, new DownloadByteArray());
return bytes;
}
/**
* @param fileUrl 文件访问地址,完整url,包含gorup
* @author duidaima.com
* @description 下载文件,将文件保存到指定的路径
*/
public byte[] downloadFileWithFullUrl(String fileUrl) {
StorePath storePath = StorePath.parseFromUrl(fileUrl);
byte[] bytes = storageClient.downloadFile(storePath.getGroup(), storePath.getPath(), new DownloadByteArray());
return bytes;
}
/**
* 将一段文本文件写到fastdfs的服务器上
*
* @param content
* @param fileExtension
* @return
*/
public String uploadFile(String content, String fileExtension) {
byte[] buff = content.getBytes(Charset.forName("UTF-8"));
ByteArrayInputStream stream = new ByteArrayInputStream(buff);
StorePath path = storageClient.uploadFile(stream, buff.length, fileExtension, null);
return getResAccessUrl(path);
}
/**
* 返回文件上传成功后的地址名称ַ
* @param storePath
* @return
*/
private String getResAccessUrl(StorePath storePath) {
String fileUrl = storePath.getFullPath();
return fileUrl;
}
/**
* 删除文件
* @param fileUrl 文件的完整路径
*/
public void deleteFile(String fileUrl) {
if (StringUtils.isEmpty(fileUrl)) {
return;
}
StorePath storePath = StorePath.parseFromUrl(fileUrl);
storageClient.deleteFile(storePath.getGroup(), storePath.getPath());
}
/**
* 上传图片,缩略图, 上传之后会讲图片压缩,变小
* 访问的时候体积很小,下载还是会下载原来上传的大小
* @param is
* @param size
* @param fileExtName
* @param metaData
* @return
*/
public String uploadImage(InputStream is, long size, String fileExtName, Set<MetaData> metaData) {
StorePath path = storageClient.uploadImageAndCrtThumbImage(is, size, fileExtName, metaData);
return getResAccessUrl(path);
}
}
编写自动配置类MinioAutoConfig:
package org.yian.config;
import io.minio.MinioClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.yian.config.properties.MinioProperties;
import org.yian.utils.minio.MinioUtil;
@EnableConfigurationProperties({
MinioProperties.class
})
public class MinioAutoConfig {
@Bean
public MinioUtil minioUtil(MinioProperties minioProperties) {
/**
* 官网给出的 构造方法
* 此类是 客户端进行操作的类
*/
MinioClient minioClient =
MinioClient.builder()
.credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
.endpoint(minioProperties.getEndpoint(),minioProperties.getPort(),minioProperties.isSecure())
.build();
return new MinioUtil(minioClient,minioProperties);
}
}
OssProperties:package org.yian.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.yian.config.properties.OssProperties;
import org.yian.utils.oss.OssUtil;
@EnableConfigurationProperties({
OssProperties.class
})
public class OssAutoConfig {
@Bean
public OssUtil ossUtil(OssProperties ossProperties) {
return new OssUtil(ossProperties.getEndpoint(),ossProperties.getAccessKey(),
ossProperties.getSecret(),ossProperties.getBucketName());
}
}
FdfsAutoConfig:package org.yian.config;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.yian.utils.fdfs.FdfsUtil;
import javax.annotation.Resource;
@EnableConfigurationProperties
public class FdfsAutoConfig {
@Resource
private FastFileStorageClient storageClient;
@Bean
public FdfsUtil fdfsUtil(){
return new FdfsUtil(storageClient);
}
}
编写spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.yian.config.MinioAutoConfig,\ org.yian.config.OssAutoConfig,\ org.yian.config.FdfsAutoConfig打包成jar
方式一:idea内直接install
方式二(需要管理员权限):
mvn install:install-file "-DgroupId=org.yian" "-DartifactId=file-store-start" "-Dversion=1.0-SNAPSHOT" "-Dfile=D:\local\file-store-start\target\file-store-start-1.0-SNAPSHOT.jar" "-Dpackaging=jar"新建一个maven项目
<dependency>
<groupId>org.yian</groupId>
<artifactId>file-store-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
配置类 minio:
# 访问的url
endpoint: http://192.168.5.128
# API的端口
port: 9001
# 秘钥
accessKey: Oe***************SrL
secretKey: F8***************r9KgN2i5
secure: false
bucket-name: test # 桶名 我这是给出了一个默认桶名
image-size: 10485760 # 我在这里设定了 图片文件的最大大小
file-size: 1073741824 # 此处是设定了文件的最大大小
oss:
#yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com
endpoint: https://oss-cn-beijing.aliyuncs.com
#阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
accessKey: LTAI***********PYtEZ
#your secret
secret: D6F******************97eC
#存储空间是您用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间
bucketName: yian
fdfs:
so-timeout: 2500 # 读取时间
connect-timeout: 600 # 连接超时时间
pool:
max-total: 200 # 连接池最大数量
max-total-per-key: 50 # 单个tracker最大连接数
max-wait-millis: 5000 # 连接耗尽最大等待时间 毫秒
tracker-list:
- 192.168.5.128:22122
测试类package org.example.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.yian.utils.fdfs.FdfsUtil;
import org.yian.utils.minio.MinioUtil;
import org.yian.utils.oss.OssUtil;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
@RestController
public class TestController {
@Resource
private MinioUtil minioUtil;
@Resource
private OssUtil ossUtil;
@Resource
private FdfsUtil fdfsUtil;
@GetMapping("minio")
public Boolean minio(){
boolean minio = minioUtil.bucketExists("test");
return minio;
}
@GetMapping("oss")
public Boolean oss(){
boolean oss = ossUtil.isFileExist("2023-03-13/yian.jpg");
return oss;
}
@GetMapping("fdfs")
public String fdfs() throws IOException {
String fdfs = fdfsUtil.uploadFile(new File("test.txt"));
return fdfs;
}
}
如果启动不想引入可以在启动类移除,比如@SpringBootApplication(exclude ={MinioAutoConfig.class}