SpringCloud微服务实战——搭建企业级开发框架(二十九):集成对象存储服务MinIO+七牛云+阿里云+腾讯云

简介: 微服务应用中图片、文件等存储区别于单体应用,单体应用可以放到本地读写磁盘文件,微服务应用必需用到分布式存储,将图片、文件等存储到服务稳定的分布式存储服务器。目前,很多云服务商提供了存储的云服务,比如阿里云OSS、腾讯云COS、七牛云对象存储Kodo、百度云对象存储BOS等等、还有开源对象存储服务器,比如FastDFS、MinIO等。

微服务应用中图片、文件等存储区别于单体应用,单体应用可以放到本地读写磁盘文件,微服务应用必需用到分布式存储,将图片、文件等存储到服务稳定的分布式存储服务器。目前,很多云服务商提供了存储的云服务,比如阿里云OSS、腾讯云COS、七牛云对象存储Kodo、百度云对象存储BOS等等、还有开源对象存储服务器,比如FastDFS、MinIO等。


  如果我们的框架只支持一种存储服务,那么在后期扩展或者修改时会有局限性,所以,这里希望能够定义一个抽象接口,想使用哪种服务就实现哪种服务,在配置多个服务时,调用的存储时可以进行选择。在这里云服务选择七牛云,开源服务选择MinIO进行集成,如果需要其他服务可以自行扩展。


  首先,在框架搭建前,我们先准备环境,这里以MinIO和七牛云为例,MinIO的安装十分简单,我们这里选择Linux安装包的方式来安装,具体方式参考:

http://docs.minio.org.cn/docs/,七牛云只需要到官网注册并实名认证即可获得10G免费存储容量https://www.qiniu.com/


一、基础底层库实现


1、在GitEgg-Platform中新建gitegg-platform-dfs (dfs: Distributed File System分布式文件系统)子工程用于定义对象存储服务的抽象接口,新建IDfsBaseService用于定义文件上传下载常用接口


/**
 * 分布式文件存储操作接口定义
 * 为了保留系统操作记录,原则上不允许上传文件物理删除,修改等操作。
 * 业务操作的修改删除文件,只是关联关系的修改,重新上传文件后并与业务关联即可。
 */
public interface IDfsBaseService {
    /**
     * 获取简单上传凭证
     * @param bucket
     * @return
     */
    String uploadToken(String bucket);
    /**
     * 获取覆盖上传凭证
     * @param bucket
     * @return
     */
    String uploadToken(String bucket, String key);
    /**
     * 创建 bucket
     * @param bucket
     */
    void createBucket(String bucket);
    /**
     * 通过流上传文件,指定文件名
     * @param inputStream
     * @param fileName
     * @return
     */
    GitEggDfsFile uploadFile(InputStream inputStream, String fileName);
    /**
     * 通过流上传文件,指定文件名和bucket
     * @param inputStream
     * @param bucket
     * @param fileName
     * @return
     */
    GitEggDfsFile uploadFile(InputStream inputStream, String bucket, String fileName);
    /**
     * 通过文件名获取文件访问链接
     * @param fileName
     * @return
     */
    String getFileUrl(String fileName);
    /**
     * 通过文件名和bucket获取文件访问链接
     * @param fileName
     * @param bucket
     * @return
     */
    String getFileUrl(String bucket, String fileName);
    /**
     * 通过文件名和bucket获取文件访问链接,设置有效期
     * @param bucket
     * @param fileName
     * @param duration
     * @param unit
     * @return
     */
    String getFileUrl(String bucket, String fileName, int duration, TimeUnit unit);
    /**
     * 通过文件名以流的形式下载一个对象
     * @param fileName
     * @return
     */
    OutputStream getFileObject(String fileName, OutputStream outputStream);
    /**
     * 通过文件名和bucket以流的形式下载一个对象
     * @param fileName
     * @param bucket
     * @return
     */
    OutputStream getFileObject(String bucket, String fileName, OutputStream outputStream);
    /**
     * 根据文件名删除文件
     * @param fileName
     */
    String removeFile(String fileName);
    /**
     * 根据文件名删除指定bucket下的文件
     * @param bucket
     * @param fileName
     */
    String removeFile(String bucket, String fileName);
    /**
     * 根据文件名列表批量删除文件
     * @param fileNames
     */
    String removeFiles(List<String> fileNames);
    /**
     * 根据文件名列表批量删除bucket下的文件
     * @param bucket
     * @param fileNames
     */
    String removeFiles(String bucket, List<String> fileNames);
}


2、在GitEgg-Platform中新建gitegg-platform-dfs-minio子工程,新建MinioDfsServiceImpl和MinioDfsProperties用于实现IDfsBaseService文件上传下载接口


@Data
@Component
@ConfigurationProperties(prefix = "dfs.minio")
public class MinioDfsProperties {
    /**
     * AccessKey
     */
    private String accessKey;
    /**
     * SecretKey
     */
    private String secretKey;
    /**
     * 区域,需要在MinIO配置服务器的物理位置,默认是us-east-1(美国东区1),这也是亚马逊S3的默认区域。
     */
    private String region;
    /**
     * Bucket
     */
    private String bucket;
    /**
     * 公开还是私有
     */
    private Integer accessControl;
    /**
     * 上传服务器域名地址
     */
    private String uploadUrl;
    /**
     * 文件请求地址前缀
     */
    private String accessUrlPrefix;
    /**
     *  上传文件夹前缀
     */
    private String uploadDirPrefix;
}


@Slf4j
@AllArgsConstructor
public class MinioDfsServiceImpl implements IDfsBaseService {
    private final MinioClient minioClient;
    private final MinioDfsProperties minioDfsProperties;
    @Override
    public String uploadToken(String bucket) {
        return null;
    }
    @Override
    public String uploadToken(String bucket, String key) {
        return null;
    }
    @Override
    public void createBucket(String bucket) {
        BucketExistsArgs bea = BucketExistsArgs.builder().bucket(bucket).build();
        try {
            if (!minioClient.bucketExists(bea)) {
                MakeBucketArgs mba = MakeBucketArgs.builder().bucket(bucket).build();
                minioClient.makeBucket(mba);
            }
        } catch (ErrorResponseException e) {
            e.printStackTrace();
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        }
    }
    @Override
    public GitEggDfsFile uploadFile(InputStream inputStream, String fileName) {
        return this.uploadFile(inputStream, minioDfsProperties.getBucket(), fileName);
    }
    @Override
    public GitEggDfsFile uploadFile(InputStream inputStream, String bucket, String fileName) {
        GitEggDfsFile dfsFile = new GitEggDfsFile();
        try {
            dfsFile.setBucket(bucket);
            dfsFile.setBucketDomain(minioDfsProperties.getUploadUrl());
            dfsFile.setFileUrl(minioDfsProperties.getAccessUrlPrefix());
            dfsFile.setEncodedFileName(fileName);
            minioClient.putObject(PutObjectArgs.builder()
                    .bucket(bucket)
                    .stream(inputStream, -1, 5*1024*1024)
                    .object(fileName)
                    .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        }
        return dfsFile;
    }
    @Override
    public String getFileUrl(String fileName) {
        return this.getFileUrl(minioDfsProperties.getBucket(), fileName);
    }
    @Override
    public String getFileUrl(String bucket, String fileName) {
        return this.getFileUrl(bucket, fileName, DfsConstants.DFS_FILE_DURATION, DfsConstants.DFS_FILE_DURATION_UNIT);
    }
    @Override
    public String getFileUrl(String bucket, String fileName, int duration, TimeUnit unit) {
        String url = null;
        try {
            url = minioClient.getPresignedObjectUrl(
                    GetPresignedObjectUrlArgs.builder()
                            .method(Method.GET)
                            .bucket(bucket)
                            .object(fileName)
                            .expiry(duration, unit)
                            .build());
        } catch (ErrorResponseException e) {
            e.printStackTrace();
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        }
        return url;
    }
    @Override
    public OutputStream getFileObject(String fileName, OutputStream outputStream) {
        return this.getFileObject(minioDfsProperties.getBucket(), fileName, outputStream);
    }
    @Override
    public OutputStream getFileObject(String bucket, String fileName, OutputStream outputStream) {
        BufferedInputStream bis = null;
        InputStream stream = null;
        try {
            stream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucket)
                            .object(fileName)
                            .build());
            bis = new BufferedInputStream(stream);
            IOUtils.copy(bis, outputStream);
        } catch (ErrorResponseException e) {
            e.printStackTrace();
        } catch (InsufficientDataException e) {
            e.printStackTrace();
        } catch (InternalException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidResponseException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (XmlParserException e) {
            e.printStackTrace();
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return outputStream;
    }
    @Override
    public String removeFile(String fileName) {
        return this.removeFile(minioDfsProperties.getBucket(), fileName);
    }
    @Override
    public String removeFile(String bucket, String fileName) {
        return this.removeFiles(bucket, Collections.singletonList(fileName));
    }
    @Override
    public String removeFiles(List<String> fileNames) {
        return this.removeFiles(minioDfsProperties.getBucket(), fileNames);
    }
    @Override
    public String removeFiles(String bucket, List<String> fileNames) {
        List<DeleteObject> deleteObject = new ArrayList<>();
        if (!CollectionUtils.isEmpty(fileNames))
        {
            fileNames.stream().forEach(item -> {
                deleteObject.add(new DeleteObject(item));
            });
        }
        Iterable<Result<DeleteError>> result = minioClient.removeObjects(RemoveObjectsArgs.builder()
                .bucket(bucket)
                .objects(deleteObject)
                .build());
        try {
            return JsonUtils.objToJsonIgnoreNull(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}


3、在GitEgg-Platform中新建gitegg-platform-dfs-qiniu子工程,新建QiNiuDfsServiceImpl和QiNiuDfsProperties用于实现IDfsBaseService文件上传下载接口


@Data
@Component
@ConfigurationProperties(prefix = "dfs.qiniu")
public class QiNiuDfsProperties {
    /**
     * AccessKey
     */
    private String accessKey;
    /**
     * SecretKey
     */
    private String secretKey;
    /**
     * 七牛云机房
     */
    private String region;
    /**
     * Bucket 存储块
     */
    private String bucket;
    /**
     * 公开还是私有
     */
    private Integer accessControl;
    /**
     * 上传服务器域名地址
     */
    private String uploadUrl;
    /**
     * 文件请求地址前缀
     */
    private String accessUrlPrefix;
    /**
     * 上传文件夹前缀
     */
    private String uploadDirPrefix;
}


@Slf4j
@AllArgsConstructor
public class QiNiuDfsServiceImpl implements IDfsBaseService {
    private final Auth auth;
    private final UploadManager uploadManager;
    private final BucketManager bucketManager;
    private final QiNiuDfsProperties qiNiuDfsProperties;
    /**
     *
     * @param bucket
     * @return
     */
    @Override
    public String uploadToken(String bucket) {
        Auth auth = Auth.create(qiNiuDfsProperties.getAccessKey(), qiNiuDfsProperties.getSecretKey());
        String upToken = auth.uploadToken(bucket);
        return upToken;
    }
    /**
     *
     * @param bucket
     * @param key
     * @return
     */
    @Override
    public String uploadToken(String bucket, String key) {
        Auth auth = Auth.create(qiNiuDfsProperties.getAccessKey(), qiNiuDfsProperties.getSecretKey());
        String upToken = auth.uploadToken(bucket, key);
        return upToken;
    }
    @Override
    public void createBucket(String bucket) {
        try {
            String[] buckets = bucketManager.buckets();
            if (!ArrayUtil.contains(buckets, bucket)) {
                bucketManager.createBucket(bucket, qiNiuDfsProperties.getRegion());
            }
        } catch (QiniuException e) {
            e.printStackTrace();
        }
    }
    /**
     *
     * @param inputStream
     * @param fileName
     * @return
     */
    @Override
    public GitEggDfsFile uploadFile(InputStream inputStream, String fileName) {
        return this.uploadFile(inputStream, qiNiuDfsProperties.getBucket(), fileName);
    }
    /**
     *
     * @param inputStream
     * @param bucket
     * @param fileName
     * @return
     */
    @Override
    public GitEggDfsFile uploadFile(InputStream inputStream, String bucket, String fileName) {
        GitEggDfsFile dfsFile = null;
        //默认不指定key的情况下,以文件内容的hash值作为文件名
        String key = null;
        if (!StringUtils.isEmpty(fileName))
        {
            key = fileName;
        }
        try {
            String upToken = auth.uploadToken(bucket);
            Response response = uploadManager.put(inputStream, key, upToken,null, null);
            //解析上传成功的结果
            dfsFile = JsonUtils.jsonToPojo(response.bodyString(), GitEggDfsFile.class);
            if (dfsFile != null) {
                dfsFile.setBucket(bucket);
                dfsFile.setBucketDomain(qiNiuDfsProperties.getUploadUrl());
                dfsFile.setFileUrl(qiNiuDfsProperties.getAccessUrlPrefix());
                dfsFile.setEncodedFileName(fileName);
            }
        } catch (QiniuException ex) {
            Response r = ex.response;
            log.error(r.toString());
            try {
                log.error(r.bodyString());
            } catch (QiniuException ex2) {
                log.error(ex2.toString());
            }
        } catch (Exception e) {
            log.error(e.toString());
        }
        return dfsFile;
    }
    @Override
    public String getFileUrl(String fileName) {
        return this.getFileUrl(qiNiuDfsProperties.getBucket(), fileName);
    }
    @Override
    public String getFileUrl(String bucket, String fileName) {
        return this.getFileUrl(bucket, fileName, DfsConstants.DFS_FILE_DURATION, DfsConstants.DFS_FILE_DURATION_UNIT);
    }
    @Override
    public String getFileUrl(String bucket, String fileName, int duration, TimeUnit unit) {
        String finalUrl = null;
        try {
            Integer accessControl = qiNiuDfsProperties.getAccessControl();
            if (accessControl != null && DfsConstants.DFS_FILE_PRIVATE == accessControl.intValue()) {
                String encodedFileName = URLEncoder.encode(fileName, "utf-8").replace("+", "%20");
                String publicUrl = String.format("%s/%s", qiNiuDfsProperties.getAccessUrlPrefix(), encodedFileName);
                String accessKey = qiNiuDfsProperties.getAccessKey();
                String secretKey = qiNiuDfsProperties.getSecretKey();
                Auth auth = Auth.create(accessKey, secretKey);
                long expireInSeconds = unit.toSeconds(duration);
                finalUrl = auth.privateDownloadUrl(publicUrl, expireInSeconds);
            }
            else {
                finalUrl = String.format("%s/%s", qiNiuDfsProperties.getAccessUrlPrefix(), fileName);
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return finalUrl;
    }
    @Override
    public OutputStream getFileObject(String fileName, OutputStream outputStream) {
        return this.getFileObject(qiNiuDfsProperties.getBucket(), fileName, outputStream);
    }
    @Override
    public OutputStream getFileObject(String bucket, String fileName, OutputStream outputStream) {
        URL url = null;
        HttpURLConnection conn = null;
        BufferedInputStream bis = null;
        try {
            String path =  this.getFileUrl(bucket, fileName, DfsConstants.DFS_FILE_DURATION, DfsConstants.DFS_FILE_DURATION_UNIT);
            url = new URL(path);
            conn = (HttpURLConnection)url.openConnection();
            //设置超时间
            conn.setConnectTimeout(DfsConstants.DOWNLOAD_TIMEOUT);
            //防止屏蔽程序抓取而返回403错误
            conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
            conn.connect();
            //得到输入流
            bis = new BufferedInputStream(conn.getInputStream());
            IOUtils.copy(bis, outputStream);
        } catch (Exception e) {
            log.error("读取网络文件异常:" + fileName);
        }
        finally {
            conn.disconnect();
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return outputStream;
    }
    /**
     *
     * @param fileName
     * @return
     */
    @Override
    public String removeFile(String fileName) {
        return this.removeFile( qiNiuDfsProperties.getBucket(), fileName);
    }
    /**
     *
     * @param bucket
     * @param fileName
     * @return
     */
    @Override
    public String removeFile(String bucket, String fileName) {
        String resultStr = null;
        try {
            Response response = bucketManager.delete(bucket, fileName);
            resultStr = JsonUtils.objToJson(response);
        } catch (QiniuException e) {
            Response r = e.response;
            log.error(r.toString());
            try {
                log.error(r.bodyString());
            } catch (QiniuException ex2) {
                log.error(ex2.toString());
            }
        } catch (Exception e) {
            log.error(e.toString());
        }
        return resultStr;
    }
    /**
     *
     * @param fileNames
     * @return
     */
    @Override
    public String removeFiles(List<String> fileNames) {
        return this.removeFiles(qiNiuDfsProperties.getBucket(), fileNames);
    }
    /**
     *
     * @param bucket
     * @param fileNames
     * @return
     */
    @Override
    public String removeFiles(String bucket, List<String> fileNames) {
        String resultStr = null;
        try {
            if (!CollectionUtils.isEmpty(fileNames) && fileNames.size() > GitEggConstant.Number.THOUSAND)
            {
                throw new BusinessException("单次批量请求的文件数量不得超过1000");
            }
            BucketManager.BatchOperations batchOperations = new BucketManager.BatchOperations();
            batchOperations.addDeleteOp(bucket, (String [])fileNames.toArray());
            Response response = bucketManager.batch(batchOperations);
            BatchStatus[] batchStatusList = response.jsonToObject(BatchStatus[].class);
            resultStr = JsonUtils.objToJson(batchStatusList);
        } catch (QiniuException ex) {
            log.error(ex.response.toString());
        } catch (Exception e) {
            log.error(e.toString());
        }
        return resultStr;
    }
}


4、在GitEgg-Platform中新建gitegg-platform-dfs-starter子工程,用于集成所有文件上传下载子工程,方便业务统一引入所有实现


<?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">
    <parent>
        <artifactId>GitEgg-Platform</artifactId>
        <groupId>com.gitegg.platform</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>gitegg-platform-dfs-starter</artifactId>
    <name>${project.artifactId}</name>
    <packaging>jar</packaging>
    <dependencies>
        <!-- gitegg 分布式文件自定义扩展-minio -->
        <dependency>
            <groupId>com.gitegg.platform</groupId>
            <artifactId>gitegg-platform-dfs-minio</artifactId>
        </dependency>
        <!-- gitegg 分布式文件自定义扩展-七牛云 -->
        <dependency>
            <groupId>com.gitegg.platform</groupId>
            <artifactId>gitegg-platform-dfs-qiniu</artifactId>
        </dependency>
    </dependencies>
</project>


5、gitegg-platform-bom中添加文件存储相关依赖


<!-- gitegg 分布式文件自定义扩展 -->
            <dependency>
                <groupId>com.gitegg.platform</groupId>
                <artifactId>gitegg-platform-dfs</artifactId>
                <version>${gitegg.project.version}</version>
            </dependency>
            <!-- gitegg 分布式文件自定义扩展-minio -->
            <dependency>
                <groupId>com.gitegg.platform</groupId>
                <artifactId>gitegg-platform-dfs-minio</artifactId>
                <version>${gitegg.project.version}</version>
            </dependency>
            <!-- gitegg 分布式文件自定义扩展-七牛云 -->
            <dependency>
                <groupId>com.gitegg.platform</groupId>
                <artifactId>gitegg-platform-dfs-qiniu</artifactId>
                <version>${gitegg.project.version}</version>
            </dependency>
            <!-- gitegg 分布式文件自定义扩展-starter -->
            <dependency>
                <groupId>com.gitegg.platform</groupId>
                <artifactId>gitegg-platform-dfs-starter</artifactId>
                <version>${gitegg.project.version}</version>
            </dependency>
           <!-- minio文件存储服务 https://mvnrepository.com/artifact/io.minio/minio -->
            <dependency>
                <groupId>io.minio</groupId>
                <artifactId>minio</artifactId>
                <version>${dfs.minio.version}</version>
            </dependency>
            <!--七牛云文件存储服务-->
            <dependency>
                <groupId>com.qiniu</groupId>
                <artifactId>qiniu-java-sdk</artifactId>
                <version>${dfs.qiniu.version}</version>
            </dependency>


二、业务功能实现


分布式文件存储功能作为系统扩展功能放在gitegg-service-extension工程中,首先需要分为几个模块:


  • 文件服务器的基本配置模块


  • 文件的上传、下载记录模块(下载只记录私有文件,对于公共可访问的文件不需要记录)


  • 前端访问下载实现


1、新建文件服务器配置表,用于存放文件服务器相关配置,定义好表结构,使用代码生成工具生成增删改查代码。


CREATE TABLE `t_sys_dfs`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id',
  `dfs_type` bigint(20) NULL DEFAULT NULL COMMENT '分布式存储分类',
  `dfs_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分布式存储编号',
  `access_url_prefix` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件访问地址前缀',
  `upload_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分布式存储上传地址',
  `bucket` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '空间名称',
  `app_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '应用ID',
  `region` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区域',
  `access_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'accessKey',
  `secret_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'secretKey',
  `dfs_default` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否默认存储 0否,1是',
  `dfs_status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '状态 0禁用,1 启用',
  `access_control` tinyint(2) NOT NULL DEFAULT 0 COMMENT '访问控制 0私有,1公开',
  `comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  `operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '1:删除 0:不删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分布式存储配置表' ROW_FORMAT = DYNAMIC;


2、新建DfsQiniuFactory和DfsMinioFactory接口实现工厂类,用于根据当前用户的选择,实例化需要的接口实现类


/**
 * 七牛云上传服务接口工厂类
 */
public class DfsQiniuFactory {
    public static IDfsBaseService getDfsBaseService(DfsDTO dfsDTO) {
        Auth auth = Auth.create(dfsDTO.getAccessKey(), dfsDTO.getSecretKey());
        Configuration cfg = new Configuration(Region.autoRegion());
        UploadManager uploadManager = new UploadManager(cfg);
        BucketManager bucketManager = new BucketManager(auth, cfg);
        QiNiuDfsProperties qiNiuDfsProperties = new QiNiuDfsProperties();
        qiNiuDfsProperties.setAccessKey(dfsDTO.getAccessKey());
        qiNiuDfsProperties.setSecretKey(dfsDTO.getSecretKey());
        qiNiuDfsProperties.setRegion(dfsDTO.getRegion());
        qiNiuDfsProperties.setBucket(dfsDTO.getBucket());
        qiNiuDfsProperties.setUploadUrl(dfsDTO.getUploadUrl());
        qiNiuDfsProperties.setAccessUrlPrefix(dfsDTO.getAccessUrlPrefix());
        qiNiuDfsProperties.setAccessControl(dfsDTO.getAccessControl());
        return new QiNiuDfsServiceImpl(auth, uploadManager, bucketManager, qiNiuDfsProperties);
    }
}


/**
 * MINIO上传服务接口工厂类
 */
public class DfsMinioFactory {
    public static IDfsBaseService getDfsBaseService(DfsDTO dfsDTO) {
        MinioClient minioClient =
                MinioClient.builder()
                        .endpoint(dfsDTO.getUploadUrl())
                        .credentials(dfsDTO.getAccessKey(), dfsDTO.getSecretKey()).build();;
        MinioDfsProperties minioDfsProperties = new MinioDfsProperties();
        minioDfsProperties.setAccessKey(dfsDTO.getAccessKey());
        minioDfsProperties.setSecretKey(dfsDTO.getSecretKey());
        minioDfsProperties.setRegion(dfsDTO.getRegion());
        minioDfsProperties.setBucket(dfsDTO.getBucket());
        minioDfsProperties.setUploadUrl(dfsDTO.getUploadUrl());
        minioDfsProperties.setAccessUrlPrefix(dfsDTO.getAccessUrlPrefix());
        minioDfsProperties.setAccessControl(dfsDTO.getAccessControl());
        return new MinioDfsServiceImpl(minioClient, minioDfsProperties);
    }
}


3、新建DfsFactory工厂类,添加@Component使用容器管理该类(默认单例),用于根据系统用户配置,生成及缓存对应的上传下载接口实现


/**
 * DfsFactory工厂类,根据系统用户配置,生成及缓存对应的上传下载接口实现
 */
@Component
public class DfsFactory {
    /**
     * DfsService 缓存
     */
    private final static Map<Long, IDfsBaseService> dfsBaseServiceMap = new ConcurrentHashMap<>();
    /**
     * 获取 DfsService
     *
     * @param dfsDTO 分布式存储配置
     * @return dfsService
     */
    public IDfsBaseService getDfsBaseService(DfsDTO dfsDTO) {
        //根据dfsId获取对应的分布式存储服务接口,dfsId是唯一的,每个租户有其自有的dfsId
        Long dfsId = dfsDTO.getId();
        IDfsBaseService dfsBaseService = dfsBaseServiceMap.get(dfsId);
        if (null == dfsBaseService) {
            Class cls = null;
            try {
                cls = Class.forName(DfsFactoryClassEnum.getValue(String.valueOf(dfsDTO.getDfsType())));
                Method staticMethod = cls.getDeclaredMethod(DfsConstants.DFS_SERVICE_FUNCTION, DfsDTO.class);
                dfsBaseService = (IDfsBaseService) staticMethod.invoke(cls, dfsDTO);
                dfsBaseServiceMap.put(dfsId, dfsBaseService);
            } catch (ClassNotFoundException | NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        return dfsBaseService;
    }
}


4、新建枚举类DfsFactoryClassEnum,用于DfsFactory 工厂类通过反射实例化对应文件服务器的接口实现类


/**
 * @ClassName: DfsFactoryClassEnum
 * @Description: 分布式存储工厂类枚举 ,因dfs表存的是数据字典表的id,这里省一次数据库查询,所以就用数据字典的id
 * @author GitEgg
 * @date 2020年09月19日 下午11:49:45
 */
public enum DfsFactoryClassEnum {
    /**
     * MINIO MINIO
     */
    MINIO("2", "com.gitegg.service.extension.dfs.factory.DfsMinioFactory"),
    /**
     * 七牛云Kodo QINIUYUN_KODO
     */
    QI_NIU("3", "com.gitegg.service.extension.dfs.factory.DfsQiniuFactory"),
    /**
     * 阿里云OSS ALIYUN_OSS
     */
    ALI_YUN("4", "com.gitegg.service.extension.dfs.factory.DfsAliyunFactory"),
    /**
     * 腾讯云COS TENCENT_COS
     */
    TENCENT("5", "com.gitegg.service.extension.dfs.factory.DfsTencentFactory");
    public String code;
    public String value;
    DfsFactoryClassEnum(String code, String value) {
        this.code = code;
        this.value = value;
    }
    public static String getValue(String code) {
        DfsFactoryClassEnum[] smsFactoryClassEnums = values();
        for (DfsFactoryClassEnum smsFactoryClassEnum : smsFactoryClassEnums) {
            if (smsFactoryClassEnum.getCode().equals(code)) {
                return smsFactoryClassEnum.getValue();
            }
        }
        return null;
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
}


5、新建IGitEggDfsService接口,用于定义业务需要的文件上传下载接口


/**
 * 业务文件上传下载接口实现
 *
 */
public interface IGitEggDfsService {
    /**
     * 获取文件上传的 token
     * @param dfsCode
     * @return
     */
    String uploadToken(String dfsCode);
    /**
     * 上传文件
     *
     * @param dfsCode
     * @param file
     * @return
     */
    GitEggDfsFile uploadFile(String dfsCode, MultipartFile file);
    /**
     * 获取文件访问链接
     * @param dfsCode
     * @param fileName
     * @return
     */
    String getFileUrl(String dfsCode, String fileName);
    /**
     * 下载文件
     * @param dfsCode
     * @param fileName
     * @return
     */
    OutputStream downloadFile(String dfsCode, String fileName, OutputStream outputStream);
}


6、新建IGitEggDfsService接口实现类GitEggDfsServiceImpl,用于实现业务需要的文件上传下载接口


@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GitEggDfsServiceImpl implements IGitEggDfsService {
    private final DfsFactory dfsFactory;
    private final IDfsService dfsService;
    private final IDfsFileService dfsFileService;
    @Override
    public String uploadToken(String dfsCode) {
        QueryDfsDTO queryDfsDTO = new QueryDfsDTO();
        queryDfsDTO.setDfsCode(dfsCode);
        DfsDTO dfsDTO = dfsService.queryDfs(queryDfsDTO);
        IDfsBaseService dfsBaseService = dfsFactory.getDfsBaseService(dfsDTO);
        String token = dfsBaseService.uploadToken(dfsDTO.getBucket());
        return token;
    }
    @Override
    public GitEggDfsFile uploadFile(String dfsCode, MultipartFile file) {
        QueryDfsDTO queryDfsDTO = new QueryDfsDTO();
        DfsDTO dfsDTO = null;
        // 如果上传时没有选择存储方式,那么取默认存储方式
        if(StringUtils.isEmpty(dfsCode)) {
            queryDfsDTO.setDfsDefault(GitEggConstant.ENABLE);
        }
        else {
            queryDfsDTO.setDfsCode(dfsCode);
        }
        GitEggDfsFile gitEggDfsFile = null;
        DfsFile dfsFile = new DfsFile();
        try {
            dfsDTO = dfsService.queryDfs(queryDfsDTO);
            IDfsBaseService dfsFileService = dfsFactory.getDfsBaseService(dfsDTO);
            //获取文件名
            String originalName = file.getOriginalFilename();
            //获取文件后缀
            String extension = FilenameUtils.getExtension(originalName);
            String hash = Etag.stream(file.getInputStream(), file.getSize());
            String fileName = hash + "." + extension;
            // 保存文件上传记录
            dfsFile.setDfsId(dfsDTO.getId());
            dfsFile.setOriginalName(originalName);
            dfsFile.setFileName(fileName);
            dfsFile.setFileExtension(extension);
            dfsFile.setFileSize(file.getSize());
            dfsFile.setFileStatus(GitEggConstant.ENABLE);
            //执行文件上传操作
            gitEggDfsFile = dfsFileService.uploadFile(file.getInputStream(), fileName);
            if (gitEggDfsFile != null)
            {
                gitEggDfsFile.setFileName(originalName);
                gitEggDfsFile.setKey(hash);
                gitEggDfsFile.setHash(hash);
                gitEggDfsFile.setFileSize(file.getSize());
            }
            dfsFile.setAccessUrl(gitEggDfsFile.getFileUrl());
        } catch (IOException e) {
            log.error("文件上传失败:{}", e);
            dfsFile.setFileStatus(GitEggConstant.DISABLE);
            dfsFile.setComments(String.valueOf(e));
        } finally {
            dfsFileService.save(dfsFile);
        }
        return gitEggDfsFile;
    }
    @Override
    public String getFileUrl(String dfsCode, String fileName) {
        String fileUrl = null;
        QueryDfsDTO queryDfsDTO = new QueryDfsDTO();
        DfsDTO dfsDTO = null;
        // 如果上传时没有选择存储方式,那么取默认存储方式
        if(StringUtils.isEmpty(dfsCode)) {
            queryDfsDTO.setDfsDefault(GitEggConstant.ENABLE);
        }
        else {
            queryDfsDTO.setDfsCode(dfsCode);
        }
        try {
            dfsDTO = dfsService.queryDfs(queryDfsDTO);
            IDfsBaseService dfsFileService = dfsFactory.getDfsBaseService(dfsDTO);
            fileUrl = dfsFileService.getFileUrl(fileName);
        }
        catch (Exception e)
        {
            log.error("文件上传失败:{}", e);
        }
        return fileUrl;
    }
    @Override
    public OutputStream downloadFile(String dfsCode, String fileName, OutputStream outputStream) {
        QueryDfsDTO queryDfsDTO = new QueryDfsDTO();
        DfsDTO dfsDTO = null;
        // 如果上传时没有选择存储方式,那么取默认存储方式
        if(StringUtils.isEmpty(dfsCode)) {
            queryDfsDTO.setDfsDefault(GitEggConstant.ENABLE);
        }
        else {
            queryDfsDTO.setDfsCode(dfsCode);
        }
        try {
            dfsDTO = dfsService.queryDfs(queryDfsDTO);
            IDfsBaseService dfsFileService = dfsFactory.getDfsBaseService(dfsDTO);
            outputStream = dfsFileService.getFileObject(fileName, outputStream);
        }
        catch (Exception e)
        {
            log.error("文件上传失败:{}", e);
        }
        return outputStream;
    }
}


7、新建GitEggDfsController用于文件上传下载通用访问控制器


@RestController
@RequestMapping("/extension")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Api(value = "GitEggDfsController|文件上传前端控制器")
@RefreshScope
public class GitEggDfsController {
    private final IGitEggDfsService gitEggDfsService;
    /**
     * 上传文件
     * @param uploadFile
     * @param dfsCode
     * @return
     */
    @PostMapping("/upload/file")
    public Result<?> uploadFile(@RequestParam("uploadFile") MultipartFile[] uploadFile, String dfsCode) {
        GitEggDfsFile gitEggDfsFile = null;
        if (ArrayUtils.isNotEmpty(uploadFile))
        {
            for (MultipartFile file : uploadFile) {
                gitEggDfsFile = gitEggDfsService.uploadFile(dfsCode, file);
            }
        }
        return Result.data(gitEggDfsFile);
    }
    /**
     * 通过文件名获取文件访问链接
     */
    @GetMapping("/get/file/url")
    @ApiOperation(value = "查询分布式存储配置表详情")
    public Result<?> query(String dfsCode, String fileName) {
        String fileUrl = gitEggDfsService.getFileUrl(dfsCode, fileName);
        return Result.data(fileUrl);
    }
    /**
     * 通过文件名以文件流的方式下载文件
     */
    @GetMapping("/get/file/download")
    public void downloadFile(HttpServletResponse response,HttpServletRequest request,String dfsCode, String fileName) {
        if (fileName != null) {
            response.setCharacterEncoding(request.getCharacterEncoding());
            response.setContentType("application/octet-stream");
            response.addHeader("Content-Disposition", "attachment;fileName=" + fileName);
            OutputStream os = null;
            try {
                os = response.getOutputStream();
                os = gitEggDfsService.downloadFile(dfsCode, fileName, os);
                os.flush();
                os.close();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (os != null) {
                    try {
                        os.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}


8、前端上传下载实现,注意的是:axios请求下载文件流时,需要设置 responseType: 'blob'


  • 上传


handleUploadTest (row) {
                this.fileList = []
                this.uploading = false
                this.uploadForm.dfsType = row.dfsType
                this.uploadForm.dfsCode = row.dfsCode
                this.uploadForm.uploadFile = null
                this.dialogTestUploadVisible = true
            },
            handleRemove (file) {
                const index = this.fileList.indexOf(file)
                const newFileList = this.fileList.slice()
                newFileList.splice(index, 1)
                this.fileList = newFileList
            },
            beforeUpload (file) {
                this.fileList = [...this.fileList, file]
                return false
            },
            handleUpload () {
                this.uploadedFileName = ''
                const { fileList } = this
                const formData = new FormData()
                formData.append('dfsCode', this.uploadForm.dfsCode)
                fileList.forEach(file => {
                  formData.append('uploadFile', file)
                })
                this.uploading = true
                dfsUpload(formData).then(() => {
                    this.fileList = []
                    this.uploading = false
                    this.$message.success('上传成功')
                }).catch(err => {
                  console.log('uploading', err)
                  this.$message.error('上传失败')
                })
            }


  • 下载


getFileUrl (row) {
                this.listLoading = true
                this.fileDownload.dfsCode = row.dfsCode
                this.fileDownload.fileName = row.fileName
                dfsGetFileUrl(this.fileDownload).then(response => {
                    window.open(response.data)
                    this.listLoading = false
                })
            },
            downLoadFile (row) {
                this.listLoading = true
                this.fileDownload.dfsCode = row.dfsCode
                this.fileDownload.fileName = row.fileName
                this.fileDownload.responseType = 'blob'
                dfsDownloadFileUrl(this.fileDownload).then(response => {
                    const blob = new Blob([response.data])
                    var fileName = row.originalName
                    const elink = document.createElement('a')
                    elink.download = fileName
                    elink.style.display = 'none'
                    elink.href = URL.createObjectURL(blob)
                    document.body.appendChild(elink)
                    elink.click()
                    URL.revokeObjectURL(elink.href)
                    document.body.removeChild(elink)
                    this.listLoading = false
                })
            }


  • 前端接口


import request from '@/utils/request'
export function dfsUpload (formData) {
    return request({
      url: '/gitegg-service-extension/extension/upload/file',
      method: 'post',
      data: formData
    })
}
export function dfsGetFileUrl (query) {
    return request({
      url: '/gitegg-service-extension/extension/get/file/url',
      method: 'get',
      params: query
    })
}
export function dfsDownloadFileUrl (query) {
  return request({
    url: '/gitegg-service-extension/extension/get/file/download',
    method: 'get',
    responseType: 'blob',
    params: query
  })
}


三、功能测试界面


1、批量上传


微信图片_20220519130836.png

上传界面


2、文件流下载及获取文件地址


微信图片_20220519130839.png

文件流下载及获取文件地址


备注


1、防止文件名重复,这里文件名统一采用七牛云的hash算法,可以防止文件重复,在界面需要展示的文件名,则存储到数据库一个文件名字段进行展示。所有的上传文件都留有记录。

相关实践学习
对象存储OSS快速上手——如何使用ossbrowser
本实验是对象存储OSS入门级实验。通过本实验,用户可学会如何用对象OSS的插件,进行简单的数据存、查、删等操作。
相关文章
|
9月前
|
数据可视化 Java BI
将 Spring 微服务与 BI 工具集成:最佳实践
本文探讨了 Spring 微服务与商业智能(BI)工具集成的潜力与实践。随着微服务架构和数据分析需求的增长,Spring Boot 和 Spring Cloud 提供了构建可扩展、弹性服务的框架,而 BI 工具则增强了数据可视化与实时分析能力。文章介绍了 Spring 微服务的核心概念、BI 工具在企业中的作用,并深入分析了两者集成带来的优势,如实时数据处理、个性化报告、数据聚合与安全保障。同时,文中还总结了集成过程中的最佳实践,包括事件驱动架构、集中配置管理、数据安全控制、模块化设计与持续优化策略,旨在帮助企业构建高效、智能的数据驱动系统。
430 1
将 Spring 微服务与 BI 工具集成:最佳实践
|
前端开发 Java API
微服务——SpringBoot使用归纳——Spring Boot集成 Swagger2 展现在线接口文档—— Swagger 简介
第6课介绍了在Spring Boot中集成Swagger2以展示在线接口文档的方法。随着前后端分离架构的发展,API文档成为连接前端与后端开发的重要纽带。然而,代码更新频繁导致文档难以同步维护,Swagger2解决了这一问题。通过Swagger,在线API文档不仅方便了接口调用方查看和测试,还支持开发者实时测试接口数据。本文使用Swagger 2.2.2版本,讲解如何在Spring Boot项目中导入并配置Swagger2工具,从而高效管理接口文档。
458 0
|
11月前
|
存储 负载均衡 Java
SpringCloud框架
本文介绍了微服务架构中常用的技术组件与原理,包括Nacos与Eureka的服务注册与发现机制、Nacos的分级存储模型、OpenFeign的远程调用流程、Ribbon与Spring LoadBalancer的负载均衡策略、Hystrix与Sentinel的限流熔断机制、滑动窗口算法原理,以及Spring Cloud Gateway的路由断言与过滤器功能,全面覆盖微服务核心治理能力。
|
人工智能 数据可视化 JavaScript
颠覆开发效率!国内首个微服务编排框架Juggle开源啦!
Juggle是国内首个开源的微服务编排框架,专注于解决企业微服务进程中接口重复开发、系统对接复杂等问题。它提供零代码、低代码和AI增强功能,通过可视化拖拽快速组装简单API为复杂接口,支持多协议、多语言脚本和流程多版本管理。相比国外框架如Conductor,Juggle更贴合国内需求,具备高效开发、企业级可靠性及信创适配等优势,助力企业实现敏捷创新与数字化转型。
1082 0
颠覆开发效率!国内首个微服务编排框架Juggle开源啦!
|
移动开发 Java 测试技术
HarmonyOS NEXT~鸿蒙系统与mPaaS三方框架集成指南
本文详细介绍了鸿蒙系统(HarmonyOS)与mPaaS框架的集成方法。鸿蒙系统作为华为开发的分布式操作系统,具备分布式架构、微内核设计等特性;mPaaS是蚂蚁金服推出的移动开发平台,提供金融级组件和全生命周期管理能力。文章从环境准备、核心功能集成(如初始化、用户认证、支付功能)、适配问题解决到调试测试及最佳实践,全方位指导开发者高效集成两者。通过遵循指南,可充分利用鸿蒙的特性和mPaaS的金融能力,构建高性能、高安全性的应用,同时避免常见兼容性问题,缩短开发周期。
653 0
|
人工智能 达摩院 并行计算
VideoRefer:阿里达摩院开源视频对象感知与推理框架,可集成 VLLM 提升其空间和时间理解能力
VideoRefer 是浙江大学与阿里达摩学院联合推出的视频对象感知与推理技术,支持细粒度视频对象理解、复杂关系分析及多模态交互,适用于视频剪辑、教育、安防等多个领域。
852 17
VideoRefer:阿里达摩院开源视频对象感知与推理框架,可集成 VLLM 提升其空间和时间理解能力
|
存储 监控 数据可视化
SaaS云计算技术的智慧工地源码,基于Java+Spring Cloud框架开发
智慧工地源码基于微服务+Java+Spring Cloud +UniApp +MySql架构,利用传感器、监控摄像头、AI、大数据等技术,实现施工现场的实时监测、数据分析与智能决策。平台涵盖人员、车辆、视频监控、施工质量、设备、环境和能耗管理七大维度,提供可视化管理、智能化报警、移动智能办公及分布计算存储等功能,全面提升工地的安全性、效率和质量。
369 0
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
543 2
|
Dubbo Java 应用服务中间件
Dubbo学习圣经:从入门到精通 Dubbo3.0 + SpringCloud Alibaba 微服务基础框架
尼恩团队的15大技术圣经,旨在帮助开发者系统化、体系化地掌握核心技术,提升技术实力,从而在面试和工作中脱颖而出。本文介绍了如何使用Dubbo3.0与Spring Cloud Gateway进行整合,解决传统Dubbo架构缺乏HTTP入口的问题,实现高性能的微服务网关。
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
1017 1