柏竹 柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档

柏竹

奋斗柏竹
首页
后端
前端
  • 应用推荐
关于
友链
  • 分类
  • 标签
  • 归档
  • Java基础

  • JavaWeb

  • 拓展技术

  • 框架技术

  • 数据库

  • 数据结构

  • Spring

  • SpringMVC

  • SpringBoot

  • SpringClound

  • Ruoyi-Vue-Plus

    • ruoyi-vue-plus-基础功能
    • ruoyi-vue-plus-权限控制
    • ruoyi-vue-plus-表格操作
    • ruoyi-vue-plus-缓存功能
    • ruoyi-vue-plus-日志功能
    • ruoyi-vue-plus-线程相关
    • ruoyi-vue-plus-OSS功能
      • aws3 存储服务
      • 运作过程
        • 初始化
        • OssFactory
        • OssClient
      • 上传配置项
      • Redis存储
      • 应用示例
        • 上传
        • 访问
        • 下载
        • 删除
        • 前端部分
      • 供应商配置项
    • ruoyi-vue-plus-代码生成功能
    • ruoyi-vue-plus-多数据源
    • ruoyi-vue-plus-任务调度
    • ruoyi-vue-plus-监控功能
    • ruoyi-vue-plus-国际化
    • ruoyi-vue-plus-XSS功能
    • ruoyi-vue-plus-防重幂&限流 功能
    • ruoyi-vue-plus-推送功能
    • ruoyi-vue-plus-序列化功能
    • ruoyi-vue-plus-数据加密
    • ruoyi-vue-plus-单元测试
    • ruoyi-vue-plus-前端插件
    • ruoyi-vue-plus-前端工具篇
    • ruoyi-vue-plus-部署篇
    • ruoyi-vue-plus-前端篇
    • ruoyi-vue-plus-后端工具篇
    • ruoyi-vue-plus-框架篇
    • ruoyi-vue-plus-问题解决
  • 后端
  • Ruoyi-Vue-Plus
柏竹
2023-12-05
目录

ruoyi-vue-plus-OSS功能

# aws3 存储服务

云存储接口协议 , 规范化应用 , 各大厂商均支持的文件存储的协议

参考文档

  • aws3配置文档 : https://docs.amazonaws.cn (opens new window)
  • ruoyi-vue-plus应用文档 : https://plus-doc.dromara.org (opens new window)

涉及信息标识

  • OssFactory Oss工厂类

  • OssClient 客户端操作类

  • sys_oss 存储文件库名

  • sys_oss_config 存储供应商配置库名

# 运作过程

  1. 项目启动 初始化配置信息
  2. Oss工厂获取通信 OssClient
  3. OssClient操作文件

# 初始化

初始化大致流程

  1. 项目运行调用 SysOssConfigServiceImpl#init()
  2. 获取库 所有的 oss供应商配置
  3. 将启用的 oss供应商 缓存到 sys_oss:default_config
  4. 将所有 oss配置缓存 缓存到 sys_oss_config

提示

该框架已将 Cache缓存 托管给Redis存储 , 点击跳转了解 (opens new window)

SysteamApplicationRunner初始化OSS配置类

SpringBoot 启动成功后执行

点击展开
@Slf4j
@RequiredArgsConstructor
@Component
public class SystemApplicationRunner implements ApplicationRunner {

    private final RuoYiConfig ruoyiConfig;
    private final ISysConfigService configService;
    private final ISysDictTypeService dictTypeService;
    private final ISysOssConfigService ossConfigService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        ossConfigService.init();
        log.info("初始化OSS配置成功");
        if (ruoyiConfig.isCacheLazy()) {
            return;
        }
        configService.loadingConfigCache();
        log.info("加载参数缓存数据成功");
        dictTypeService.loadingDictCache();
        log.info("加载字典缓存数据成功");
    }

}
@Slf4j
@RequiredArgsConstructor
@Service
public class SysOssConfigServiceImpl implements ISysOssConfigService {

    private final SysOssConfigMapper baseMapper;

    /**
     * 项目启动时,初始化参数到缓存,加载配置类
     */
    @Override
    public void init() {
        List<SysOssConfig> list = baseMapper.selectList();
        // 加载OSS初始化配置
        for (SysOssConfig config : list) {
            String configKey = config.getConfigKey();
            if ("0".equals(config.getStatus())) {
                RedisUtils.setCacheObject(OssConstant.DEFAULT_CONFIG_KEY, configKey);
            }
            CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config));
        }
    }
}

实现 ApplicationRunner接口的run()方法 , 启动成功后执行run()方法

# OssFactory

Oss工厂主要负责提供 OssClient , 其中套有一层 缓存/校验配置 , 一旦 缓存不存在/校验配置有修改 则重新获取 OssClient 并缓存

点击展开
@Slf4j
public class OssFactory {

    private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();

    /**
     * 获取默认实例
     */
    public static OssClient instance() {
        // 获取redis 默认类型
        String configKey = RedisUtils.getCacheObject(OssConstant.DEFAULT_CONFIG_KEY);
        if (StringUtils.isEmpty(configKey)) {
            throw new OssException("文件存储服务类型无法找到!");
        }
        return instance(configKey);
    }

    /**
     * 根据类型获取实例
     */
    public static OssClient instance(String configKey) {
        String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
        if (json == null) {
            throw new OssException("系统异常, '" + configKey + "'配置信息不存在!");
        }
        OssProperties properties = JsonUtils.parseObject(json, OssProperties.class);
        OssClient client = CLIENT_CACHE.get(configKey);
        if (client == null) {
            CLIENT_CACHE.put(configKey, new OssClient(configKey, properties));
            log.info("创建OSS实例 key => {}", configKey);
            return CLIENT_CACHE.get(configKey);
        }
        // 配置不相同则重新构建
        if (!client.checkPropertiesSame(properties)) {
            CLIENT_CACHE.put(configKey, new OssClient(configKey, properties));
            log.info("重载OSS实例 key => {}", configKey);
            return CLIENT_CACHE.get(configKey);
        }
        return client;
    }

}

# OssClient

OssClient 提供文件操作的服务 , 用于 上传 , 删除 , 访问 等操作

核心部分

  • OssClient()构造方法 , 根据配置信息构建 通信客户端
  • upload()上传方法 , 通过 文件流和路径 实现上传文件功能
  • delete()删除方法 , 只需提供路径即可直接删除

提示

创建桶时 需要配置桶策略 , 其策略分别有: 只读 , 只写 , 读写 三种

策略配置文档 : https://docs.amazonaws.cn (opens new window)

点击展开











 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 





 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

 
 
 
 
 
 
 
 
 





































































































































public class OssClient {

    // 厂商标识
    private final String configKey;

    // 配置信息
    private final OssProperties properties;

    // S3 客户端
    private final AmazonS3 client;

    public OssClient(String configKey, OssProperties ossProperties) {
        this.configKey = configKey;
        this.properties = ossProperties;
        try {
            // 访问端点 , 指定 IP/域名 和 存储区域标识
            AwsClientBuilder.EndpointConfiguration endpointConfig =
                new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());

            // 获取授权凭证
            AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
            AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
            // 设置访问协议
            ClientConfiguration clientConfig = new ClientConfiguration();
            if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
                clientConfig.setProtocol(Protocol.HTTPS);
            } else {
                clientConfig.setProtocol(Protocol.HTTP);
            }
            AmazonS3ClientBuilder build = AmazonS3Client.builder()
                .withEndpointConfiguration(endpointConfig) // 端点
                .withClientConfiguration(clientConfig) // 客户端配置
                .withCredentials(credentialsProvider) // 访问凭证
                .disableChunkedEncoding(); // 禁用分块编码
            // 不在云服务商的使用域名访问 , 默认采用minio
            if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
                // minio 使用https限制使用域名访问 需要此配置 站点填域名
                build.enablePathStyleAccess();
            }
            this.client = build.build();

            // 创建桶
            createBucket();
        } catch (Exception e) {
            if (e instanceof OssException) {
                throw e;
            }
            throw new OssException("配置错误! 请检查系统配置:[" + e.getMessage() + "]");
        }
    }

    public void createBucket() {
        try {
            String bucketName = properties.getBucketName();
            // 如果Bucket已存在不做操作
            if (client.doesBucketExistV2(bucketName)) {
                return;
            }
            // 创建桶 (发创建桶请求)
            CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
            // 设置桶访问策略并配置桶
            AccessPolicyType accessPolicy = getAccessPolicy();
            createBucketRequest.setCannedAcl(accessPolicy.getAcl());
            client.createBucket(createBucketRequest);
            client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
        } catch (Exception e) {
            throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
        }
    }

    public UploadResult upload(byte[] data, String path, String contentType) {
        return upload(new ByteArrayInputStream(data), path, contentType);
    }

    public UploadResult upload(InputStream inputStream, String path, String contentType) {
        if (!(inputStream instanceof ByteArrayInputStream)) {
            inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
        }
        try {
            // 元数据
            ObjectMetadata metadata = new ObjectMetadata();
            // 上传文件类型 (后缀)
            metadata.setContentType(contentType);
            // 文件流
            metadata.setContentLength(inputStream.available());
            // 设置要上传的文件相关信息
            PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
            // 设置上传对象的 Acl 为公共读
            putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
            // 上传
            client.putObject(putObjectRequest);
        } catch (Exception e) {
            throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
        }
        // 返回上传结果
        return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
    }

    public UploadResult upload(File file, String path) {
        try {
            PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
            // 设置上传对象的 Acl 为公共读
            putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
            client.putObject(putObjectRequest);
        } catch (Exception e) {
            throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
        }
        return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
    }

    public void delete(String path) {
        // 去除前部分 (根据Client的自定义域名配置)
        path = path.replace(getUrl() + "/", "");
        try {
            client.deleteObject(properties.getBucketName(), path);
        } catch (Exception e) {
            throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
        }
    }

    public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
        return upload(data, getPath(properties.getPrefix(), suffix), contentType);
    }

    public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
        return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
    }

    public UploadResult uploadSuffix(File file, String suffix) {
        return upload(file, getPath(properties.getPrefix(), suffix));
    }

    /**
     * 获取文件元数据
     *
     * @param path 完整文件路径
     */
    public ObjectMetadata getObjectMetadata(String path) {
        path = path.replace(getUrl() + "/", "");
        S3Object object = client.getObject(properties.getBucketName(), path);
        return object.getObjectMetadata();
    }

    public InputStream getObjectContent(String path) {
        path = path.replace(getUrl() + "/", "");
        S3Object object = client.getObject(properties.getBucketName(), path);
        return object.getObjectContent();
    }

    public String getUrl() {
        String domain = properties.getDomain();
        String endpoint = properties.getEndpoint();
        String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
        // 云服务商直接返回
        if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
            if (StringUtils.isNotBlank(domain)) {
                return header + domain;
            }
            return header + properties.getBucketName() + "." + endpoint;
        }
        // minio 单独处理 (需要配置指定桶)
        if (StringUtils.isNotBlank(domain)) {
            return header + domain + "/" + properties.getBucketName();
        }
        return header + endpoint + "/" + properties.getBucketName();
    }

    public String getPath(String prefix, String suffix) {
        // 生成uuid
        String uuid = IdUtil.fastSimpleUUID();
        // 文件路径
        String path = DateUtils.datePath() + "/" + uuid;
        if (StringUtils.isNotBlank(prefix)) {
            path = prefix + "/" + path;
        }
        return path + suffix;
    }


    public String getConfigKey() {
        return configKey;
    }

    /**
     * 获取私有URL链接
     *
     * @param objectKey 对象KEY
     * @param second    授权时间
     */
    public String getPrivateUrl(String objectKey, Integer second) {
        GeneratePresignedUrlRequest generatePresignedUrlRequest =
            new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
                .withMethod(HttpMethod.GET)
                .withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
        URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
        return url.toString();
    }

    /**
     * 检查配置是否相同
     */
    public boolean checkPropertiesSame(OssProperties properties) {
        return this.properties.equals(properties);
    }

    /**
     * 获取当前桶权限类型
     *
     * @return 当前桶权限类型code
     */
    public AccessPolicyType getAccessPolicy() {
        return AccessPolicyType.getByType(properties.getAccessPolicy());
    }

    private static String getPolicy(String bucketName, PolicyType policyType) {
        StringBuilder builder = new StringBuilder();
        builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
        if (policyType == PolicyType.WRITE) {
            builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n");
        } else if (policyType == PolicyType.READ_WRITE) {
            builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n");
        } else {
            builder.append("\"s3:GetBucketLocation\"\n");
        }
        builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
        builder.append(bucketName);
        builder.append("\"\n},\n");
        if (policyType == PolicyType.READ) {
            builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
            builder.append(bucketName);
            builder.append("\"\n},\n");
        }
        builder.append("{\n\"Action\": ");
        switch (policyType) {
            case WRITE:
                builder.append("[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n");
                break;
            case READ_WRITE:
                builder.append("[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n");
                break;
            default:
                builder.append("\"s3:GetObject\",\n");
                break;
        }
        builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
        builder.append(bucketName);
        builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
        return builder.toString();
    }

}

# 上传配置项

配置文件后端限制上传大小

 # 单个文件上传大小
 spring.servlet.multipart.max-file-size=10MB
 # 设置总上传的文件大小
 spring.servlet.multipart.max-request-size=10MB

# Redis存储

类型 key val 说明
hash sys_oss_config field: <供应商标识key> ; value: <jsonOss配置> 存储所有供应商配置信息
spring sys_oss:default_config <供应商标识key> 选中应用的供应商配置
hash sys_oss field: <ossId> ; value: <oss对象文件信息> 访问文件存储的信息

# 应用示例

# 上传

上传 SysOssController#upload()

点击展开
@SaCheckPermission("system:oss:upload")
@Log(title = "OSS对象存储", businessType = BusinessType.INSERT)
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<SysOssUploadVo> upload(@RequestPart("file") MultipartFile file) {
    if (ObjectUtil.isNull(file)) {
        return R.fail("上传文件不能为空");
    }
    // 主要存储
    SysOssVo oss = ossService.upload(file);
    SysOssUploadVo uploadVo = new SysOssUploadVo();
    uploadVo.setUrl(oss.getUrl());
    uploadVo.setFileName(oss.getOriginalName());
    uploadVo.setOssId(oss.getOssId().toString());
    return R.ok(uploadVo);
}

上传实现 SysOssServiceImpl#upload()

点击展开
@Override
public SysOssVo upload(MultipartFile file) {
    // 
    String originalfileName = file.getOriginalFilename();
    String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
    // 获取 oss实例 
    OssClient storage = OssFactory.instance();
    UploadResult uploadResult;
    try {
        uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
    } catch (IOException e) {
        throw new ServiceException(e.getMessage());
    }
    // 保存文件信息
    SysOss oss = new SysOss();
    oss.setUrl(uploadResult.getUrl());
    oss.setFileSuffix(suffix);
    oss.setFileName(uploadResult.getFilename());
    oss.setOriginalName(originalfileName);
    oss.setService(storage.getConfigKey());
    baseMapper.insert(oss);
    SysOssVo sysOssVo = MapstructUtils.convert(oss, SysOssVo.class);
    return this.matchingUrl(sysOssVo);
}

# 访问

查询 SysOssController#listByIds()

@SaCheckPermission("system:oss:list")
@GetMapping("/listByIds/{ossIds}")
public R<List<SysOssVo>> listByIds(@NotEmpty(message = "主键不能为空")
                                   @PathVariable Long[] ossIds) {
    List<SysOssVo> list = ossService.listByIds(Arrays.asList(ossIds));
    return R.ok(list);
}

查询实现 SysOssServiceImpl#listByIds()

采用代理形式调用其目的主要是触发 SpringCache缓存@Cacheable , 从缓存中获取文件相关信息

@Override
public List<SysOssVo> listByIds(Collection<Long> ossIds) {
    List<SysOssVo> list = new ArrayList<>();
    for (Long id : ossIds) {
        SysOssVo vo = SpringUtils.getAopProxy(this).getById(id);
        if (ObjectUtil.isNotNull(vo)) {
            // private策略处理(私有则返回有效期的url)
            list.add(this.matchingUrl(vo));
        }
    }    return list;
}

单个文件信息获取 SysOssServiceImpl#getById()

/* 当查询数据时, 会优先查缓存是否存在 */
@Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#ossId")
@Override
public SysOssVo getById(Long ossId) {
    return baseMapper.selectVoById(ossId);
}

# 下载

下载接口 SysOssController#download()

@SaCheckPermission("system:oss:download")
@GetMapping("/download/{ossId}")
public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException {
    ossService.download(ossId, response);
}

下载实现 SysOssServiceImpl#download()

点击展开
@Override
public void download(Long ossId, HttpServletResponse response) throws IOException {
    SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId);
    if (ObjectUtil.isNull(sysOss)) {
        throw new ServiceException("文件数据不存在!");
    }
    // 响应头编码
    FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
    response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
    OssClient storage = OssFactory.instance();
    try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
        int available = inputStream.available();
        IoUtil.copy(inputStream, response.getOutputStream(), available);
        response.setContentLength(available);
    } catch (Exception e) {
        throw new ServiceException(e.getMessage());
    }
}

响应头文件处理 FileUtils.setAttachmentResponseHeader()

public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) {
    String percentEncodedFileName = percentEncode(realFileName);
    String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName);
    response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
    response.setHeader("Content-disposition", contentDispositionValue);
    response.setHeader("download-filename", percentEncodedFileName);
}

# 删除

删除接口 SysOssController#remove()

@SaCheckPermission("system:oss:remove")
@Log(title = "OSS对象存储", businessType = BusinessType.DELETE)
@DeleteMapping("/{ossIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
                      @PathVariable Long[] ossIds) {
    return toAjax(ossService.deleteWithValidByIds(List.of(ossIds), true));
}

删除实现 SysOssServiceImpl#deleteWithValidByIds()

@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
    if (isValid) {
        // 做一些业务上的校验,判断是否需要校验
    }
    List<SysOss> list = baseMapper.selectBatchIds(ids);
    for (SysOss sysOss : list) {
        OssClient storage = OssFactory.instance(sysOss.getService());
        storage.delete(sysOss.getUrl());
    }
    return baseMapper.deleteBatchIds(ids) > 0;
}

# 前端部分

前端篇章 : 点击跳转

# 供应商配置项

引用外部供应商需要以下配置

配置项 标识
endpoint 访问站点 . 访问oss供应商主要url
domain 自定义域名
prefix 前缀 , 文件路径前缀名
accessKey 密钥
secretKey 密钥
bucketName 桶空间
region 存储区域
isHttps 是否https(Y=是,N=否)
accessPolicy 桶权限类型(0private; 1public; 2custom)
#ruoyi-vue-plus

← ruoyi-vue-plus-线程相关 ruoyi-vue-plus-代码生成功能→

最近更新
01
HTTPS自动续签
10-21
02
博客搭建-简化版(脚本)
10-20
03
ruoyi-vue-plus-部署篇
07-13
更多文章>
Theme by Vdoing | Copyright © 2019-2024 | 桂ICP备2022009417号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式