SpringBoot 下载打包图片

  • Post author:
  • Post category:其他




目的描述

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



实现功能



前提,思考流程

  1. 判断当前文件夹是否存在(防止上一次删除失败),存在则删除
  2. 创建文件夹
  3. 下载文件
  4. 将文件打包
  5. 将文件传输给前端
  6. 删除文件



第一步,前后端文件交互

采用

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下全是图片,不管是文件夹还是文件,所以说这里只需要压缩一个自定的文件夹就足够了

具体的实现:

  1. 通过文件夹路径创建

    File
  2. 判断是否是文件夹(这里也可以做成非文件夹格式,写成通用的方式,也就不仅限于传文件夹了)
  3. 获取文件夹下的内容

    fileList

    (因为当前文件夹是不需要打包的,所以会放到上层遍历)
  4. 构建 ZipOutputStream 作为递归参数
  5. 遍历

    fileList

    ,递归处理

    1. 判断是文件,构建ZipEntry
    2. 判断是文件夹,遍历递归
  6. 递归完成,关闭流

代码实现:

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



版权声明:本文为DespairC原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。