• Spring Boot框架中针对数据文件模板的下载总结


    1.前言

    在我们的日常开发中,经常会碰到注入导入Excel数据到系统中的需求,而在导入Excel数据时,一般的业务系统都会提供数据的Excel模板,只有提交的Excel数据满足业务系统要求的模板时,数据才能够正常的导入系统中。因此针对这种需求,一般我们会在系统中提供一个Excel模板的下载按钮,业务人员在使用时,可以先下载Excel模板,然后按照模板中的格式将数据填充,即可导入成功。本文主要总结目前在开发这类需求时碰到的问题。

    2.解决方案

    从需求上来看,目前有大致三种解决方案,针对数据文件的模板下载,分别是:

    • 模板文件直接存放在前端,作为静态资源,前端直接可以发送请求进行下载
    • 模板文件存服务器磁盘,提供接口下载
    • 模板文件存储在项目jar包中,提供接口下载

    2.1 作为静态资源直接下载

    第一种方式是最简单的,将数据文件直接作为静态资源放在前端目录,前端通过请求即可进行下载

    2.2 模板文件存储在服务器,提供接口下载

    第二种也是我们经常使用的方法,开发人员将模板文件放在服务器中的某个目录下,通过在代码中配置存储目录的方式,并且提供下载接口,当前端发起接口请求时,服务端根据请求将文件写入到响应流中

    示例代码如下:

    @Value("${templateFile}")
    String downloadFilePath;
    
    @GetMapping("/download")
    public void downloadExcel(HttpServletResponse response){
        logger.info("下载Excel模板");
        try {
            File file=new File(downloadFilePath);
            ServletUtil.write(response,file);
        } catch (IOException e) {
            logger.error(e.getMessage(),e);
        }
    }
    

    因为文件存储在磁盘中,并且通过Spring提供的@Value注解将文件的位置在配置文件中进行配置,因此文件对象我们可以直接通过new File的方式直接获取到文件,最终调用工具类ServletUtil将该文件写入到HttpServletResponse的流中去,实现下载的目录

    2.3 模板文件存在在jar中,提供接口下载

    通过上面的两种下载方式,我们基本已经能实现文件的下载,满足业务的需要,但有时候我也会思考,是否把数据模板文件直接放在Spring Boot的jar中,这种方式的优势:

    • 防止模板文件存储在磁盘时被误删的操作发送
    • 如果程序部署需要迁移服务器,能有效避免下载接口的容错,忘记迁移模板文件等情况会导致程序异常
    • 和程序代码存储在一起更加完整

    基于上面的优势,因此,针对数据模板文件,我认为应该和项目直接放在一起,这样对于程序部署等都是非常有利的。

    一般,在Spring Boot的开发框架中,我们可以在resources目录下建立文件夹,然后将相应的数据文件放入目录中,再提供接口读取该文件进行下载

    目录结构如下:

    |---project
    |--------src/main/java
    |--------src/main/resources
    |------------data
    # 模板文件
    |--------------template.xlsx
    

    因为我们将文件放在了resources目录下,此时如果要读取该文件,我们需要利用到Spring提供的ClassPathResource类进行读取,调用代码如下:

    ClassPathResource classPathResource=new ClassPathResource("data/tag_data_template.xlsx");
    

    此时,我们的下载接口代码如下:

    @GetMapping("/download")
    public void downloadExcel(HttpServletResponse response){
        logger.info("下载Excel模板");
        ClassPathResource classPathResource=new ClassPathResource("data/template.xlsx");
        try {
            //创建临时文件
            File file=File.createTempFile("template",".xlsx");
            //从当前resources目录下的文件流拷贝到File中
            FileUtils.copyInputStreamToFile(classPathResource.getInputStream(),file);
            logger.info("fileName:{}",file.getName());
            //将临时文件写出到流中
            ServletUtil.write(response,file);
        } catch (IOException e) {
            logger.error(e.getMessage(),e);
        }
    }
    

    这里会有1个疑问点,就是我们既然已经使用了Spring提供的ClassPathResource进行读取文件,而该类通过继承AbstractFileResolvingResource也提供了getFile方法获取File对象,为何不直接调用?

    比如下载的接口代码改成这样:

    @GetMapping("/download")
    public void downloadExcel(HttpServletResponse response){
        logger.info("下载Excel模板");
        ClassPathResource classPathResource=new ClassPathResource("data/template.xlsx");
        try {
            //直接获取文件
            File file=classPathResource.getFile();
            //将临时文件写出到流中
            ServletUtil.write(response,file);
        } catch (IOException e) {
            logger.error(e.getMessage(),e);
        }
    }
    

    通过源码来分析

    //AbstractFileResolvingResource.getFile
    @Override
    public File getFile() throws IOException {
        URL url = getURL();
        if (url.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {
            return VfsResourceDelegate.getResource(url).getFile();
        }
        return ResourceUtils.getFile(url, getDescription());
    }
    
    //ResourceUtils
    /** URL protocol for a file in the file system: "file". */
    public static final String URL_PROTOCOL_FILE = "file";
    //ResourceUtils.getFile
    public static File getFile(URL resourceUrl, String description) throws FileNotFoundException {
        Assert.notNull(resourceUrl, "Resource URL must not be null");
        if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) {
            throw new FileNotFoundException(
                description + " cannot be resolved to absolute file path " +
                "because it does not reside in the file system: " + resourceUrl);
        }
        try {
            return new File(toURI(resourceUrl).getSchemeSpecificPart());
        }
        catch (URISyntaxException ex) {
            // Fallback for URLs that are not valid URIs (should hardly ever happen).
            return new File(resourceUrl.getFile());
        }
    }
    

    在最终的ResourceUtils.getFile方法获取File对象时,Spring会对当前URL对象的协议进行判断,如果文件的协议不是file,则会抛出异常,提示

     class path resource [data/tag_data_template.xlsx] cannot be resolved to absolute file path because it does not reside in the file system: jar:file:/home/app.jar/BOOT-INF/classes!/data/template.xlsx
    

    大致的意思就是该文件不在文件系统中,既然Spring不允许这么干,那么我们只能通过获取该文件的输入流的方式,将流写到临时文件中去,最终将该临时文件写出。

    //FileUtils.copyInputStreamToFile方法
    //commons-io 包中提供的方法
    public static void copyInputStreamToFile(InputStream source, File destination) throws IOException {
        try {
            FileOutputStream output = openOutputStream(destination);
            try {
                IOUtils.copy(source, output);
                output.close(); // don't swallow close Exception if copy completes normally
            } finally {
                IOUtils.closeQuietly(output);
            }
        } finally {
            IOUtils.closeQuietly(source);
        }
    }
    

    以上的操作完成后,我们可能还会碰到部署时,代码还是会抛异常的问题,说文件找不到,这种情况一般会和我们项目的maven打包配置有关,我们需要在项目的maven配置中将模板文件也一起打包进去,例如增加配置如下:

    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
    		   <!--包含data目录下的所有文件一起打包-->
                <include>**/data/**</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
    

    至此,就大功告成了!!!

    3.附录

    3.1 ServletUtil.write方法

    ServletUtil工具类是引用的开源项目Hutool中的一个关于Servlet的工具类封装.

    write方法提供了将文件写入到流中的封装,来看具体的源码:

    封装了我们工作中基础的写出流的操作,我们在代码中也可以通过调用此方法简化我们的代码。

    /** 默认缓存大小 8192*/
    public static final int DEFAULT_BUFFER_SIZE = 2 << 12;
    /**
     * 返回文件给客户端
     * 
     * @param response 响应对象{@link HttpServletResponse}
     * @param file 写出的文件对象
     * @since 4.1.15
    */
    public static void write(HttpServletResponse response, File file) {
        final String fileName = file.getName();
        //根据文件名称获取文件的响应类型,如果没有则默认application/octet-stream
        final String contentType = ObjectUtil.defaultIfNull(FileUtil.getMimeType(fileName),"application/octet-stream");
        BufferedInputStream in = null;
        try {
            in = FileUtil.getInputStream(file);
            //再次调用,写出Header等信息
            write(response, in, contentType, fileName);
        } finally {
            IoUtil.close(in);
        }
    }
    
    /**
     * 返回数据给客户端
     * 
     * @param response 响应对象{@link HttpServletResponse}
     * @param in 需要返回客户端的内容
     * @param contentType 返回的类型
     * @param fileName 文件名
     * @since 4.1.15
    */
    public static void write(HttpServletResponse response, InputStream in, String contentType, String fileName) {
        final String charset = ObjectUtil.defaultIfNull(response.getCharacterEncoding(), CharsetUtil.UTF_8);
        response.setHeader("Content-Disposition", StrUtil.format("attachment;filename={}", URLUtil.encode(fileName, charset)));
        response.setContentType(contentType);
        //写出
        write(response, in);
    }
    
    /**
     * 返回数据给客户端
     * 
     * @param response 响应对象{@link HttpServletResponse}
     * @param in 需要返回客户端的内容
    */
    public static void write(HttpServletResponse response, InputStream in) {
    	write(response, in, IoUtil.DEFAULT_BUFFER_SIZE);
    }
    
    /**
     * 返回数据给客户端
     * 
     * @param response 响应对象{@link HttpServletResponse}
     * @param in 需要返回客户端的内容
     * @param bufferSize 缓存大小
    */
    public static void write(HttpServletResponse response, InputStream in, int bufferSize) {
        ServletOutputStream out = null;
        try {
            out = response.getOutputStream();
            IoUtil.copy(in, out, bufferSize);
        } catch (IOException e) {
            throw new UtilException(e);
        } finally {
            IoUtil.close(out);
            IoUtil.close(in);
        }
    }
    
  • 相关阅读:
    bzoj1505 [NOI2004]小H的小屋
    最大值
    数学
    OI中的快速傅里叶变换(FFT)
    旅游规划
    加分二叉树
    浅谈 字符串hash
    二分的弟弟“三分”
    Trie树(c++)
    克鲁斯卡尔
  • 原文地址:https://www.cnblogs.com/xiaoymin/p/14474314.html
Copyright © 2020-2023  润新知