logo头像
Snippet 博客主题

SpringBoot下载打包图片

现有需求:将若干图片从互联网下载,并打包成指定压缩包,返回给前端

实现功能
前提,思考流程
判断当前文件夹是否存在(防止上一次删除失败),存在则删除
创建文件夹
下载文件
将文件打包
将文件传输给前端
删除文件
第一步,前后端文件交互
采用 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 连接到对应网址,建立稳定连接后,获取连接的输入流,创建本地的输出流,通过输出流写到本地文件并命名
将本地文件传输给前端
具体实现:通过 response 获取 输出流,获取本地文件的输入流,读取到字节数组byte[]当中,通过输出流输出字节数组,即完成了传输
工具类代码:

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
1
串联实现代码:

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;
}

安卓学习教程公众号

微信打赏

祝你事业顺心,富贵吉祥,赞赏鼓励