图片上传
1、使用腾讯云对象存储(COS)
1)引入 COS 依赖:
<!-- 腾讯云 cos 服务 -->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.227</version>
</dependency>
2)在项目的 config
包下新建 CosClientConfig
类。负责读取配置文件,并创建一个 COS 客户端的 Bean。代码如下:
@Configuration
@ConfigurationProperties(prefix = "cos.client")
@Data
public class CosClientConfig {
/**
* 域名
*/
private String host;
/**
* secretId
*/
private String secretId;
/**
* 密钥(注意不要泄露)
*/
private String secretKey;
/**
* 区域
*/
private String region;
/**
* 桶名
*/
private String bucket;
@Bean
public COSClient cosClient() {
// 初始化用户身份信息(secretId, secretKey)
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
// 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
ClientConfig clientConfig = new ClientConfig(new Region(region));
// 生成cos客户端
return new COSClient(cred, clientConfig);
}
3)填写配置文件。
一定要注意防止密码泄露! 所以我们新建 application-local.yml
文件,并且在 .gitignore
中忽略该文件的提交,这样就不会将代码等敏感配置提交到代码仓库了。

application-local.yml
配置代码如下:
# 对象存储配置(需要从腾讯云获取)
cos:
client:
host: xxx
secretId: xxx
secretKey: xxx
region: xxx
bucket: xxx
可以通过如下方式分别获取需要的配置。
host 为存储桶域名,可以在 COS 控制台的域名信息部分找到:
secretId、secretKey 密钥对:在腾讯云访问管理 => 密钥管理中获取。
region 表示地域名,可以 点此获取。3n/Enp4sfaVRq/PFMJKPxPdBTxeNas/Bp33wsl/l9Wg=
bucket 是存储桶名,可以点进存储桶详情页获取
2、通用能力类
在 manager
包下新建 CosManager
类,提供通用的对象存储操作,比如文件上传、文件下载等。
💡 Manager 也是人为约定的一种写法,表示通用的、可复用的能力,可供其他代码(比如 Service)调用。
该类需要引入对象存储配置和 COS 客户端,用于和 COS 进行交互。代码如下:
@Component
public class CosManager {
@Resource
private CosClientConfig cosClientConfig;
@Resource
private COSClient cosClient;
// ... 一些操作 COS 的方法
}
3、文件上传下载
参考 官方文档 的“上传对象”部分,可以编写出文件上传的代码。
1)CosManager
新增上传、下载对象的方法,代码如下:
/**
* 上传对象
*
* @param key 唯一键
* @param file 文件
*/
public PutObjectResult putObject(String key, File file) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
file);
return cosClient.putObject(putObjectRequest);
}
/**
* 下载对象
*
* @param key 唯一键
*/
public COSObject getObject(String key) {
GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key);
return cosClient.getObject(getObjectRequest);
}
2)为了方便测试,在 FileController
中编写测试文件上传接口。
核心流程是先接受用户上传的文件,指定上传的路径,然后调用 cosManager.putObject
方法上传文件到 COS 对象存储;上传成功后,会返回一个文件的 key(其实就是文件路径),便于我们访问和下载文件。
需要注意,测试接口一定要加上管理员权限!防止任何用户随意上传文件。
测试文件上传接口代码如下:
/**
* 测试文件上传
*
* @param multipartFile
* @return
*/
@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);
}
}
}
}
/**
* 测试文件下载
*
* @param filepath 文件路径
* @param response 响应对象
*/
@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();
}
}
}
4、图片上传
1、数据模型
在 model.dto.picture
下新建用于接受请求参数的类。由于图片需要支持重复上传(基础信息不变,只改变图片文件),所以要添加图片 id 参数:
@Data
public class PictureUploadRequest implements Serializable {
/**
* 图片 id(用于修改)
*/
private Long id;
private static final long serialVersionUID = 1L;
}
在 model.vo
下新建上传成功后返回给前端的响应类,这是一个视图包装类,可以额外关联上传图片的用户信息。还可以编写 Picture 实体类和该 VO 类的转换方法,便于后续快速传值。
@Data
public class PictureVO implements Serializable {
/**
* id
*/
private Long id;
/**
* 图片 url
*/
private String url;
/**
* 图片名称
*/
private String name;
/**
* 简介
*/
private String introduction;
/**
* 标签
*/
private List<String> tags;
/**
* 分类
*/
private String category;
/**
* 文件体积
*/
private Long picSize;
/**
* 图片宽度
*/
private Integer picWidth;
/**
* 图片高度
*/
private Integer picHeight;
/**
* 图片比例
*/
private Double picScale;
/**
* 图片格式
*/
private String picFormat;
/**
* 用户 id
*/
private Long userId;
/**
* 创建时间
*/
private Date createTime;
/**
* 编辑时间
*/
private Date editTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 创建用户信息
*/
private UserVO user;
private static final long serialVersionUID = 1L;
/**
* 封装类转对象
*/
public static Picture voToObj(PictureVO pictureVO) {
if (pictureVO == null) {
return null;
}
Picture picture = new Picture();
BeanUtils.copyProperties(pictureVO, picture);
// 类型不同,需要转换(json转str)
picture.setTags(JSONUtil.toJsonStr(pictureVO.getTags()));
return picture;
}
/**
* 对象转封装类
*/
public static PictureVO objToVo(Picture picture) {
if (picture == null) {
return null;
}
PictureVO pictureVO = new PictureVO();
BeanUtils.copyProperties(picture, pictureVO);
// 类型不同,需要转换(str转json)
pictureVO.setTags(JSONUtil.toList(picture.getTags(), String.class));
return pictureVO;
}
}
2、通用文件上传服务
之前虽然我们已经编写了通用的对象存储操作类 CosManager,但这个类并不能直接满足我们的图片上传需求。
比如:
- 图片是否符合要求?需要校验
- 将图片上传到哪里?需要指定路径
- 如何解析图片?需要使用数据万象服务
1)在 model.dto.file
中新增用于接受图片解析信息的包装类:
▼java复制代码@Data
public class UploadPictureResult {
/**
* 图片地址
*/
private String url;
/**
* 图片名称
*/
private String picName;
/**
* 文件体积
*/
private Long picSize;
/**
* 图片宽度
*/
private int picWidth;
/**
* 图片高度
*/
private int picHeight;
/**
* 图片宽高比
*/
private Double picScale;
/**
* 图片格式
*/
private String picFormat;
}
2)参考 数据万象 的文档,在 CosManager 中添加上传图片并解析图片的方法:
如果你之前没有使用过数据万象,需要先 开通数据万象并授权,否则会报错:

3)在 FileManager 中编写上传图片的方法:
/**
* 上传图片
*
* @param multipartFile 文件
* @param uploadPathPrefix 上传路径前缀
* @return
*/
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);
}
}
/**
* 校验文件
*
* @param multipartFile multipart 文件
*/
public void validPicture(MultipartFile multipartFile) {
ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空");
// 1. 校验文件大小
long fileSize = multipartFile.getSize();
final long ONE_M = 1024 * 1024L;
ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M");
// 2. 校验文件后缀
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());
}
}
上述代码中有几个实现关键:
- 由于文件校验规则较复杂,单独抽象为 validPicture 方法,对文件大小、类型进行校验。
- 文件上传时,会先在本地创建临时文件,无论上传是否成功,都要记得删除临时文件,否则会导致资源泄露。
- 可以根据自己的需求定义文件上传地址,比如此处鱼皮给文件名前增加了上传日期和 16 位 uuid 随机数,便于了解文件上传时间并防止文件重复。还预留了一个 uploadPathPrefix 参数,由调用方指定上传文件到哪个目录。
💡 如果多个项目共享存储桶,可以给上传文件路径再加一个 ProjectName 前缀。不过建议还是每个项目独立分配资源。
3、服务开发
在 PictureService 中编写上传图片的方法:
接口:
/**
* 上传图片
*
* @param multipartFile
* @param pictureUploadRequest
* @param loginUser
* @return
*/
PictureVO uploadPicture(MultipartFile multipartFile,
PictureUploadRequest pictureUploadRequest,
User loginUser);
实现类:
@Override
public PictureVO uploadPicture(MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, User loginUser) {
ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);
// 用于判断是新增还是更新图片
Long pictureId = null;
if (pictureUploadRequest != null) {
pictureId = pictureUploadRequest.getId();
}
// 如果是更新图片,需要校验图片是否存在
if (pictureId != null) {
boolean exists = this.lambdaQuery()
.eq(Picture::getId, pictureId)
.exists();
ThrowUtils.throwIf(!exists, ErrorCode.NOT_FOUND_ERROR, "图片不存在");
}
// 上传图片,得到信息
// 按照用户 id 划分目录
String uploadPathPrefix = String.format("public/%s", loginUser.getId());
UploadPictureResult uploadPictureResult = fileManager.uploadPicture(multipartFile, uploadPathPrefix);
// 构造要入库的图片信息
Picture picture = new Picture();
picture.setUrl(uploadPictureResult.getUrl());
picture.setName(uploadPictureResult.getPicName());
picture.setPicSize(uploadPictureResult.getPicSize());
picture.setPicWidth(uploadPictureResult.getPicWidth());
picture.setPicHeight(uploadPictureResult.getPicHeight());
picture.setPicScale(uploadPictureResult.getPicScale());
picture.setPicFormat(uploadPictureResult.getPicFormat());
picture.setUserId(loginUser.getId());
// 如果 pictureId 不为空,表示更新,否则是新增
if (pictureId != null) {
// 如果是更新,需要补充 id 和编辑时间
picture.setId(pictureId);
picture.setEditTime(new Date());
}
boolean result = this.saveOrUpdate(picture);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "图片上传失败");
return PictureVO.objToVo(picture);
}
上述代码中,注意:
- 我们将所有图片都放到了 public 目录下,并且每个用户的图片存储到对应用户 id 的目录下,便于管理。
- 如果 pictureId 不为空,表示更新已有图片的信息,需要判断对应 id 的图片是否存在,并且更新时要指定 editTime 编辑时间。可以调用 MyBatis Plus 提供的 saveOrUpdate 方法兼容创建和更新操作。
4、接口开发
在 PictureController 中编写上传图片接口,注意仅管理员可用:
/**
* 上传图片(可重新上传)
*/
@PostMapping("/upload")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<PictureVO> uploadPicture(
@RequestPart("file") MultipartFile multipartFile,
PictureUploadRequest pictureUploadRequest,
HttpServletRequest request) {
User loginUser = userService.getLoginUser(request);
PictureVO pictureVO = pictureService.uploadPicture(multipartFile, pictureUploadRequest, loginUser);
return ResultUtils.success(pictureVO);
}
5、测试
使用 Swagger 进行测试,发现当上传图片过大时,会触发一段报错。但这个报错不是我们自定义的异常导致的,而是由于 Tomcat 服务器默认限制了请求中文件上传的大小。
需要在 application.yml
中更改配置,调大允许上传的文件大小:
spring:
# 开放更大的文件上传体积
servlet:
multipart:
max-file-size: 10MB
扩展思路
1)可以用枚举类(FileUploadBizEnum)支持根据业务场景区分文件上传路径、校验规则等,从而复用 FileManager。
2)目前在文件上传时,会先在本地创建临时文件。如果你不需要对文件进行额外的处理、想进一步提高性能,可以直接用流的方式将请求中的文件上传到 COS。以下代码仅供参考:
// 上传文件
public static String uploadToCOS(MultipartFile multipartFile, String bucketName, String key) throws Exception {
// 创建 COS 客户端
COSClient cosClient = createCOSClient();
try (InputStream inputStream = multipartFile.getInputStream()) {
// 元信息配置
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(multipartFile.getSize());
metadata.setContentType(multipartFile.getContentType());
// 创建上传请求
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream, metadata);
// 上传文件
cosClient.putObject(putObjectRequest);
// 生成访问链接
return "https://" + bucketName + ".cos." + cosClient.getClientConfig().getRegion().getRegionName()
+ ".myqcloud.com/" + key;
} finally {
cosClient.shutdown();
}
}
3)补充更严格的校验,比如为支持的图片格式定义枚举,仅允许上传枚举定义的格式。
图片管理
图片管理功能具体可以拆分为:
- 【管理员】根据 id 删除图片
- 【管理员】更新图片
- 【管理员】分页获取图片列表(不需要脱敏和限制条数)
- 【管理员】根据 id 获取图片(不需要脱敏)
- 分页获取图片列表(需要脱敏和限制条数)
- 根据 id 获取图片(需要脱敏)
- 修改图片
1、数据模型
每个操作都需要提供一个请求类,都放在 model.dto.picture
包下。