生成OSS工具starter

引言

  • 我们之前编写了一个控制OSS的工具模块,主要实现了以下几个功能:
  1. 将链接配置写在了配置文件中,并编写一个类读取配置文件中的信息,并注册为一个Bean对象
  2. 将OssClient初始化后注册为一个Bean
  3. 将OSS原生的方法进行封装

接下来,我们将尝试将这些功能剥离出来,并编写一个starter,以方便我们之后随时通过Maven引入这个starter,实现对OSS的控制

↓ 编写好的starter放在了下面的Git仓库中,虽然只是一个学习和测试的工具,可能很多地方还不够完善,但是欢迎大家访问

oss-mosfield-springboot-starter: 阿里云OSS对象存储工具starter (gitee.com)

1.1. 结构及功能

  • 与之前直接编写模块代码相比,编写成starter有什么变化?

starter依赖于SpringBoot的自动装配,SpringBoot项目在启动的时候,通过扫描各个依赖包中 META-INF/spring目录下的org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,获取需要被加入Spring容器中的配置类。 所以,我们需要把我们的配置类的全路径名写在这个文件中。

↓ 关于自动装配的原理,可以参考这篇博客

小默的博客 - SpringBoot框架原理 之 自动装配 (xiaomo.link)

目录结构

  link                  
    └─ xiaomo
      └─ oss
        └─ OssAutoConfiguration
        └─ OssHelper
        └─ OssProperties
        └─ MinioUtils
  META-INF
    └─ spring
      └─ org.springframework.boot.autoconfigure.AutoConfiguration.imports

功能

  1. 操作桶相关(桶是否存在,新建桶,删除桶,获取所有桶,获取桶内对象信息)
  2. 对象操作:
    1. 上传到指定的桶的指定位置
    2. 下载指定桶内的指定文件
    3. 删除指定文件
    4. 获取文件的url
    5. 获取文件的二进制数组
  3. 工具
    1. 对图片附件进行压缩,可以按最大尺寸或按质量压缩

1.2. 使用说明

下面的内容为使用此starter的说明!

1.2.1. 依赖

拉取到本starter项目后,需要在maven中编译本项目,并使用maven安装至本地仓库

安装后,使用本starter时需要引入如下依赖

<dependency>
  <groupId>link.xiaomo</groupId>
  <artifactId>oss-mosfield-springboot-starter</artifactId>
  <version>0.1.1</version>
</dependency>

1.2.2. 填写配置文件

  • 引入本starter后,在引入的项目中的配置文件里配置如下信息
  • 此处以application.yml的格式为例
# oss相关配置      
oss:
  # 阿里云oss节点,根据实际情况选择
  endpoint: https://oss-cn-qingdao.aliyuncs.com
  # 桶名字
  bucketName: 实际默认桶名字
  accessKeyId: 实际accessKeyId
  accessKeySecret: 实际accessKeySecret

# 选填:上传文件大小限制
spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB

1.3. starter源码

接下来将展示本starter的相关源码

1.3.0. 引入OSS相关依赖

<!--OSS核心-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
</dependency>
<!--jdk9以上版本需要以下三个-->
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

<!--注解相关-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<!--编解码工具-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.21</version>
</dependency>
<!--图片处理-->
<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.8</version>
</dependency>

<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>
<!-- 添加slf4j日志api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.20</version>
</dependency>
<!-- 添加logback-classic依赖 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<!-- 添加logback-core依赖 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>

<!--springboot单元测试-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

1.3.1. 配置类OssAutoConfiguration

这个类就是我们需要再org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中注册的类,会在Springboot启动的时候扫描到。

这个类主要实现了三个功能:

  1. 通过@EnableConfigurationProperties注解将OssProperties类注册为Bean,这个Bean对象用于从配置文件中读取OSS配置信息
  2. 通过@Bean注解将OSS对象初始化并注册为Bean,它用于调用OSS的原生方法
  3. 通过@Bean注解将我们的业务类OssHelper初始化,注入OSS对象和OssProperties对象,也将其注册为Bean,方便我们随时使用这个业务类。
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(OssProperties.class)
public class OssAutoConfiguration {

    @Autowired
    private OssProperties ossProperties;

    @Bean
    public OSS ossClient() {
        return new OSSClientBuilder().build(ossProperties.getEndpoint(), ossProperties.getAccessKeyId(), ossProperties.getAccessKeySecret());
    }

    @Bean
    public OssHelper ossHelper(@Autowired OSS ossClient) {
        return new OssHelper(ossClient,ossProperties);
    }

}

1.3.2. OssProperties

在这个类中,我们通过@ConfigurationProperties注解,从配置文件中读取 oss下的配置信息

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @Author Mosfield
 */
@ConfigurationProperties(prefix = "oss")
public class OssProperties {
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    public String getEndpoint() {
        return endpoint;
    }

    public void setEndpoint(String endpoint) {
        this.endpoint = endpoint;
    }

    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;
    }

    public String getBucketName() {
        return bucketName;
    }

    public void setBucketName(String bucketName) {
        this.bucketName = bucketName;
    }
}

1.3.3. OssHelper类的方法

此starter的功能,主要是通过OssHelper类中的方法实现,我们在这个类中对OSS的原生方法进行了封装,并对很多方法进行了重载,以方便我们在不同的情况下使用。具体的方法如下:

import com.aliyun.oss.OSS;
import com.aliyun.oss.model.*;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * OSS业务实现类
 *
 * @Author Mosfield
 */
public class OssHelper {

    /**
     * OSS客户端单例
     */
    private final OSS ossClient;

    /**
     * OSS配置信息
     */
    private final OssProperties ossProperties;

    public OssHelper(OSS ossClient, OssProperties ossProperties) {
        this.ossClient = ossClient;
        this.ossProperties = ossProperties;
    }

    /**
     * 查看bucket是否存在
     *
     * @param bucketName
     * @return
     */
    public Boolean bucketExists(String bucketName) {
        return ossClient.doesBucketExist(bucketName);
    }

    /**
     * 创建存储bucket
     *
     * @param bucketName
     */
    public void makeBucket(String bucketName) {
        // 创建CreateBucketRequest对象。
        CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);

        // 如果创建存储空间的同时需要指定存储类型、存储空间的读写权限、数据容灾类型, 请参考如下代码。
        // 此处以设置存储空间的存储类型为标准存储为例介绍。
        //createBucketRequest.setStorageClass(StorageClass.Standard);
        // 数据容灾类型默认为本地冗余存储,即DataRedundancyType.LRS。如果需要设置数据容灾类型为同城冗余存储,请设置为DataRedundancyType.ZRS。
        //createBucketRequest.setDataRedundancyType(DataRedundancyType.ZRS);
        // 设置存储空间读写权限为公共读,默认为私有。
        //createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead);

        // 在支持资源组的地域创建Bucket时,您可以为Bucket配置资源组。
        //createBucketRequest.setResourceGroupId(rsId);

        // 创建存储空间
        ossClient.createBucket(createBucketRequest);
    }

    /**
     * 删除存储bucket
     *
     * @param bucketName
     */
    public void removeBucket(String bucketName) {
        // 删除存储空间。
        ossClient.deleteBucket(bucketName);
    }

    /**
     * 获取全部bucket
     *
     * @return
     */
    public List<Bucket> listBuckets() {
        return ossClient.listBuckets();
    }

    /**
     * 递归查看桶内的所有文件信息
     *
     * @return 存储bucket内文件对象信息
     */
    public List<String> listObjects() {
        return this.listObjects(ossProperties.getBucketName(), "");
    }

    /**
     * 列出某个存储桶中的所有对象。
     *
     * @param bucketName  存储桶名称
     * @param prefix      对象名称的前缀
     * @return
     */
    public List<String> listObjects(String bucketName, String prefix) {
        List<String> res = new ArrayList<>();
        // 构造ListObjectsRequest请求。
        ListObjectsRequest listObjectsRequest = new ListObjectsRequest(bucketName);

        if (prefix == null) {
            prefix = "";
        }

        // 设置prefix参数来获取fun目录下的所有文件。
        listObjectsRequest.setPrefix(prefix);

        // 列出文件。
        ObjectListing listing = ossClient.listObjects(listObjectsRequest);
        // 遍历所有文件
        for (OSSObjectSummary objectSummary : listing.getObjectSummaries()) {
            System.out.println(objectSummary.getKey());
        }
        return res;
    }

    /**
     * 判断文件是否存在
     *
     * @param fileName 文件名称, 如果要带文件夹请用 / 分割, 例如 /help/index.html
     * @return true存在, 反之
     */
    public Boolean checkFileIsExist(String fileName) {
        return this.checkFileIsExist(fileName, ossProperties.getBucketName());
    }

    /**
     * 判断文件是否存在
     *
     * @param fileName 文件名称, 如果要带文件夹请用 / 分割, 例如 /help/index.html
     * @param bucket   桶名称
     * @return true存在, 反之
     */
    public Boolean checkFileIsExist(String fileName, String bucket) {
        if (fileName == null || "".equals(fileName)) {
            return false;
        }
        System.out.println(ossProperties);
        System.out.println(ossClient);
        return ossClient.doesObjectExist(bucket, fileName);
    }

    /**
     * 删除默认桶内的指定文件
     *
     * @param fileName
     * @return
     * @throws Exception
     */
    public void remove(String fileName) {
        this.remove(fileName, ossProperties.getBucketName());
    }

    /**
     * 删除指定桶内的指定文件
     *
     * @param fileName
     * @return
     * @throws Exception
     */
    public void remove(String fileName, String bucket) {
        ossClient.deleteObject(bucket, fileName);
    }

    /**
     * 把文件上传到默认桶,名字为原名字
     *
     * @param file
     * @return
     */
    public String uploadFileObject(MultipartFile file) throws IOException {
        return this.uploadFileObject(file, file.getOriginalFilename(), ossProperties.getBucketName());
    }

    /**
     * 把文件上传到默认桶
     *
     * @param file
     * @param name 路径和新名字
     * @return
     */
    public String uploadFileObject(MultipartFile file, String name) throws IOException {
        return this.uploadFileObject(file, name, ossProperties.getBucketName());
    }

    /**
     * 把文件上传到指定桶的指定路径下,桶名为空则上传到默认桶,路径为空则上传到桶的根目录
     *
     * @param file
     * @param name
     * @param bucket
     * @return
     */
    public String uploadFileObject(MultipartFile file, String name, String bucket) throws IOException {
        if (file.isEmpty()) {
            return null;
        }
        return this.uploadFileObject(file.getInputStream(), name, ossProperties.getBucketName());
    }

    /**
     * 把文件流上传到默认桶
     *
     * @param inputStream
     * @param name 路径和新名字
     * @return
     */
    public String uploadFileObject(InputStream inputStream, String name) {
        return this.uploadFileObject(inputStream, name, ossProperties.getBucketName());
    }

    /**
     * 把文件流上传到指定桶
     *
     * @param inputStream
     * @param name 路径和新名字
     * @return
     */
    public String uploadFileObject(InputStream inputStream, String name, String bucket) {
        // 创建PutObjectRequest对象。
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, name, inputStream);
        // 创建PutObject请求。
        ossClient.putObject(putObjectRequest);
        // 返回访问的url
        return this.getPresignedObjectUrl(name);
    }

    /**
     * 从默认的桶里下载文件
     *
     * @param fileName
     * @return
     */
    public InputStream downloadFileObject(String fileName) {
        return this.downloadFileObject(fileName, ossProperties.getBucketName());
    }

    /**
     * 从指定的桶里下载文件
     *
     * @param fileName
     * @param bucket
     * @return
     */
    public InputStream downloadFileObject(String fileName, String bucket) {
        // 先判断是否存在该文件,不存在返回null
        Boolean result = this.checkFileIsExist(fileName, bucket);
        if (!result) {
            return null;
        }

        return ossClient.getObject(bucket, fileName).getObjectContent();
    }

    /**
     * 获取文件的url
     *
     * @param fileName
     * @return
     */
    public String getPresignedObjectUrl(String fileName) {
        return this.getPresignedObjectUrl(fileName, ossProperties.getBucketName());
    }

    /**
     * 从指定的桶中获取文件的url
     *
     * @param fileName
     * @return
     */
    public String getPresignedObjectUrl(String fileName, String bucket) {
        // 先判断是否存在该文件,不存在返回null
        Boolean result = this.checkFileIsExist(fileName, bucket);
        if (!result) {
            return null;
        }
        // 设置签名URL过期时间,单位为毫秒。
        Date expiration = new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 100);
        // 生成以GET方法访问的签名URL,访客可以直接通过浏览器访问相关内容。
        URL url = ossClient.generatePresignedUrl(bucket, fileName, expiration);
        return url.toString();
    }

    /**
     * 获取公开桶里文件的url
     *
     * @param fileName
     * @return
     */
    public String getPublicUrlByName(String fileName) {
        return this.getPublicUrlByName(fileName, ossProperties.getBucketName());
    }

    /**
     * 获取指定的公开桶里文件的url
     *
     * @param fileName
     * @return
     */
    public String getPublicUrlByName(String fileName, String bucket) {
        String url = "https://" + bucket + "."
                + ossProperties.getEndpoint().split("https://")[1]
                + "/" + fileName; //文件名
        return url;
    }

    /**
     * 根据路径和名字,获取默认桶内对应的二进制数组
     *
     * @param fileName
     * @return
     */
    public byte[] getByteArrByName(String fileName) throws IOException {
        return this.getByteArrByName(fileName, ossProperties.getBucketName());
    }

    /**
     * 根据路径和名字,获取指定桶内对应的二进制数组
     *
     * @param fileName
     * @param bucket
     * @return
     */
    public byte[] getByteArrByName(String fileName, String bucket) throws IOException {
        // 先判断是否存在该文件,不存在返回null
        Boolean result = this.checkFileIsExist(fileName, bucket);
        if (!result) {
            return null;
        }
        InputStream inputStream = this.downloadFileObject(fileName, bucket);

        return IOUtils.toByteArray(inputStream);
    }
}

1.3.4.OssUtils工具类

目前工具类中提供了两个静态的工具方法
分别是 压缩图片获取UUID文件路径名,其中获取文件路径名的方法提供了两种参数形式。

import net.coobird.thumbnailator.Thumbnails;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

public class OssUtils {

    /**
     * 压缩图片
     *
     * @param file      原始文件
     * @param maxWidth  最大宽
     * @param maxHeight 最大高
     * @param quality   质量压缩比,范围是(0,1]
     * @return
     */
    public static MultipartFile zipImg(MultipartFile file, Integer maxWidth, Integer maxHeight, Float quality) throws IOException {

        String fileName = file.getName();
        String originalFilename = file.getOriginalFilename();
        String contentType = file.getContentType();

        InputStream is = null;
        // 通过MultipartFile得到InputStream,从而得到BufferedImage
        BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
        // 获取图片的真实长和宽
        int width = bufferedImage.getWidth();
        int height = bufferedImage.getHeight();

        if (maxWidth == null) {
            maxWidth = width;
        }
        if (maxHeight == null) {
            maxHeight = height;
        }

        // 计算图片的压缩比
        float rate = 1f;
        if (width > maxWidth) {
            rate = 1f * maxWidth / width;
        }
        if (height > maxHeight && rate > 1f * maxHeight / height) {
            rate = 1f * maxHeight / height;
        }

        // 质量压缩比
        if(quality == null){
            quality=1f;
        }

        is = file.getInputStream();
        // 此处换成任意路径即可,临时文件存放位置
        File tempFile = new File("/" + originalFilename);

        Thumbnails.of(is)
                .scale(rate)
                .outputQuality(quality)
                .toFile(tempFile);

        FileInputStream fileInputStream = new FileInputStream(tempFile);
        MockMultipartFile newFile = new MockMultipartFile(fileName, originalFilename, contentType, fileInputStream);

        fileInputStream.close();
        // 删除临时文件
        boolean success = tempFile.delete();

        return newFile;
    }

    /**
     * 通过UUID的规则生成新的文件名,并在前面拼接上路径前缀
     * @param fileName 原文件名
     * @return
     */
    public static String getNewFileNameByUUID(String fileName) {
        return OssUtils.getNewFileNameByUUID(fileName,"");
    }

    /**
     * 通过UUID的规则生成新的文件名,并在前面拼接上路径前缀
     *
     * @param fileName 原文件名
     * @param prefix   路径名可以为空。如果不为空,那不应该以/开头,应该以/结尾
     * @return
     */
    public static String getNewFileNameByUUID(String fileName, String prefix) {
        // 处理前缀为null的情况
        if (prefix == null) {
            prefix = "";
        }
        // 去除空格
        prefix = prefix.trim();
        // 判断前缀是否以斜线开头,如果是,就删除开头的斜线
        if (prefix.startsWith("/")) {
            prefix = prefix.replaceFirst("/", "");
        }
        // 判断如果内容不为空,且不以斜线结尾,则在尾部加上斜线
        if (prefix.length() > 0 && !prefix.endsWith("/")) {
            prefix = prefix + "/";
        }

        return prefix + UUID.randomUUID().toString().replaceAll("-", "")
                + fileName.substring(fileName.lastIndexOf("."));
    }
}
如人饮水,冷暖自知。
最后更新于 2023-08-02