目的描述
现有需求:将若干图片从互联网下载,并打包成指定压缩包,返回给前端
实现功能
前提,思考流程
- 判断当前文件夹是否存在(防止上一次删除失败),存在则删除
- 创建文件夹
- 下载文件
- 将文件打包
- 将文件传输给前端
- 删除文件
第一步,前后端文件交互
采用
HttpServletResponse
通过流来传输文件,具体接口定义为下面代码:
@ApiOperation(value = "导出图片", httpMethod = "POST")
@RequestMapping(value = "/downloadImage", method = RequestMethod.POST, produces = "application/json")
public void downloadImage(
@ApiParam(name = "RequestVo", value = "查询实体")
@Validated @RequestBody RequestVo requestVo, HttpServletResponse response){
log.info("检验数据表批量导出图片入参:" + requestVo);
service.batchDownloadImage(requestVo, response);
}
接口不需要定义返回值,当通过流传输的时候,就不会通过这边的返回来返回内容
确定了如何去和前端进行交互后,就需要书写与互联网交互的工具类
HttpUtil
第二步,网络交互 HttpUtil
这里简单介绍一下要做的事情
-
通过 URL 下载文件到本地
-
通过 URL 连接到对应网址,建立稳定连接后,获取连接的
输入流
,创建本地的
输出流
,通过
输出流
写到本地文件并命名
-
通过 URL 连接到对应网址,建立稳定连接后,获取连接的
-
将本地文件传输给前端
-
具体实现:通过 response 获取
输出流
,获取本地文件的
输入流
,读取到字节数组
byte[]
当中,通过
输出流
输出字节数组,即完成了传输
-
具体实现:通过 response 获取
工具类代码
:
package com.hwh.communitymanage.utils;
import lombok.extern.slf4j.Slf4j;
import javax.activation.MimetypesFileTypeMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
/**
* @description: 网络文件下载工具类
* @author: HWH
* @create: 2022-10-14 09:28
**/
@Slf4j
public class HttpUtil {
/**
* Http 下载文件到本地
* @param urlStr 下载文件地址
* @param path 存储全路径(包括名称)
*/
public static void downloadHttpFileToLocal(String urlStr, String path){
try{
URL url = new URL(urlStr);
URLConnection conn = url.openConnection();
InputStream inputStream = conn.getInputStream();
// 创建输出流
FileOutputStream outputStream = new FileOutputStream(path);
int byteRead;
byte[] buffer = new byte[1024];
while((byteRead = inputStream.read(buffer)) != -1){
outputStream.write(buffer, 0, byteRead);
}
// 关闭流
inputStream.close();
outputStream.close();;
}catch (Exception e){
throw new RuntimeException("下载失败", e);
}
}
/**
* 传输文件流
* @param path 传输文件路径
* @param fileName 传输文件名称
* @param response http响应
* @return 是否成功传输
*/
public static Boolean transferFileStream(String path, String fileName, HttpServletResponse response){
if(path == null){
log.error("文件路径不能为空");
return false;
}
File file = new File(path + File.separator + fileName);
if(!file.exists()){
log.error("文件不存在");
return false;
}
long startTime = System.currentTimeMillis();
FileInputStream inputStream = null;
BufferedInputStream bis = null;
try{
// 设置头
setResponse(fileName, response);
// 开启输入流
inputStream = new FileInputStream(file);
bis = new BufferedInputStream(inputStream);
// 获取输出流
OutputStream os = response.getOutputStream();
// 读取文件
byte[] buffer = new byte[1024];
int i;
// 传输
while((i = bis.read(buffer)) != -1){
os.write(buffer, 0, i);
}
// 计时
long endTime = System.currentTimeMillis();
log.info("文件传输时间: {} ms", endTime - startTime);
}catch (Exception e){
throw new RuntimeException("文件传输失败");
}finally {
try{
if(inputStream != null){
inputStream.close();
}
if(bis != null){
bis.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
return true;
}
private static void setResponse(String fileName, HttpServletResponse response) throws UnsupportedEncodingException {
response.addHeader("Content-Disposition",
"attachment;fileName=" + URLEncoder.encode(fileName, "UTF-8"));// 设置文件名
}
}
第三步,文件交互 FileUtil
考虑:
对于下载的图片,需要根据不同的需求放到不同的文件夹当中,可以根据日期,也可以根据类型。所以需要一个文件夹创建的功能
- 并且有可能并发的情况下,由于文件的内容是在使用完后就要删除的,所以每个用户需要有专属的文件夹;还有单用户可能会发生并发的情况,可以考虑加上时间戳
因此考虑到要实现的功能为
- 创建文件夹,包括父路径全部
- 删除文件夹包括文件夹下所有的内容
自我思考:当下载图片的并发可能会很大的时候,也就会涉及到服务器的IO频率高,可能会导致服务器处理不过来的情况
因此可以采用缓存的方式,文件不再是即刻删除,而是采用延时删除,例如消息队列中的延时队列,定时删除线程等等方式实现
但是也有弊端,存储空间可能会消耗大,并且要确定文件/文件夹的
命中几率
较高,才有实用的价值,不然就是白白增加了服务器的负担。
实现:
回到正题,如何
实现
现有的功能
-
创建文件夹,采用
Files.createDirectories(Path path)
方法,就可以将本目录以及父目录全部创建完成 - 删除文件夹,采用递归的方式,当是文件夹的时候,向下递归,知道遇到文件再删除,然后删除空文件夹
下面贴出我的代码实现:
package com.hwh.communitymanage.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
/**
* @Author: HwH
* @Description:
* @Date: created in 15:38 2022/9/27
*/
@Slf4j
public class FileUtil {
/**
* 强制创建文件夹,连同父级同时创建,同时如果本层文件夹已存在,删除
* @param pathStr 文件路径
*/
public static boolean makeDirAndParent(String pathStr){
// 如果存在,删除
File file = new File(pathStr);
if(file.exists()){
deletedPath(pathStr);
}
try {
// 创建父级及本级文件夹
Path path = Paths.get(pathStr);
Files.createDirectories(path);
}catch (Exception e){
throw new RuntimeException("创建文件夹失败");
}
return true;
}
/**
* 删除文件夹及文件夹下的文件
* @param sourcePath 文件夹路径
* @return 如果删除成功true否则false
*/
public static boolean deletedPath(String sourcePath){
File sourceFile = new File(sourcePath);
if(!sourceFile.exists()){
log.error("文件不存在, 删除失败");
return false;
}
// 获取文件下子目录
File[] fileList = sourceFile.listFiles();
if(fileList != null) {
// 遍历
for (File file : fileList) {
// 判断是否为子目录
if (file.isDirectory()) {
deletedPath(file.getPath());
} else {
file.delete();
}
}
}
sourceFile.delete();
return true;
}
}
第四步,压缩文件夹 ZipUtil
这里为什么说是压缩文件夹而不是压缩文件,因为在我看来所有需要的文件和文件都在一个文件夹下面,可以是根目录,也可以是自定目录,比如:/image/123456,此时123456下全是图片,不管是文件夹还是文件,所以说这里只需要压缩一个自定的文件夹就足够了
具体的实现:
-
通过文件夹路径创建
File
- 判断是否是文件夹(这里也可以做成非文件夹格式,写成通用的方式,也就不仅限于传文件夹了)
-
获取文件夹下的内容
fileList
(因为当前文件夹是不需要打包的,所以会放到上层遍历) - 构建 ZipOutputStream 作为递归参数
-
遍历
fileList
,递归处理- 判断是文件,构建ZipEntry
- 判断是文件夹,遍历递归
- 递归完成,关闭流
代码实现:
package com.hwh.communitymanage.utils;
import io.jsonwebtoken.lang.Collections;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import java.io.*;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @description: 压缩工具类
* @author: HWH
* @create: 2022-10-14 10:21
**/
@Slf4j
public class ZipUtil {
/**
* 文件夹内容批量压缩
* @param sourcePath 文件夹路径列表(压缩后不包括该文件夹)
* @param targetPath 存储路径路径
* @param zipName 压缩文件名称
* @return zip的绝对路径
*/
public static boolean fileToZip(String sourcePath, String targetPath, String zipName){
if(sourcePath == null || targetPath == null){
log.error("文件夹路径/存放/文件名 不能为空, filePathList:{}, targetPath:{}", sourcePath, targetPath);
return false;
}
// 开始时间
long startTime = System.currentTimeMillis();
FileOutputStream fos = null;
ZipOutputStream zos = null;
try{
// 读取文件夹
File sourceFile = new File(sourcePath);
if(!sourceFile.exists() || !sourceFile.isDirectory()){
log.error("文件夹不存在, sourceFile: {}", sourcePath);
return false;
}
// 获取文件夹下内容
File[] fileList = sourceFile.listFiles();
if(fileList == null || fileList.length == 0 ){
log.error("文件夹不能为空, sourceFile: {}", sourceFile);
return false;
}
// 开启输出流
fos = new FileOutputStream(targetPath + File.separator + zipName);
zos = new ZipOutputStream(fos);
// 遍历文件夹下所有
for(File file : fileList){
compress(file, zos, file.getName());
}
// 结束时间
long end = System.currentTimeMillis();
log.info("文件压缩耗时:{} ms", end - startTime);
}catch (Exception e){
throw new RuntimeException("文件压缩失败");
}finally {
// 关闭流
try{
if(zos != null){
zos.close();
}
if(fos != null){
fos.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
return true;
}
/**
* 递归文件压缩
* @param sourceFile 来源文件/文件夹
* @param zos 文件流
* @param name 名称
*/
private static void compress(File sourceFile, ZipOutputStream zos, String name) throws IOException {
byte[] buffer = new byte[1024 * 1024];
// 文件
if(sourceFile.isFile()){
// 添加进压缩包
zos.putNextEntry(new ZipEntry(name));
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(sourceFile));
int read;
while((read = bis.read(buffer, 0, 1024)) != -1){
zos.write(buffer, 0, read);
}
zos.closeEntry();
bis.close();
}
// 文件夹
else{
// 获取文件夹下的文件
File[] fileList = sourceFile.listFiles();
// 空文件夹处理
if(fileList == null || fileList.length == 0){
zos.putNextEntry(new ZipEntry(name + "/"));
zos.closeEntry();
}
// 非空,遍历递归向下处理
else{
for(File file : fileList){
compress(file, zos, name + "/" + file.getName());
}
}
}
}
}
第五步,串联使用
下面代码有用到属性注入,需要配置到 application.yml 中
imagePath: image
串联实现代码:
private final String ZIP_SUFFIX = ".zip";
@Value("ImagePath")
private String IMAGE_BASE_PATH;
private boolean downloadImage(Request requestVo,List<ImagePo> urlList, HttpServletResponse response, boolean batch){
if(urlList.isEmpty()){
throw new BusinessException("不存在相应信息");
}
// 根目录 (基础路径/学号+时间戳)
String basePath = IMAGE_BASE_PATH + File.separator + AppContext.getContext().getUserInfo().getStudentNum() + System.currentTimeMillis();
// 创建根目录文件夹
FileUtil.makeDirAndParent(basePath);
// 压缩包名称
zipName = requestVo.getStartTime() + "-" + requestVo.getEndTime() + ZIP_SUFFIX;
// 下载图片
/....这里一般会复杂些,移除掉了.../
HttpUtil.downloadHttpFileToLocal(url, imagePath);
// 文件压缩
ZipUtil.fileToZip(basePath, zipName);
// 文件传输
HttpUtil.transferFileStream(basePath, zipName, response);
// 删除文件
FileUtil.deletedPath(basePath);
return true;
}