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

Spring Boot框架中针对数据文件模板的下载总结的更多相关文章

  1. spring boot(十八)集成FastDFS文件上传下载

    上篇文章介绍了如何使用Spring Boot上传文件,这篇文章我们介绍如何使用Spring Boot将文件上传到分布式文件系统FastDFS中. 这个项目会在上一个项目的基础上进行构建. 1.pom包 ...

  2. Spring Boot项目中的字体文件问题_Failed to decode downloaded font

    1.问题:字体文件加载失败,本来应该是“X”号,现在只有一个小方块 2.原因:问题是Maven正在过滤字体文件并破坏它们. <resource> <filtering>true ...

  3. 在Spring Boot项目中使用Spock框架

    转载:https://www.jianshu.com/p/f1e354d382cd Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring B ...

  4. 在Spring Boot项目中使用Spock测试框架

    本文首发于个人网站:在Spring Boot项目中使用Spock测试框架 Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目 ...

  5. (Spring Boot框架)快速入门

    Spring Boot 系列文章推荐 Spring Boot 入门 Spring Boot 属性配置和使用 Spring Boot 集成MyBatis Spring Boot 静态资源处理 今天介绍一 ...

  6. 初识Spring Boot框架(二)之DIY一个Spring Boot的自动配置

    在上篇博客初识Spring Boot框架中我们初步见识了SpringBoot的方便之处,很多小伙伴可能也会好奇这个Spring Boot是怎么实现自动配置的,那么今天我就带小伙伴我们自己来实现一个简单 ...

  7. spring boot 在框架中注入properties文件里的值(Spring三)

    前一篇博客实现了打开第一个页面 链接:https://blog.csdn.net/qq_38175040/article/details/105709758 本篇博客实现在框架中注入propertie ...

  8. spring boot 框架 启动更新项目,以及生成 "实体_"文件

    1.更新项目 clean  --->  更新项目 ---> package--->refresh 即可.(这几个步骤一个不能够少) 2.项目中的类的依赖关系存在,但是无法导入依赖 m ...

  9. spring boot 学习(二)spring boot 框架整合 thymeleaf

    spring boot 框架整合 thymeleaf spring boot 的官方文档中建议开发者使用模板引擎,避免使用 JSP.因为若一定要使用 JSP 将无法使用. 注意:本文主要参考学习了大神 ...

  10. Spring Boot项目中使用Mockito

    本文首发于个人网站:Spring Boot项目中使用Mockito Spring Boot可以和大部分流行的测试框架协同工作:通过Spring JUnit创建单元测试:生成测试数据初始化数据库用于测试 ...

随机推荐

  1. axios.delete传参,400错误

    我在使用axios.delete进行传参的时候,发现会报400错误 后端代码(C#) 前端代码 这样的参数请求会报400错误 后端就一个参数,前端发一个id为什么接受不到呢? 在网上找了半天,终于明白 ...

  2. NC18987 粉嘤花之恋

    题目链接 题目 题目描述 qn是个特别可爱的小哥哥,qy是个特别好的小姐姐,他们两个是一对好朋友 [ cp (划掉~) 又是一年嘤花烂漫时,小qn于是就邀请了qy去嘤花盛开的地方去玩.当qy和qn来到 ...

  3. 【Android】使用Socket实现跨设备通讯

    1 Socket 简介 ​ Socket(套接字)是应用层与 TCP/IP 协议通信的中间软件抽象层,它是一组接口,用户只需面向 Socket 编程,即可实现跨设备(网络)通讯. ​ Socket 是 ...

  4. 虚拟化技术VirtualBox和vagrant基本使用

    虚拟化技术VirtualBox和vagrant基本使用 1.首先安装VirtualBox 可以去官网下载 https://www.virtualbox.org/ 2.安装vagrant(根据自己电脑得 ...

  5. Spring源码之springMVC

    目录 web.xml 程序入口 servlet 初始化 运行阶段 销毁阶段 DispatcherServlet 初始化 DispatcherServlet 的逻辑处理 web.xml 它的作用是配置初 ...

  6. Direct2D 几何篇

    微软文档:Geometries overview 本篇通过官方文档学习,整理出来的demo,初始样本请先创建一个普通的desktop app. // Test_Direct2D_Brush.cpp : ...

  7. win32 - this 指针

    this指针是存在与类的成员函数中,指向被调用函数所在的类实例的地址. 根据以下程序来说明this指针, #include<iostream.h> class Point { int x, ...

  8. C++ 析构函数的调用顺序

    如果指针指向基类,但是指针在运行时指向派生类,则该基类必须具有虚拟析构函数,以便破坏派生类.如果没有虚拟析构函数,则只会运行基类的析构函数. 比如: Base* basePtr; basePtr = ...

  9. 搜索引擎RAG召回效果评测MTEB介绍与使用入门

    RAG 评测数据集建设尚处于初期阶段,缺乏针对特定领域和场景的专业数据集.市面上常见的 MS-Marco 和 BEIR 数据集覆盖范围有限,且在实际使用场景中效果可能与评测表现不符.目前最权威的检索榜 ...

  10. 麒麟系统开发笔记(九):在国产麒麟系统上搭建宇视摄像头SDK基础环境Demo

    前言   国产麒麟系统开发上,使用宇视摄像头,本篇使用宇视官网的提供的SDK,搭建基础的国产系统上宇视摄像头SDK开发化境Demo.   效果演示      宇视SDK下载   CSDN粉丝0积分下载 ...