支持所有云的文件上传

6/1/2022

# 一、背景与问题

文件上传是一个很基本的需求,但是每个公司使用的云计算可能不一样,比如有些公司用的是阿里云、华为云、腾讯云、联通、电信等等;但是无论是哪一种云,其实他们都是遵循的 亚马逊的 S3 文件存储协议。
如果是私有化部署,还得支持 本地存储。

# 二、架构与思想

  • 使用S3协议作为基础进行设计,目的为了满足所有云。
  • 记录所有的上传文件,存到数据库
  • 保留文件的基础信息: 大小,文件名,时间等等
  • 区分 公共文件和私有文件

# 三、具体使用

# 3.1、修改配置

SmartAdmin支持的文件上传模式有local、cloud两种,文件上传接口参考:FileController

  • local:为本地文件上传,文件存储在服务器本地,预览地址为http://localhost:1024/preview/${folder}/${fileKey}
  • cloud: 为云文件存储,目前支持主流的云存储厂商阿里云、华为云、七牛云等

sa-common项目中的配置文件sa-common.yaml,有一段文件上传S3协议的配置,如下:

# 文件上传 配置
file:
  storage:
    mode: cloud                                    # cloud 为云存储;local 为本地存储,则下面的cloud配置将失效
    local:
      path: ${localPath:/home}/smart_admin_v2/upload/
    cloud:
      region: oss-cn-qingdao                       # 自行修改
      endpoint: oss-cn-qingdao.aliyuncs.com        # 自行修改
      bucket-name: common-sit                      # 自行修改  
      access-key:                                  # 自行修改
      secret-key:                                  # 自行修改   
      url:
        expire: 7200000
        public: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.2、定义文件存放位置

如果所有文件都存放到一起,那么后续想做个分类都好方便,所以这里需要定义一下 文件目录 FileFolderTypeEnum.java,建议按照功能业务大块拆分:

public enum FileFolderTypeEnum implements BaseEnum {
    COMMON(1, FileFolderTypeEnum.FOLDER_PUBLIC + "/common/", "通用"),
    NOTICE(2, FileFolderTypeEnum.FOLDER_PUBLIC + "/notice/", "公告"),
    HELP_DOC(3, FileFolderTypeEnum.FOLDER_PUBLIC + "help-doc", "帮助中心"),
    FEEDBACK(4, FileFolderTypeEnum.FOLDER_PUBLIC + "/feedback/", "意见反馈"),
    ;
1
2
3
4
5
6

# 3.3、上传

对应前端组件
在前端提供了file-previewfile-preview-modalfile-upload 三个文件相关的组件可供使用;

序列化与反序列化
@JsonDeserialize(using = FileKeyVoDeserializer.class) 此反序列化,用于将前端传输的fileVO的JSON数组转化为可供数据库直接存储的fileKey字符串。
@JsonSerialize(using = FileKeyVoDeserializer.class) 此序列化,用于将fileKey字符串转化为可供前端直接使用的fileVO的JSON数组。
在业务上逗号分割
在数据存储上我们一般在对应的商品表中创建cover_pic字段,此字段用于存储文件key信息,多张图片的话,我们一般采用逗号分割的方式存储此字段。 字段定义方式:

    @ApiModelProperty("商品封面")
    @Length(max = 250, message = "商品封面最多250字符")
    @JsonSerialize(using = FileKeyVoSerializer.class)
    @JsonDeserialize(using = FileKeyVoDeserializer.class)
    private String coverPic;
1
2
3
4
5

SmartAdmin的前端文件上传组件,返回的JSON数据是以fileVOJSON数组的方式返回的,为了减少前后端数据二次处理的繁琐工作,特意增加了JSON的序列化和反序列化处理。
除了上面的两个序列化类外,我们还提供了FileKeySerializer.class,此类可将fileKey字符串转化为文件请求全路径地址。

比如 反序列化:

@Data
public class NoticeUpdateFormVO extends NoticeVO {

    @ApiModelProperty("附件")
    @JsonSerialize(using = FileKeyVoSerializer.class)
    private String attachment;

    @ApiModelProperty("可见范围")
    private List<NoticeVisibleRangeVO> visibleRangeList;
}
1
2
3
4
5
6
7
8
9
10

比如 序列化:

@Data
public class NoticeDetailVO {

    @ApiModelProperty("id")
    private Long noticeId;

    @ApiModelProperty("标题")
    private String title;

    @ApiModelProperty("附件")
    @JsonSerialize(using = FileKeyVoSerializer.class)
    private String attachment;
1
2
3
4
5
6
7
8
9
10
11
12

# 四、实现原理

# 4.1、表结构

表结构用于记录:文件基本信息:大小、文件名、时间、上传人信息,表设计如下

# 4.2、亚马逊S3协议

亚马逊S3协议有java库:

<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-java-sdk-s3</artifactId>
  <version>1.11.842</version>
</dependency>
1
2
3
4
5

# 4.3、文件实现类

为了满足 本地、云存储 多种方式,系统中定义了 接口IFileStorageService 文件接口;
并提供两种实现类:

FileStorageCloudServiceImpl.java   使用亚马逊S3协议的实现类
FileStorageLocalServiceImpl.java   本地存储实现类
1
2

具体如何判断使用本地存储实现类还是使用 云S3存储实现类,请看 sa-common项目中的 FileCloudConfig,使用了条件注解@ConditionalOnProperty,代码如下:

   @Bean
    @ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = "cloud")
    public IFileStorageService initCloudFileService() {
        return new FileStorageCloudServiceImpl();
    }

    @Bean
    @ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = "local")
    public IFileStorageService initLocalFileService() {
        return new FileStorageLocalServiceImpl();
    }
1
2
3
4
5
6
7
8
9
10
11

# 4.4、 缓存

我们知道,对于某些私有化的文件,当我们访问的时候需要后端请求云计算生成一个可以访问的 url地址,并且这个url地址有个过期时间
但是文件服务又是一个很基础的服务,获取访问地址,后端需要发请求,是阻塞的,如果我们频繁的调用,会很慢,所以我们做了一个redis缓存;

    private String getCacheUrl(String fileKey) {
        String redisKey = redisService.generateRedisKey(RedisKeyConst.Support.FILE_URL, fileKey);
        String fileUrl = redisService.get(redisKey);
        if (null != fileUrl) {
            return fileUrl;
        }
        ResponseDTO<String> responseDTO = fileStorageService.getFileUrl(fileKey);
        if (!responseDTO.getOk()) {
            return null;
        }
        fileUrl = responseDTO.getData();
        redisService.set(redisKey, fileUrl, fileStorageService.cacheExpireSecond());
        return fileUrl;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.5、 文件key生成规则

32位 uuid + 当前年月日时分秒 + 文件格式后缀

比如:c0e6e9340a8c4c4aa8c8062bdc5f8bcc_20221125191500_png 具体实现IFileStorageService.java

     /**
     * 根据文件类型 生成文件名,格式如下:
     * [uuid]_[日期时间]_[文件类型]
     */
    default String generateFileNameByType(String fileType) {
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        String time = LocalDateTimeUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_FORMATTER);
        return uuid + "_" + time + "_" + fileType;
    }

1
2
3
4
5
6
7
8
9
10

# 联系我们

1024创新实验室-主任:卓大 (opens new window),混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室(河南·洛阳) (opens new window) 致力于成为中原领先、国内一流的技术团队,以技术创新为驱动,合作各类项目(软件外包、技术顾问、培训等等)。

加 主任 “卓大” 微信
拉你入群,一起学习
关注 “小镇程序员”
分享代码与生活、技术与赚钱
请 “1024创新实验室” 喝咖啡
支持我们的开源与分享

告白气球 (钢琴版)
JESSE T