背景
在项目开发中我们常常需要使用到文件上传和下载的功能,最常见的比如实现用户头像的上传和下载。
最简单的方式就是直接上传到后端的服务器中,通过 Java 自带的文件读写 API 就能实现上传和下载。但这种方式存在不少缺点,比如:
- 单台服务器存储容量有限:如果单台服务器存储满了,要么扩容要么删文件,都是比较麻烦的
- 不够安全:用户可能上传病毒、木马等恶意文件,危机服务器安全;同时要注意控制用户权限
- 不利于迁移:如果后续要更换服务器部署,所有文件都需要进行迁移
- 不利于管理:只能手动进行一些简单的管理,缺乏数据处理、流量控制等高级能力
所以,如果是作为个人项目或者是一些小demo,可以采取将文件直接上传到服务器的做法
但如果要对项目进行部署上线,还是推荐使用更加专业的做法,也就是使用第三方的存储服务,最常用的就是对象存储
一、什么是对象存储
对象存储是一种 存储海量文件 的 分布式 存储服务,具有高扩展、低成本、高可靠等特点
开源的对象存储服务有 MinIO;长夜版的云服务有阿里云的对象存储服务(OSS)、腾讯云的对象存储服务(COS)等等
这里主要介绍的是腾讯云的 COS,能够实现基本的对象存储,并且可以通过控制台、API、SDK等多样化的方式快速接入,实现文件的上传、下载和管理
二、后端开发
1、创建对象存储服务
这里使用的是腾讯云的对象存储服务,可以在 云产品福利专区 购买
购买后按照以下操作步骤,创建对象存储
1)首先进入腾讯云控制台,创建存储桶
直接点击下一步即可
点击创建
创建成功后,就可以在web控制台上传文件
上传的文件可以使用默认的域名进行访问
2、后端接入COS对象存储
这里后续都以SpringBoot项目为例
2.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中忽略该文件的提交,这样就不会将敏感信息提交到仓库了。
# 对象存储配置(需要从腾讯云获取)
cos:
client:
host: xxx
secretId: xxx
secretKey: xxx
region: xxx
bucket: xxx
可以通过以下的方式获取配置信息:
-
host为存储桶域名,可以在COS控制台域名信息部分找到
-
secretId、secretKey可以在腾讯云访问管理获取,如果没有可以新建一个
-
region表示域名,可以在存储桶列表的基本信息中找到
-
bucket表示存储桶名,可以在存储桶详情页获取,也可以直接从访问域名里截取
2.2、COS通用能力类
在manager包下编写CosManager类,提供通用的对象存储操作,比如文件上传、文件下载
该类需要引入对象存储配置、对象存储客户端
import com.hongxiac.habit.config.CosClientConfig;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.PutObjectResult;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.File;
@Component
public class CosManager {
@Resource
private CosClientConfig cosClientConfig;
@Resource
private COSClient cosClient;
// 操作Cos对象的一些方法...
}
2.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);
}
2)为了方便测试,先在FileController中编写测试文件上传的接口
@RestController
@Slf4j
public class FileController {
@Autowired
private CosManager cosManager;
/**
* 测试文件上传
*
* @param multipartFile
* @return
*/
@PostMapping("/test/upload")
public Result<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);
log.info("file upload sucess, filepath = " + filepath);
// 返回可访问地址
return Result.success(filepath);
} catch (Exception e) {
log.error("file upload error, filepath = " + filepath, e);
throw new BussinessException( "上传失败");
} finally {
if (file != null) {
// 删除临时文件
boolean delete = file.delete();
if (!delete) {
log.error("file delete error, filepath = {}", filepath);
}
}
}
}
}
2.4、文件下载
官方文档介绍了两种下载方式:
1、流式下载(适合返回给前端)
2、直接下载到后端服务器(适合后端对文件进一步处理)
当然还有第三种方式:
3、通过文件URL直接下载(适合单一的、可以被用户公共访问的资源,比如用户头像等等)
对于安全性较高的场景,一般的做法是:先通过后端服务器校验用户权限,然后从 COS 下载文件到服务器,再由服务器返回给前端
下面演示的是如何将文件下载到后端服务器
1)在CosManager编写对象下载的方法,根据对象的key获取存储信息
/**
* 下载对象
*
* @param key 唯一键
*/
public COSObject getObject(String key) {
GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key);
return cosClient.getObject(getObjectRequest);
}
2)为了方便测试,先在FileController中编写测试文件下载的接口
核心流程:
1、根据文件路径获取COS文件对象
/**
* 测试文件下载
* @param filepath 文件路径
* @param response 响应
* @throws IOException
*/
@PostMapping("/test/download")
public void testDownloadFile(String filepath, HttpServletResponse response) throws IOException {
// 1、根据文件路径获取COS文件对象
// 2、将COS文件对象转化成文件流
// 3、写入响应,注意设置文件下载专属的响应头
// 4、关闭input流
COSObjectInputStream cosObjectInput = null;
try {
COSObject cosObject = cosManager.getObject(filepath);
cosObjectInput = cosObject.getObjectContent();
// 将COS文件对象转化成文件流
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("fail download error, filepath = " +filepath, e);
throw new BussinessException("下载事变");
}finally{
if(cosObjectInput != null){
cosObjectInput.close();
}
}
}
2.5、测试
启动项目,打开 Swagger 接口文档,测试文件上传和下载的接口
注意:
项目启动需要使用local配置,因为我们前面对象存储配置是放在applicaion-local.yml文件的,如果不使用local配置会读取不到
有两种方式使用local配置启动项目:
1)编译器启动
点击编译器启动的Edit Configurations
在Active profiles添加local参数后,点击Apply生效
2)在主配置文件applicaion.yml中指定激活的环境配置
spring:
profiles:
active: local
3、业务实现
3.1、文件服务类
前面我们已经实现了CosManager,并且在里面实现了文件的上传和下载
但我们并不能直接通过它来上传、下载文件,因为还有以下问题:
- 文件是否符合要求? 需要校验
- 将图片上传到哪里?需要指定路径
所以,需要编写一个更贴近业务的文件上传服务类FileManager(也可以是FileService)
package com.hongxiac.habit.manager;
import com.hongxiac.habit.common.ErrorCode;
import com.hongxiac.habit.config.CosClientConfig;
import com.hongxiac.habit.exception.BussinessException;
import com.hongxiac.habit.utils.FileUtils;
import com.qcloud.cos.COSClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* 文件管理器
*/
@Service
@Slf4j
public class FileManager {
@Resource
private CosManager cosManager;
@Resource
private CosClientConfig cosClientConfig;
// 图片文件最大大小
final static long PICTURE_MAX_SIZE = 5 * 1024 * 1024L;
// 允许上传的图片类型
final static List<String> ALLOW_PICTURE_TYPES = Arrays.asList("jpeg","jpg","png","gif");
/**
* 校验上传图片
* @param file
*/
public void validatePicture(MultipartFile file) {
// 1. 参数校验
if (file == null) {
throw new BussinessException(ErrorCode.PARAMS_ERROR, "文件为空");
}
// 2. 文件大小校验
long fileSize = file.getSize();
if (fileSize > PICTURE_MAX_SIZE) {
throw new BussinessException(ErrorCode.PARAMS_ERROR, "文件大小不能超过 " + (PICTURE_MAX_SIZE / 1024 / 1024) + "MB");
}
// 3. 文件类型校验
String fileType = FileUtils.getFileType(file);
if(!ALLOW_PICTURE_TYPES.contains(fileType)){
throw new BussinessException(ErrorCode.PARAMS_ERROR,"文件类型错误");
}
}
public String uploadPicture(MultipartFile file, String filePrefix) {
// 1. 校验文件
validatePicture(file);
// 2. 生成文件存储路径
String fileType = FileUtils.getFileType(file);
String fileName = UUID.randomUUID().toString() + "." + fileType;
String filePath = filePrefix + "/" + fileName;
File tempFile = null;
// 3. 上传文件到腾讯云
try {
tempFile = FileUtils.multipartFileToFile(file);
cosManager.putObject(filePath, tempFile);
} catch (Exception e) {
log.error("文件上传失败", e);
throw new BussinessException(ErrorCode.SYSTEM_ERROR, "文件上传失败");
}finally {
deleteTempFile(tempFile);
}
// 4. 返回文件访问URL
String fileUrl = cosClientConfig.getHost() + "/" + filePath;
return fileUrl;
}
/**
* 删除临时文件
* @param tempFile
*/
public void deleteTempFile(File tempFile){
if(tempFile == null){
return;
}
boolean deleteResult = tempFile.delete();
if(!deleteResult){
log.error("file delete error, filepath = {}", tempFile.getAbsolutePath());
}
}
}
上面的代码有几个核心:
- 单独封装了文件校验的方法,用于校验文件大小、类型
- 文件上传前,需要在服务器创建临时文件,无论是否上传成功最后都需要删除,避免资源占用
- 可以根据自己的需求定义文件路径、文件名
- 最后返回的是文件的URL,可以直接通过URL进行访问文件
3.2、业务服务类
接下来我们就可以直接在业务服务类中直接调用文件服务类上传文件
这里的代码以实现用户头像的上传、下载为例
/**
* 用户服务实现类
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
private PasswordEncoder passwordEncoder;
@Autowired
private FileManager fileManager;
@Override
public String uploadAvatar(MultipartFile file) {
String uploadPrefix = "avatar/" + getCurrentUserId();
String fileUrl = fileManager.uploadPicture(file, uploadPrefix);
return fileUrl;
}
// ...其他方法
}
package com.hongxiac.habit.controller;
import com.hongxiac.habit.common.ErrorCode;
import com.hongxiac.habit.common.BaseResponse;
import com.hongxiac.habit.dto.LoginRequest;
import com.hongxiac.habit.entity.User;
import com.hongxiac.habit.exception.BussinessException;
import com.hongxiac.habit.manager.CosManager;
import com.hongxiac.habit.service.UserService;
import com.hongxiac.habit.utils.JwtUtil;
import com.hongxiac.habit.manager.FileManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth")
@Slf4j
public class UserController {
@PostMapping("/avatar")
public BaseResponse<String> uploadAvatar(@RequestParam("file") MultipartFile file,
HttpServletRequest request) {
// 1. 获取当前登录用户
User loginUser = userService.getLoginUser(request);
if (loginUser == null) {
throw new BussinessException(ErrorCode.NOT_LOGIN_ERROR);
}
// 2. 上传头像
String avatarUrl = userService.uploadAvatar(file);
return BaseResponse.success(avatarUrl);
}
// ...其他方法
}