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>定义properties
package 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.factories
org.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}