JAVA后端对象存储( 图片分享平台)详解

本部分内容主要来源于鱼皮智能协图云图库部分,并在笔者个人项目学习的基础上进行扩展衍生。

本地存储

直接将文件保存在本地服务器的磁盘目录中

java 复制代码
package com.itheima.controller;

import com.itheima.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.UUID;

@Slf4j
@RestController
public class UploadController {

    private static final String UPLOAD_DIR = "D:/images/";
    /**
     * 上传文件 - 参数名file
     */
    @PostMapping("/upload")
    public Result upload(MultipartFile file) throws Exception {
        log.info("上传文件:{}, {}, {}", username, age, file);
        if (!file.isEmpty()) {
            // 生成唯一文件名
            String originalFilename = file.getOriginalFilename();
            //获取扩展名
            String extName = originalFilename.substring(originalFilename.lastIndexOf("."));
            String uniqueFileName = UUID.randomUUID().toString().replace("-", "") + extName;
            // 拼接完整的文件路径
            File targetFile = new File(UPLOAD_DIR + uniqueFileName);

            // 如果目标目录不存在,则创建它
            if (!targetFile.getParentFile().exists()) {
                targetFile.getParentFile().mkdirs();
            }
            // 保存文件
            file.transferTo(targetFile);
        }
        return Result.success();
    }
}

要点:

MultipartFile,用来接收上传的文件

MultipartFile 常见方法:

  • String getOriginalFilename(); //获取原始文件名

  • void transferTo(File dest);`` //将接收的文件转存到磁盘文件中

  • long getSize(); //获取文件的大小,单位:字节

  • byte[] getBytes();`` //获取文件内容的字节数组

  • InputStream getInputStream(); //获取接收到的文件内容的输入流

originalFilename.lastIndexOf("."):找到原始文件名中最后一个点(.)的位置

改变 Tomcat 服务器默认限制的文件上传大小:

java 复制代码
spring:  
  servlet:  
    multipart:  
      max-file-size: 10MB

优缺点:

优点:方便快捷

缺点:

  • 不安全:磁盘如果损坏,所有的文件就会丢失1

  • 容量有限:如果存储大量的图片,磁盘空间有限(磁盘不可能无限制扩容)

  • 无法直接访问

自己搭建存储服务器

使用fastDFS 、MinIO等。等我后续使用了再补充。

使用现成云服务

在黑马javaweb中使用了阿里云oss,而鱼皮这里使用的是腾讯云。实际上操作都大同小异,因此这里只详细讲解使用腾讯云的过程。

使用流程

新建腾讯云账号--开通腾讯云对象存储--创建存储桶(Bucket)--开通数据万象(后续解析图片)--后端代码操作对象(SDK或API)

后端代码操作对象

引入依赖

bash 复制代码
  
<dependency>  
    <groupId>com.qcloud</groupId>  
    <artifactId>cos_api</artifactId>  
    <version>5.6.227</version>  
</dependency>

CosClientConfig

java 复制代码
@Configuration  
@ConfigurationProperties(prefix = "cos.client")  
@Data  
public class CosClientConfig {  
  
      
    private String host;  
  
      
    private String secretId;  
  
      
    private String secretKey;  
  
      
    private String region;  
  
      
    private String bucket;  
  
    @Bean  
    public COSClient cosClient() {  
        
        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);  
        
        ClientConfig clientConfig = new ClientConfig(new Region(region));  
        
        return new COSClient(cred, clientConfig);  
    }  
}

读取配置,创建COS客户端Bean类

@Value适合单个零散配置,@ConfigurationProperties适合批量绑定一组前缀相同的配置,更适合配置类场景。都是用来读取.yml文件的配置信息。

配置文件填写

java 复制代码
cos:  
  client:  
    host: xxx  
    secretId: xxx  
    secretKey: xxx  
    region: xxx  
    bucket: xxx

保存在local配置文件中,在.gitignore中忽略该文件的提交

编写通用能力类

创建CosManager类,编写文件的上传与下载操作

文件上传

java 复制代码
  

@Component  
public class CosManager {  
  
    @Resource  
    private CosClientConfig cosClientConfig;  
  
    @Resource  
    private COSClient cosClient;  

    public PutObjectResult putObject(String key, File file) {  
    PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,  
            file);  
    return cosClient.putObject(putObjectRequest);  
}
  
    
}

文件上传接口

java 复制代码
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)  
@PostMapping("/test/upload")  
public BaseResponse<String> testUploadFile(@RequestPart("file") MultipartFile multipartFile) {  
    
    String filename = multipartFile.getOriginalFilename();  
    String filepath = String.format("/test/%s", filename);  
    File file = null;  
    try {  
        
        file = File.createTempFile(filepath, null);  
        multipartFile.transferTo(file);  
        cosManager.putObject(filepath, file);  
        
        return ResultUtils.success(filepath);  
    } catch (Exception e) {  
        log.error("file upload error, filepath = " + filepath, e);  
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");  
    } finally {  
        if (file != null) {  
            
            boolean delete = file.delete();  
            if (!delete) {  
                log.error("file delete error, filepath = {}", filepath);  
            }  
        }  
    }  
}

要点:

1.创建本地空文件 createTempFile(filepath, null)

2.目标文件转存本地 transferTo(file)

3.传入路径与文件,上传云端

4.删除本地文件

文件下载

1.下载到后端进行处理

2.获取文件下载输入流传给前端

3.获取URL路径访问传给前端

方式1:

java 复制代码
  
public COSObject getObject(String key) {  
    GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key);  
    return cosClient.getObject(getObjectRequest);  
}

文件下载接口

java 复制代码
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)  
@GetMapping("/test/download/")  
public void testDownloadFile(String filepath, HttpServletResponse response) throws IOException {  
    COSObjectInputStream cosObjectInput = null;  
    try {  
        COSObject cosObject = cosManager.getObject(filepath);  
        cosObjectInput = cosObject.getObjectContent();  
        
        byte[] bytes = IOUtils.toByteArray(cosObjectInput);  
        
        response.setContentType("application/octet-stream;charset=UTF-8");  
        response.setHeader("Content-Disposition", "attachment; filename=" + filepath);  
        
        response.getOutputStream().write(bytes);  
        response.getOutputStream().flush();  
    } catch (Exception e) {  
        log.error("file download error, filepath = " + filepath, e);  
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "下载失败");  
    } finally {  
        if (cosObjectInput != null) {  
            cosObjectInput.close();  
        }  
    }  
}

要点:

1.通过路径获取文件内容访问入口CosObject

2.获取文件输入流cosObjectInput

3.将文件输入流转换为字节数组

4.通过response的输出流getOutputStream传给前端

5.刷新流,关闭流

文件解析

通过数据万象进行解析,在CosManager添加解析方法

java 复制代码
  
public PutObjectResult putPictureObject(String key, File file) {  
    PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,  
            file);  
    
    PicOperations picOperations = new PicOperations();  
    
    picOperations.setIsPicInfo(1);  
    
    putObjectRequest.setPicOperations(picOperations);  
    return cosClient.putObject(putObjectRequest);  
}

不用记,直接看文档

以上即为云存储的基本工作流程。接下来详细讲解鱼皮智能协图云库项目要点。

智能协图云图库

设计库表

sql 复制代码
create table if not exists picture  
(  
    id           bigint auto_increment comment 'id' primary key,  
    url          varchar(512)                       not null comment '图片 url',  
    name         varchar(128)                       not null comment '图片名称',  
    introduction varchar(512)                       null comment '简介',  
    category     varchar(64)                        null comment '分类',  
    tags         varchar(512)                      null comment '标签(JSON 数组)',  
    picSize      bigint                             null comment '图片体积',  
    picWidth     int                                null comment '图片宽度',  
    picHeight    int                                null comment '图片高度',  
    picScale     double                             null comment '图片宽高比例',  
    picFormat    varchar(32)                        null comment '图片格式',  
    userId       bigint                             not null comment '创建用户 id',  
    createTime   datetime default CURRENT_TIMESTAMP not null comment '创建时间',  
    editTime     datetime default CURRENT_TIMESTAMP not null comment '编辑时间',  
    updateTime   datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',  
    isDelete     tinyint  default 0                 not null comment '是否删除',  
    INDEX idx_name (name),                 
    INDEX idx_introduction (introduction), 
    INDEX idx_category (category),         
    INDEX idx_tags (tags),                 
    INDEX idx_userId (userId)              
) comment '图片' collate = utf8mb4_unicode_ci;

生成基础代码

1.通过MybatisX插件生成实体类,Mapper,Service等基础代码。

2.优化基础代码:主键自增->雪花算法(IdType.ASSIGN_ID),逻辑删除(@TableLogic)

接下来遵循一个请求接口对应一个DTO类,对应一个返回VO类进行设计增删改查的过程。

图片上传

1.数据模型,dto与vo设计

dto:PictureUploadRequest包含id,支持重复上传同一id图片的修改上传

vo:PictureVO包含vo转obj与obj转vo的方法。

要点:

简单属性复制--BeanUtils.copyProperties()

复杂属性--picture.setTags(JSONUtil.toJsonStr(pictureVO.getTags()));与 pictureVO.setTags(JSONUtil.toList(picture.getTags(), String.class));

Picture 是 "和数据库打交道的工具",数据库能存什么类型,它的字段就是什么类型;

PictureVO 是 "和前端打交道的工具",前端需要什么类型,它的字段就是什么类型;

数据库无法存储 List 集合,JSON 字符串是数组型标签的最优存储格式(兼顾可读性、查询精准性);

2.编写FileManager

java 复制代码
public UploadPictureResult uploadPicture(MultipartFile multipartFile, String uploadPathPrefix) {  
    
    validPicture(multipartFile);  
    
    String uuid = RandomUtil.randomString(16);  
    String originFilename = multipartFile.getOriginalFilename();  
    String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,  
            FileUtil.getSuffix(originFilename));  
    String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename);  
    File file = null;  
    try {  
        
        file = File.createTempFile(uploadPath, null);  
        multipartFile.transferTo(file);  
        
        PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);  
        ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();  
        
        UploadPictureResult uploadPictureResult = new UploadPictureResult();  
        int picWidth = imageInfo.getWidth();  
        int picHeight = imageInfo.getHeight();  
        double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();  
        uploadPictureResult.setPicName(FileUtil.mainName(originFilename));  
        uploadPictureResult.setPicWidth(picWidth);  
        uploadPictureResult.setPicHeight(picHeight);  
        uploadPictureResult.setPicScale(picScale);  
        uploadPictureResult.setPicFormat(imageInfo.getFormat());  
        uploadPictureResult.setPicSize(FileUtil.size(file));  
        uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath);  
        return uploadPictureResult;  
    } catch (Exception e) {  
        log.error("图片上传到对象存储失败", e);  
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");  
    } finally {  
        this.deleteTempFile(file);  
    }  
}  
  
  
public void validPicture(MultipartFile multipartFile) {  
    ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空");  
    
    long fileSize = multipartFile.getSize();  
    final long ONE_M = 1024 * 1024L;  
    ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M");  
    
    String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());  
    
    final List<String> ALLOW_FORMAT_LIST = Arrays.asList("jpeg", "jpg", "png", "webp");  
    ThrowUtils.throwIf(!ALLOW_FORMAT_LIST.contains(fileSuffix), ErrorCode.PARAMS_ERROR, "文件类型错误");  
}  
  
  
public void deleteTempFile(File file) {  
    if (file == null) {  
        return;  
    }  
    
    boolean deleteResult = file.delete();  
    if (!deleteResult) {  
        log.error("file delete error, filepath = {}", file.getAbsolutePath());  
    }  
}

要点:

1.校验图片是否合规validPicture()

2.获取上传的文件名与路径,getSuffix()获取后缀,

3.上传文件并解析

4.删除上传时生成的本地文件

服务开发

Controller->service->serviceimpl

图片增删改查

编写通过PictureQueryRequest构建QueryWrapper的方法

没有太大难度,这里依旧详细讲解一下分页查询的要点

java 复制代码
@Override  
public Page<PictureVO> getPictureVOPage(Page<Picture> picturePage, HttpServletRequest request) {  
    List<Picture> pictureList = picturePage.getRecords();  
    Page<PictureVO> pictureVOPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal());  
    if (CollUtil.isEmpty(pictureList)) {  
        return pictureVOPage;  
    }  
    
    List<PictureVO> pictureVOList = pictureList.stream().map(PictureVO::objToVo).collect(Collectors.toList());  
    
    Set<Long> userIdSet = pictureList.stream().map(Picture::getUserId).collect(Collectors.toSet());  
    Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()  
            .collect(Collectors.groupingBy(User::getId));  
    
    pictureVOList.forEach(pictureVO -> {  
        Long userId = pictureVO.getUserId();  
        User user = null;  
        if (userIdUserListMap.containsKey(userId)) {  
            user = userIdUserListMap.get(userId).get(0);  
        }  
        pictureVO.setUser(userService.getUserVO(user));  
    });  
    pictureVOPage.setRecords(pictureVOList);  
    return pictureVOPage;  
}

1.获取分页对象,对象实体+元信息pictureService.page(),返回Page<T>类型。

2.分页对象转VO对象

(1)通过分页对象获取对象实体getRecords(),返回List<T>。

(2)构建空的Page<T>VO对象,只包含多少条数据页数等元信息

(3)处理对象实体,这里利用set避免重复查询

(4)对象实体添加到空的Page<T>VO对象里

3.返回VO对象

获取预置标签和分类

java 复制代码
@GetMapping("/tag_category")  
public BaseResponse<PictureTagCategory> listPictureTagCategory() {  
    PictureTagCategory pictureTagCategory = new PictureTagCategory();  
    List<String> tagList = Arrays.asList("热门", "搞笑", "生活", "高清", "艺术", "校园", "背景", "简历", "创意");  
    List<String> categoryList = Arrays.asList("模板", "电商", "表情包", "素材", "海报");  
    pictureTagCategory.setTagList(tagList);  
    pictureTagCategory.setCategoryList(categoryList);  
    return ResultUtils.success(pictureTagCategory);  
}
相关推荐
梅梅绵绵冰2 小时前
springboot初步2
java·spring boot·后端
wearegogog1232 小时前
基于MATLAB的D2D仿真场景实现
开发语言·网络·matlab
froginwe112 小时前
Chart.js 散点图详解
开发语言
独自破碎E2 小时前
【纵向扫描】最长公共前缀
java·开发语言
nuo5342022 小时前
C语言实现类似面向对象的三大特性
c语言·开发语言
pp起床2 小时前
【苍穹外卖】Day03 菜品管理
java·数据库·mybatis
321.。2 小时前
深入理解 Linux 线程封装:从 pthread 到 C++ 面向对象实现
linux·开发语言·c++
IT空门:门主2 小时前
Spring AI Alibaba使用教程
java·人工智能·spring
zfoo-framework2 小时前
kotlin
android·开发语言·kotlin