0.简介

本教程是基于实战开发的,自定义一个存储相关的starter,主要是集成cos、oss等。教程里面不但详细讲解了starter开发的步骤和细节,同时对springBoot的一些注解做了详细的解释

废话不多说,马上开撸~

1.新建一个项目

新建一个springboot多模块的项目,目录结构如图:




springboot pg数据库备份 springboot怎么操作数据库_spring


1.yfway-object-storage-core:实现业务逻辑的核心代码

2.yfway-object-storage-starter: 提供必要的依赖项来使用starter , 指定Starter的自动装配类

注:如果不知道怎么建多模块,可自行百度,或留言

2.准备工作

2.1统一返回值类

@Data
public class StorageResult implements Serializable {
    private Boolean isSuccess;
    private String message;
    private Object data;
}

简单定义,仅供参考

2.2pom文件依赖

yfway-object-storage-core模块的pom文件

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
	
    <!--COS相关-->
    <dependency>
        <groupId>com.qcloud</groupId>
        <artifactId>cos_api</artifactId>
        <version>5.6.89</version>
    </dependency>
	
    <!--配置相关-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

yfway-object-storage-starter模块的pom文件

<dependencies>
    <dependency>
        <groupId>com.storage</groupId>
        <artifactId>yfway-object-storage-core</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </dependency>
</dependencies>

引入yfway-object-storage-core这个模块

3.新建配置类

相信大家在使用springboot都接触过以下配置:

spring:
  datasource:
    #MySQL配置
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: root

这个是数据库连接信息的配置,这个信息基本都是固定不变的,类似的,当我们使用cos(腾讯存储对象)时,也会要求填写配置信息,例如bucket,secretId,secretKey等,例如下面是我们实现的一个例子:

yf-storage:
  enable: true
  cos:
    bucket-name: 你的bucket
    secret-id: 你的secretId
    secret-key: 你的secretKey
    region: ap-guangzhou

接下来就是看看我们自定义的starter如何读取到这个配置

在yfway-object-storage-core模块中新建一个config目录,新建StorageProperties.java,内容如下:

package com.yfway.storage.core.config;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@Setter
@ConfigurationProperties(prefix = StorageProperties.STOREAGE_PREFIX)
public class StorageProperties {
    static final String STOREAGE_PREFIX = "yf-storage";
    private Boolean enable = true;

    private Cos cos;

    @Data
    public static class Cos{
        private String region;
        private String secretId;
        private String secretKey;
        private String bucketName;
    }
}

这段代码有个非常重要的注解

@ConfigurationProperties 是一个用于绑定配置属性的注解,它可以将外部的配置文件或配置中心中的属性值,绑定到一个 Java 类的属性上。 我们就这可以这样获取cos的配置

StorageProperties.getCos()

4.开发上传接口

这里简单来说就是开发一个接口,大家根据实际情况去写自己的代码,其功能就是实现文件上传。下面的代码仅供参考

4.1新建一个YfCosService接口类

内容如下:

public interface YfCosService {
    /**
     * 上传文件, 根据文件大小自动选择简单上传或者分块上传。
     * @param objectName 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
     * @param file 文件对象
     * @param yfCosConfig 额外配置
     * @param objectMetadata  若需要设置对象的自定义 Headers 可参照下列代码,若不需要可省略下面这几行,对象自定义 Headers 的详细信息可参考 https://cloud.tencent.com/document/product/436/13361
     * @return com.yfway.storage.core.common.StorageResult
     */
    StorageResult uploadFile(String objectName, File file, YfCosConfig yfCosConfig, ObjectMetadata objectMetadata);
}

这里只列出一个上传的接口来讲解,其它接口可自行扩展

4.2新建一个AbstractYfCosService抽象类

内容如下:

public abstract class AbstractYfCosService implements YfCosService{
    private final boolean enabled;

    public AbstractYfCosService(boolean enabled) {
        this.enabled = enabled;
    }

    @Override
    public StorageResult uploadFile(String objectName, File file, YfCosConfig yfCosConfig, ObjectMetadata objectMetadata) {
        StorageResult baseResult = new StorageResult();
        baseResult.setIsSuccess(true);

        if (this.enabled){
            baseResult = this.doUploadFile(objectName,file,yfCosConfig,objectMetadata);
        }else{
            baseResult.setIsSuccess(false);
            baseResult.setMessage("service is disabled");
        }
        return baseResult;
    }

    protected abstract StorageResult doUploadFile(String objectName, File file, YfCosConfig yfCosConfig,ObjectMetadata objectMetadata);
}

该抽象类继承了YfCosService,主要是实现了uploadFile

这里我们定义了一个抽象方法doUploadFile,整个上传的核心代码要在doUploadFile这个方法里面实现

4.3新建一个YfCosStorage类,继承AbstractYfCosService

内容如下:

@Slf4j
public class YfCosStorage extends AbstractYfCosService {
    private final String region;
    private final String secretId;
    private final String secretKey;
    private final String bucketName;

    public YfCosStorage(boolean enabled, String region, String secretId, String secretKey, String bucketName) {
        super(enabled);
        this.region = region;
        this.secretId = secretId;
        this.secretKey = secretKey;
        this.bucketName = bucketName;
    }

    private COSClient createCosClient(){
        try {
            COSCredentials cred = new BasicCOSCredentials(this.secretId, this.secretKey);
            ClientConfig clientConfig = new ClientConfig(new Region(this.region));
            //clientConfig.setHttpProtocol(HttpProtocol.https);
            return new COSClient(cred, clientConfig);
        }catch (Exception e){
            log.info("Create COS client is error:" + e.getMessage());
            return null;
        }
    }

    /**
     *  创建 TransferManager 实例,这个实例用来后续调用高级接口
     */
    private TransferManager createTransferManager(YfCosConfig yfCosConfig) {
        // 创建一个 COSClient 实例,这是访问 COS 服务的基础实例。
        // 详细代码参见本页: 简单操作 -> 创建 COSClient
        COSClient cosClient = createCosClient();

        YfCosConfig.TransferManager config = yfCosConfig.getTransferManager();

        // 自定义线程池大小,建议在客户端与 COS 网络充足(例如使用腾讯云的 CVM,同地域上传 COS)的情况下,设置成16或32即可,可较充分的利用网络资源
        // 对于使用公网传输且网络带宽质量不高的情况,建议减小该值,避免因网速过慢,造成请求超时。
        ExecutorService threadPool = Executors.newFixedThreadPool(config.getThreadPoolSize());

        // 传入一个 threadpool, 若不传入线程池,默认 TransferManager 中会生成一个单线程的线程池。
        TransferManager transferManager = new TransferManager(cosClient, threadPool);

        // 设置高级接口的配置项
        // 分块上传阈值和分块大小
        TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration();
        transferManagerConfiguration.setMultipartUploadThreshold(config.getMultipartUploadThreshold());
        transferManagerConfiguration.setMinimumUploadPartSize(config.getMinimumUploadPartSize());
        transferManager.setConfiguration(transferManagerConfiguration);

        return transferManager;
    }

    @Override
    protected StorageResult doUploadFile(String objectName, File file, YfCosConfig yfCosConfig, ObjectMetadata objectMetadata) {
        StorageResult storageResult = new StorageResult();
        storageResult.setIsSuccess(true);

        if (Objects.isNull(yfCosConfig)){
            //读取默认配置
            yfCosConfig = YfCosConfig.defaultConfig();
        }
        TransferManager transferManager = createTransferManager(yfCosConfig);

        PutObjectRequest putObjectRequest = new PutObjectRequest(this.bucketName, objectName, file);
        // 设置存储类型(如有需要,不需要请忽略此行代码), 默认是标准(Standard), 低频(standard_ia)
        // 更多存储类型请参见 https://cloud.tencent.com/document/product/436/33417
        if (!Objects.isNull(yfCosConfig.getStorageClass())){
            putObjectRequest.setStorageClass(yfCosConfig.getStorageClass());
        }

        //若需要设置对象的自定义 Headers 可参照下列代码,若不需要可省略下面这几行,对象自定义 Headers 的详细信息可参考 https://cloud.tencent.com/document/product/436/13361
        if (!Objects.isNull(objectMetadata)){
            putObjectRequest.withMetadata(objectMetadata);
        }

        try {
            // 高级接口会返回一个异步结果Upload
            // 可同步地调用 waitForUploadResult 方法等待上传完成,成功返回 UploadResult, 失败抛出异常
            Upload upload = transferManager.upload(putObjectRequest);
            UploadResult uploadResult = upload.waitForUploadResult();
            Map<String,Object> result = new HashMap<>();
            result.put("requestId",uploadResult.getRequestId());
            result.put("etag",uploadResult.getETag());
            result.put("crc64Ecma",uploadResult.getCrc64Ecma());
            result.put("key",uploadResult.getKey());
            storageResult.setData(result);

        } catch (CosClientException | InterruptedException e) {
            e.printStackTrace();
            storageResult.setIsSuccess(false);
            storageResult.setMessage(e.getMessage());
        }finally {
            // 确定本进程不再使用 transferManager 实例之后,关闭之
            transferManager.shutdownNow(true);
        }

        return storageResult;
    }
}

核心代码:doUploadFile方法里面实现了上传逻辑,这里的上传代码可以直接抄cos官方文档给出来的代码

5.新建自动装配类

上面我们虽然实现了接口,如何把它变成bean注入到应用程序的bean容器中,就是我们接下来的工作了

新建一个cos的配置类,内容如下:

@Configuration
@ConditionalOnProperty(value = "yf-storage.enable",havingValue = "true",matchIfMissing = true)
@EnableConfigurationProperties(StorageProperties.class)
public class YfCosStorageConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public YfCosService yfCosStorage(StorageProperties properties){
        StorageProperties.Cos cos = properties.getCos();
        return new YfCosStorage(properties.getEnable(),cos.getRegion(),cos.getSecretId(),cos.getSecretKey(),cos.getBucketName());
    }
}

下面来详细解释一下例子中用到的注解

@Configuration 是一个用于定义配置类的注解,它通常与其他注解一起使用以声明应用程序中的 bean。

上面的例子将YfCosStorageConfiguration类声明为一个配置类,其中的方法yfCosStorage()使用了@Bean注解,表示会创建一个bean,并添加到应用程序的bean容器中

@ConditionalOnProperty 是一个条件注解,它可以根据指定的属性值判断是否要创建一个 bean。

如果应用程序的配置文件中存在 yf-storage.enable=true的属性,那么YfCosServicebean 将被创建。如果不存在该属性,且matchIfMissing属性为true,则 YfCosServicebean 也会被创建。如果matchIfMissing属性为false,则 YfCosService bean 不会被创建。

@EnableConfigurationProperties 是一个注解,它可以将使用 @ConfigurationProperties 注解的类注入为 Spring 的 bean,以便在应用程序中使用。例如上面的例子,如果没有使用该注解,你就不能这样获取信息

public YfCosService yfCosStorage(StorageProperties properties){
    properties.getCos();
}

@ConditionalOnMissingBean 是一个条件注解,它可以根据指定的 bean 是否存在来判断是否创建一个新的 bean

接下来我们新建自动装配类YfStorageAutoConfiguration,内容如下:

@Configuration
@EnableConfigurationProperties(StorageProperties.class)
@Import({YfCosStorageConfiguration.class})
public class YfStorageAutoConfiguration {
}

它的作用就算导入上面的cos配置类YfCosStorageConfiguration,之所以这样写主要是为了扩展,如果新增oss的配置类,我们可以这样写

@Configuration
@EnableConfigurationProperties(StorageProperties.class)
@Import({YfCosStorageConfiguration.class,YfOssStorageConfiguration.class})
public class YfStorageAutoConfiguration {
}

6.定义一个注解

内容如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(YfStorageAutoConfiguration.class)
public @interface EnableYfStorage {
}

这个注解的作用就是导入自动装配类,主要是用于yfway-object-storage-starter模块

7.新建spring.factories文件

spring.factories 是 Spring Boot 中的一个特殊的配置文件,它用于自动化配置(auto-configuration)和自动装配(auto-wiring)。

在 Spring Boot 应用程序中,spring.factories 文件通常位于 META-INF/ 目录下,用于定义自动化配置和自动装配的类 。

我们来到yfway-object-storage-starter这个模块,新建一个自动装配类,内容如下:

@Configuration
@EnableYfStorage
public class YfStorageAutoConfiguration {
}

看到@EnableYfStorage这个注解没,我们引入了这个注解,就相当于了yfway-object-storage-core这个模块的自动装配类

最后一步新建spring.factories文件,添加以下内容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.yfway.storage.config.YfStorageAutoConfiguration

自此一个自定义starter就此完成

接下来就是打成jar包,供其它项目使用

下面是一个调用例子:

首先引入我们打包好的依赖

<dependency>
    <groupId>com.storage</groupId>
    <artifactId>yfway-object-storage-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

groupId artifactId version都是怎么来的?

其实就是对应yfway-object-storage-starter的pom文件里面的相关配置

<parent>
    <artifactId>yfway-object-storage</artifactId>
    <groupId>com.storage</groupId>
    <version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>yfway-object-storage-starter</artifactId>

最后就是测试用例了:

@Autowired
YfCosService yfCosService;

@Test
public void upload()throws Exception {
    String objectName = "test/test12.txt";
    String path = "E:\\a.txt";
    yfCosService.uploadFile(objectName,new File(path),null,null);
}

8.优化

上面的调用例子并不是很简洁,首先你要使用@Autowired注解来装配YfCosService,然后调用uploadFile方法,并且方法里面必须传四个参数(即使可以为空),因此我们可以进一步优化这个starter,希望得到以下结果:

@Test
public void upload()throws Exception {
    String objectName = "test/test12.txt";
    String path = "E:\\a.txt";
    YfCosStorageUtil.uploadFile(objectName,path);
}

封装一个YfCosStorageUtil工具类,根据参数的可选性,定义一系列上传方法,

public class YfCosStorageUtil {
    @Autowired
    private YfCosService yfCosService;

    @Getter
    private static YfCosService storage;

    @PostConstruct
    public void init(){
        storage = this.yfCosService;
    }

    public static StorageResult uploadFile(String objectName, String path){
       return storage.uploadFile(objectName, new File(path), null, null);
    }

    public static StorageResult uploadFile(String objectName, String path, YfCosConfig yfCosConfig){
        return storage.uploadFile(objectName, new File(path), yfCosConfig, null);
    }

    public static StorageResult uploadFile(String objectName, File file){
        return storage.uploadFile(objectName, file, null, null);
    }

    public static StorageResult uploadFile(String objectName, File file, YfCosConfig yfCosConfig){
        return storage.uploadFile(objectName, file, yfCosConfig, null);
    }

    public static StorageResult uploadFile(String objectName, File file, YfCosConfig yfCosConfig, ObjectMetadata objectMetadata){
        return storage.uploadFile(objectName, file, yfCosConfig, objectMetadata);
    }
}

简单来说就是使用YfCosStorageUtil对uploadFile接口进一步封装。

这里需要注意的是因为静态方法无法直接使用@Autowired注入进行的bean,因此这里使用@PostConstruct注解对其进行初始化(类似于类的构造函数)

接下来还要改两个地方:

1.打开自动装配类YfCosStorageConfiguration,新加如下代码:

@Bean
@ConditionalOnMissingBean
public YfCosStorageUtil yfStorageUtil(){
    return new YfCosStorageUtil();
}

其实它的作用就是使这段代码生效

@PostConstruct
public void init(){
    storage = this.yfCosService;
}

2.打开YfStorageAutoConfiguration,要加一个@Order注解,代码如下:

@Order(1)
@Configuration
@EnableConfigurationProperties(StorageProperties.class)
@Import({YfCosStorageConfiguration.class})
public class YfStorageAutoConfiguration {
}

@Order 用于指定 Bean 的加载顺序, @Order 中的值越小,优先级越高 ,如果不加上这个注解,YfCosStorageUtil中可能无法直接使用YfCosService