对象存储使用
本文作者:程序员飞云
目前笔者正在做一个功能来实现文件上传和下载的功能,为了便于之后的使用,所以编写了这篇博客来记录一下基本的使用。包含两个方面的内容,一是最基础的文件上传下载,二是目前使用比较常见的对象存储来实现文件上传和下载。
开发前置条件
Java
Maven
Spring Boot
基础版本
最简单的就是将文件存放到服务器里面去。
- 首先需要引入web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 定义参数
首先我们需要定义好两个参数
- 文件目录:用于存放对应的文件,否则就是存放在tomcat的临时文件里面,不便于之后的回显以及下载。
- 服务器地址: 当我们上传文件后,我们肯定是希望知道对应的链接,然后下载,这个就定义好了对应的地址
@RequestMapping( "/file" )
@RestController
public class FileController {
/**
* 存放文件的目录
*/
public static final String BASE_DIR = "D:" + File.separator + "picture" + File.separator;
/**
* 服务器地址
*/
public static final String BASE_URL = "http://localhost:8080/file/download?fileName=";
}
- 编写文件上传接口
步骤:
- 获取文件名,便于保存
- 获取存放文件夹的目录位置,不存在就创建
- 上传文件
- 回显文件下载地址
@PostMapping( "/upload" )
public HashMap<String, Object> upload(@RequestParam( value = "file" ) MultipartFile file) {
// 获取文件名
String fileName = file.getOriginalFilename();
// 编写对应的上传文件路径,使用目录拼接文件名
File uploadFilePath = new File(BASE_DIR + fileName);
try {
// 判断目录是否存在,不存在则创建
boolean existPath = uploadFilePath.getParentFile().exists();
if (!existPath) {
uploadFilePath.getParentFile().mkdirs();
}
// 使用MultipartFile的方法完成文件上传,上传至uploadFilePath路径
file.transferTo(uploadFilePath);
} catch (IOException e) {
throw new RuntimeException(e);
}
// 输出对应的服务器地址
String outputUrl = BASE_URL + fileName;
HashMap<String, Object> map = new HashMap<>();
map.put("url", outputUrl);
return map;
}
里面还可以设置MultipartFile
的大小,类型。
开始测试

上传成功,也是返回了文件下载的地址,目前是无用的,因为还没有编写下载的文件地址。现在可以查看下文件的位置是否存在。

- 编写下载接口
需要传递一个fileName也就是我们之前定义好的地址里面的参数
步骤:
- 判断文件路径是否存在
- 设置响应类型
- response里面读取数据
基本上都是一些固定步骤,没什么太好说的。
/**
* 文件下载
*
* @param fileName 文件名
* @param response
*/
@GetMapping( "/download" )
public String download(@RequestParam( value = "fileName" ) String fileName, HttpServletResponse response) {
File file = new File(BASE_DIR + fileName);
if (!file.exists()) {
return "文件不存在";
}
// 重置response
response.reset();
// 设置响应类型
response.setContentType("application/octet-stream");
response.setCharacterEncoding("utf-8");
// 设置响应头,告诉浏览器要下载文件
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
// 读取文件并写入response的输出流
try (BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
byte[] bytes = new byte[1024];
OutputStream outputStream = response.getOutputStream();
int i = 0;
while ((i = bis.read(bytes)) != -1) {
outputStream.write(bytes, 0, i);
outputStream.flush();
}
} catch (IOException e) {
return "下载失败";
}
return "下载成功";
}

- 前端界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件</title>
</head>
<body>
<div style="padding: 100px">
<input type="file" id="inputFile">
<div style="margin: 100px 0">
<button onclick="upload()">上传文件</button>
</div>
<div id="url"></div>
</div>
<script>
function upload() {
// 获取input 选择的的文件
const fileInput = document.getElementById('inputFile')
const fd = new FormData()
fd.append('file', fileInput.files[0])
fetch('http://localhost:8080/file/upload', {
method: 'POST',
body: fd
}).then(res => res.json()).then(res => {
// 获取json里面url对应的结果
document.getElementById("url").innerText = `上传成功,文件url: ${res.url}`
const downloadUrl = `下载链接: <a href="${res.url}" target="_blank">${res.url}</a>`;
document.getElementById("url").innerHTML += downloadUrl;
})
}
</script>
</body>
</html>
开始测试

使用对象存储
上面的方式存在一些缺点
- 不利于扩展:这些数据都是存放在服务器里面,一旦说服务器内存满了,那么只能增加新的存储空间,或者清理之前的文件。
- 不利于迁移:一旦换了服务器,那么就需要将文件全部迁移过来,中间可能会出现文件丢失等情况。
- 不利于管理:现在对于文件只能进行一些初始操作,例如文件大小,上传日期等等,无法进行数据管理,流量控制等等。
- 不安全:如果没有做好安全防御设置,用户可能在通过一些恶意代码来访问服务器里面的资源等等。
并不是说服务器里面不能存放文件,可以存放一些临时文件,对于这些文件可以定期删除,不会影响到相关服务,但是一旦涉及到要持久化保存一些文件,用户需要下载,访问的情况,可以使用对象存储来解决这个问题。
1. 什么是对象存储
可以存储海量文件的分布式存储服务,具有高扩展,低成本,可靠安全等功能。
目前由开源的对象存储服务 MinIO,还有商业版的云服务,例如亚马逊的S3,腾讯云的COS,阿里云的OSS,七牛云的kodo。
如果需要使用对象存储的话,建议使用一些大厂的,有相对的保障,例如流量计费,防盗链等等安全性,稳定性也是可以的,除了基本的对象存储的优点外,还可以通过控制台、API、SDK 和工具等多样化方式,简单快速地接入对象存储,进行多格式文件的上传、下载和管理,实现海量数据存储和管理。像一些MinIO的开源项目,可以自己学习一下,小范围使用,不建议来实际使用。
接下来笔者将会介绍腾讯云对象存储的相关使用,以及完成文件的上传下载功能。
2. 创建并使用
首先需要创建存储桶,填一些基础信息,访问权限有三个,第一个只支持自己使用,第二个第你的用户通过一定的配置,也能使用你的存储桶进行存储,第三个是任何人都可以用你的存储桶来存储(非常不推荐使用),内容安全可以自选。接下来一直配置就行。这里面还可以配置一些防盗链,服务端加密,绑定域名等等,这边可以自行探索。

建议点击右边的排列,便于方便之后查看对应的详细信息

创建完成后可以查看对应文件信息,例如我这边上传了一个图片,通过访问对象地址就能访问了。

3. 后端开发
不管使什么对象存储,首先第一件事情就是看对应的文档,一般而言,官方文档都会有很详细的说明。快速入门
1. 引入依赖
除了基础依赖,建议引入lombok
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.155</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 初始化客户端

@Configuration
@ConfigurationProperties( prefix = "cos.client" )
@Data
public class CosClientConfig {
/**
* 腾讯云账户secretId
*/
private String secretId;
/**
* 腾讯云账户secretKey
*/
private String secretKey;
/**
* COS的区域地址
*/
private String region;
/**
* COS的Bucket名称
*/
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);
}
}
secretId,secretKey获取地址 https://console.cloud.tencent.com/cam/capi,请及时保存对应的信息,这个只会在创建的时候看见对应的信息
填写配置文件
这边建议创建新的配置文件,例如application-local.yml
,在启动项目的时候采用local启动,并且在.gitignore里面将配置文件忽略,这样就能防止无意将密码传入到github或者gitee里面去,导致密钥泄露。

里面的id,key之前已经获取了,bucket就是存储桶的名称,region就是所属地域的英文
配置启动

3. 通用能力类
编写一个通用类CosManager,提供通用的对象存储操作,这样就不需要每次都要写一些基本信息,供其他代码调用
/**
* 通用对象存储类
*/
@Component
public class CosManager {
@Resource
private COSClient cosClient;
@Resource
private CosClientConfig cosClientConfig;
//
}
4. 文件上传

可以看到文件上传需要返回一个PutObjectResult
的对象
有三种方式,但是这三种方式里面核心都是要对应的key,以及相应的文件信息File和bucketName,但是其中的bucketName我们已经在cosClientConfig里面定义好了,所以我们可以直接调用就能得到,所以只需要输入对应的key以及相应的文件就可以了。
/**
* 文件上传
*
* @param key 唯一键
* @param filePath 文件路径
* @return
*/
public PutObjectResult putObjectRequest(String key, String filePath) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, new File(filePath));
return cosClient.putObject(putObjectRequest);
}
/**
* 文件上传
*
* @param key 唯一键
* @param file 文件
* @return
*/
public PutObjectResult putObjectRequest(String key, File file) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key, file);
return cosClient.putObject(putObjectRequest);
}
1.新建常量,存储域名
/**
* 文件常量
*/
public interface FileConstant {
/**
* COS 访问地址
* todo 需替换配置
*/
String COS_HOST = "xxx";
}
这个域名可以在概览里面查看

2. 编写上传接口
步骤
- 获取文件原始信息,例如文件名
- 创建临时文件,调用对象存储
- 完成上传,删除临时文件
@RequestMapping( "/file" )
@RestController
@Slf4j
public class FileController {
@Resource
private CosManager cosManager;
@PostMapping( "/test/upload" )
public String upload(@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.putObjectRequest(filePath, file);
// 返回地址
return filePath;
} catch (IOException e) {
log.error("上传文件失败,filePath=" + filePath, e);
throw new RuntimeException(e);
} finally {
if (file != null) {
// 删除临时文件
boolean delete = file.delete();
if (!delete) {
log.error("删除临时文件失败,filePath=" + filePath);
}
}
}
}
}
3. 测试上传

这里可以在对应的存储桶里面看到对应的文件。
5. 文件下载

里面有两种方式,第一种方式比较高级,我们可以使用第二种,相对而言比较简单,直接返回给前端使用就可以。可以看到里面有个CosObject
的类,我们可以在之前的Manager里面编写对应的方法,只需要传入对应的key,以及bucket
/**
* 文件下载
* @param filepath 唯一键,文件路径
* @return
*/
public COSObject getCosObject(String filepath) {
GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), filepath);
return cosClient.getObject(getObjectRequest);
}
编写controller
@GetMapping( "/test/download" )
public void download(String filepath, HttpServletResponse response) throws IOException {
COSObjectInputStream cosObjectInput = null;
try {
COSObject cosObject = cosManager.getCosObject(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 RuntimeException("下载失败");
} finally {
if (cosObjectInput != null) {
cosObjectInput.close();
}
}
}
6. 单个文件删除

/**
* 删除单个文件
*
* @param key 文件的key
* @throws CosClientException
* @throws CosServiceException
*/
public void deleteObject(String key) throws CosClientException, CosServiceException {
String bucketName = cosClientConfig.getBucket();
cosClient.deleteObject(bucketName, key);
}
测试
@Test
void deleteObject() {
cosManager.deleteObject("/test/gly1.jpg");
}
进行测试的时候需修改测试运行配置,否则无法识别对应的local配置

7. 删除多个文件
一定要注意文件名不能以/
开头
/**
* 删除多个文件
* @param keyList
* @return
* @throws MultiObjectDeleteException
* @throws CosClientException
* @throws CosServiceException
*/
public DeleteObjectsResult deleteObjects(List<String> keyList)
throws MultiObjectDeleteException, CosClientException, CosServiceException {
DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(cosClientConfig.getBucket());
// 设置要删除的key列表, 最多一次删除1000个
ArrayList<DeleteObjectsRequest.KeyVersion> keyVersions = new ArrayList<>();
// 传入要删除的文件名
// 注意文件名不允许以正斜线/或者反斜线\开头,例如:
// 存储桶目录下有a/b/c.txt文件,如果要删除,只能是 keyList.add(new KeyVersion("a/b/c.txt")), 若使用 keyList.add(new KeyVersion("/a/b/c.txt"))会导致删除不成功
for (String key : keyList) {
keyVersions.add(new DeleteObjectsRequest.KeyVersion(key));
}
deleteObjectsRequest.setKeys(keyVersions);
DeleteObjectsResult deleteObjectsResult = cosClient.deleteObjects(deleteObjectsRequest);
return deleteObjectsResult;
}
测试:
@Test
void deleteObjects() {
cosManager.deleteObjects(Arrays.asList("test/logo.jpg", "test/logo.png"));
}
8. 删除目录
一定需要注意删除目录的时候一定要加上后缀/
,例如/a/
这样的形式,因为如果是/a
可能会将其他包含/a
的数据删除
/**
* 删除目录
*
* @param delPrefix 包含后缀/
* @throws CosClientException
* @throws CosServiceException
*/
public void deleteDir(String delPrefix) throws CosClientException, CosServiceException {
ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
// 设置 bucket 名称
listObjectsRequest.setBucketName(cosClientConfig.getBucket());
// prefix 表示列出的对象名以 prefix 为前缀
// 这里填要列出的目录的相对 bucket 的路径
listObjectsRequest.setPrefix(delPrefix);
// 设置最大遍历出多少个对象, 一次 listobject 最大支持1000
listObjectsRequest.setMaxKeys(1000);
// 保存每次列出的结果
ObjectListing objectListing = null;
do {
objectListing = cosClient.listObjects(listObjectsRequest);
// 这里保存列出的对象列表
List<COSObjectSummary> cosObjectSummaries = objectListing.getObjectSummaries();
if (CollUtil.isEmpty(cosObjectSummaries)) {
break;
}
ArrayList<DeleteObjectsRequest.KeyVersion> delObjects = new ArrayList<>();
for (COSObjectSummary cosObjectSummary : cosObjectSummaries) {
delObjects.add(new DeleteObjectsRequest.KeyVersion(cosObjectSummary.getKey()));
}
DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(cosClientConfig.getBucket());
deleteObjectsRequest.setKeys(delObjects);
cosClient.deleteObjects(deleteObjectsRequest);
// 标记下一次开始的位置
String nextMarker = objectListing.getNextMarker();
listObjectsRequest.setMarker(nextMarker);
} while (objectListing.isTruncated());
}
测试
@Test
void deleteDir() {
cosManager.deleteDir("/test/");
}
完整的代码
1. COSClientConfig
/**
* 腾讯云对象存储客户端
*/
@Configuration
@ConfigurationProperties( prefix = "cos.client" )
@Data
public class CosClientConfig {
/**
* accessKey
*/
private String accessKey;
/**
* secretKey
*/
private String secretKey;
/**
* 区域
*/
private String region;
/**
* 桶名
*/
private String bucket;
@Bean
public COSClient cosClient() {
// 初始化用户身份信息(secretId, secretKey)
COSCredentials cred = new BasicCOSCredentials(accessKey, secretKey);
// 设置bucket的区域, COS地域的简称请参照 https://www.qcloud.com/document/product/436/6224
ClientConfig clientConfig = new ClientConfig(new Region(region));
// 生成cos客户端
return new COSClient(cred, clientConfig);
}
}
2.对象存储操作 CosManager
/**
* Cos 对象存储操作
*/
@Component
public class CosManager {
@Resource
private CosClientConfig cosClientConfig;
@Resource
private COSClient cosClient;
private TransferManager transferManager;
/**
* 让transferManager在CosManager初始化完成的时候创建
*/
@PostConstruct
public void init() {
// 自定义线程池大小,建议在客户端与 COS 网络充足(例如使用腾讯云的 CVM,同地域上传 COS)的情况下,设置成16或32即可,可较充分的利用网络资源
// 对于使用公网传输且网络带宽质量不高的情况,建议减小该值,避免因网速过慢,造成请求超时。
ExecutorService threadPool = Executors.newFixedThreadPool(32);
// 传入一个 threadpool, 若不传入线程池,默认 TransferManager 中会生成一个单线程的线程池。
transferManager = new TransferManager(cosClient, threadPool);
}
/**
* 上传对象
*
* @param key 唯一键
* @param localFilePath 本地文件路径
* @return
*/
public PutObjectResult putObject(String key, String localFilePath) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
new File(localFilePath));
return cosClient.putObject(putObjectRequest);
}
/**
* 上传对象
*
* @param key 唯一键
* @param file 文件
* @return
*/
public PutObjectResult putObject(String key, File file) {
PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,
file);
return cosClient.putObject(putObjectRequest);
}
/**
* 文件下载
*
* @param filepath 唯一键,文件路径
* @return
*/
public COSObject getCosObject(String filepath) {
GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), filepath);
return cosClient.getObject(getObjectRequest);
}
/**
* 将对象写入到指定的文件
*
* @param key
* @param localFilePath
* @return
*/
public Download download(String key, String localFilePath) throws InterruptedException {
GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key);
// 返回一个异步结果 Download, 可同步的调用 waitForCompletion 等待下载结束, 成功返回 void, 失败抛出异常
Download download = transferManager.download(getObjectRequest, new File(localFilePath));
download.waitForCompletion();
return download;
}
/**
* 删除单个文件
*
* @param key 文件的key
* @throws CosClientException
* @throws CosServiceException
*/
public void deleteObject(String key) throws CosClientException, CosServiceException {
String bucketName = cosClientConfig.getBucket();
cosClient.deleteObject(bucketName, key);
}
/**
* 删除多个文件
* @param keyList
* @return
* @throws MultiObjectDeleteException
* @throws CosClientException
* @throws CosServiceException
*/
public DeleteObjectsResult deleteObjects(List<String> keyList)
throws MultiObjectDeleteException, CosClientException, CosServiceException {
DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(cosClientConfig.getBucket());
// 设置要删除的key列表, 最多一次删除1000个
ArrayList<DeleteObjectsRequest.KeyVersion> keyVersions = new ArrayList<>();
// 传入要删除的文件名
// 注意文件名不允许以正斜线/或者反斜线\开头,例如:
// 存储桶目录下有a/b/c.txt文件,如果要删除,只能是 keyList.add(new KeyVersion("a/b/c.txt")), 若使用 keyList.add(new KeyVersion("/a/b/c.txt"))会导致删除不成功
for (String key : keyList) {
keyVersions.add(new DeleteObjectsRequest.KeyVersion(key));
}
deleteObjectsRequest.setKeys(keyVersions);
DeleteObjectsResult deleteObjectsResult = cosClient.deleteObjects(deleteObjectsRequest);
return deleteObjectsResult;
}
/**
* 删除目录
*
* @param delPrefix 包含后缀/
* @throws CosClientException
* @throws CosServiceException
*/
public void deleteDir(String delPrefix) throws CosClientException, CosServiceException {
ListObjectsRequest listObjectsRequest = new ListObjectsRequest();
// 设置 bucket 名称
listObjectsRequest.setBucketName(cosClientConfig.getBucket());
// prefix 表示列出的对象名以 prefix 为前缀
// 这里填要列出的目录的相对 bucket 的路径
listObjectsRequest.setPrefix(delPrefix);
// 设置最大遍历出多少个对象, 一次 listobject 最大支持1000
listObjectsRequest.setMaxKeys(1000);
// 保存每次列出的结果
ObjectListing objectListing = null;
do {
objectListing = cosClient.listObjects(listObjectsRequest);
// 这里保存列出的对象列表
List<COSObjectSummary> cosObjectSummaries = objectListing.getObjectSummaries();
if (CollUtil.isEmpty(cosObjectSummaries)) {
break;
}
ArrayList<DeleteObjectsRequest.KeyVersion> delObjects = new ArrayList<>();
for (COSObjectSummary cosObjectSummary : cosObjectSummaries) {
delObjects.add(new DeleteObjectsRequest.KeyVersion(cosObjectSummary.getKey()));
}
DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(cosClientConfig.getBucket());
deleteObjectsRequest.setKeys(delObjects);
cosClient.deleteObjects(deleteObjectsRequest);
// 标记下一次开始的位置
String nextMarker = objectListing.getNextMarker();
listObjectsRequest.setMarker(nextMarker);
} while (objectListing.isTruncated());
}
}
贡献者
flycodeu
版权所有
版权归属:flycodeu