本文作者:程序员飞云
目前笔者正在做一个功能来实现文件上传和下载的功能,为了便于之后的使用,所以编写了这篇博客来记录一下基本的使用。包含两个方面的内容,一是最基础的文件上传下载,二是目前使用比较常见的对象存储来实现文件上传和下载。
开发前置条件
Java
Maven
Spring Boot
基础版本
最简单的就是将文件存放到服务器里面去。
- 首先需要引入web依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>- 定义参数
首先我们需要定义好两个参数
- 文件目录:用于存放对应的文件,否则就是存放在tomcat的临时文件里面,不便于之后的回显以及下载。
- 服务器地址: 当我们上传文件后,我们肯定是希望知道对应的链接,然后下载,这个就定义好了对应的地址
@RequestMapping( "/file" )@RestControllerpublic 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" )@Datapublic 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,提供通用的对象存储操作,这样就不需要每次都要写一些基本信息,供其他代码调用
/** * 通用对象存储类 */@Componentpublic 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@Slf4jpublic 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;}测试:
@Testvoid 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()); }测试
@Testvoid deleteDir() { cosManager.deleteDir("/test/");}完整的代码
1. COSClientConfig
/** * 腾讯云对象存储客户端 */@Configuration@ConfigurationProperties( prefix = "cos.client" )@Datapublic 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 对象存储操作 */@Componentpublic 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()); }}
评论