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);
}
}
Spring Boot框架中针对数据文件模板的下载总结的更多相关文章
- spring boot(十八)集成FastDFS文件上传下载
上篇文章介绍了如何使用Spring Boot上传文件,这篇文章我们介绍如何使用Spring Boot将文件上传到分布式文件系统FastDFS中. 这个项目会在上一个项目的基础上进行构建. 1.pom包 ...
- Spring Boot项目中的字体文件问题_Failed to decode downloaded font
1.问题:字体文件加载失败,本来应该是“X”号,现在只有一个小方块 2.原因:问题是Maven正在过滤字体文件并破坏它们. <resource> <filtering>true ...
- 在Spring Boot项目中使用Spock框架
转载:https://www.jianshu.com/p/f1e354d382cd Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring B ...
- 在Spring Boot项目中使用Spock测试框架
本文首发于个人网站:在Spring Boot项目中使用Spock测试框架 Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目 ...
- (Spring Boot框架)快速入门
Spring Boot 系列文章推荐 Spring Boot 入门 Spring Boot 属性配置和使用 Spring Boot 集成MyBatis Spring Boot 静态资源处理 今天介绍一 ...
- 初识Spring Boot框架(二)之DIY一个Spring Boot的自动配置
在上篇博客初识Spring Boot框架中我们初步见识了SpringBoot的方便之处,很多小伙伴可能也会好奇这个Spring Boot是怎么实现自动配置的,那么今天我就带小伙伴我们自己来实现一个简单 ...
- spring boot 在框架中注入properties文件里的值(Spring三)
前一篇博客实现了打开第一个页面 链接:https://blog.csdn.net/qq_38175040/article/details/105709758 本篇博客实现在框架中注入propertie ...
- spring boot 框架 启动更新项目,以及生成 "实体_"文件
1.更新项目 clean ---> 更新项目 ---> package--->refresh 即可.(这几个步骤一个不能够少) 2.项目中的类的依赖关系存在,但是无法导入依赖 m ...
- spring boot 学习(二)spring boot 框架整合 thymeleaf
spring boot 框架整合 thymeleaf spring boot 的官方文档中建议开发者使用模板引擎,避免使用 JSP.因为若一定要使用 JSP 将无法使用. 注意:本文主要参考学习了大神 ...
- Spring Boot项目中使用Mockito
本文首发于个人网站:Spring Boot项目中使用Mockito Spring Boot可以和大部分流行的测试框架协同工作:通过Spring JUnit创建单元测试:生成测试数据初始化数据库用于测试 ...
随机推荐
- 【Unity3D】阴影原理及应用
1 阴影原理 光源照射到不透明物体上,会向该物体的后面投射阴影,如果阴影区域存在其他物体,这些物体不被光源照射的部分就需要渲染阴影.因此,我们可以将阴影的生成抽象出 2 个流程:物体投射阴影.物体 ...
- 【Unity3D】动画回调函数、动画事件、动画曲线
1 动画回调函数 动画回调函数是指动画在开始时.执行中.结束时回调的函数,主要有:OnStateEnter.OnStateUpdate.OnStateExit.OnStateMove.OnStat ...
- Vue+SpringBoot+ElementUI实战学生管理系统-2.搭建Vue+elementUI脚手架
1.项目介绍 前一片介绍了项目的整体情况,这一篇开始搭建前端工程,需要的朋友可以拿去自己定制.:) 2.获取源码 源码是捐赠方式获取,详细请QQ联系我 :)! 3.项目截图 登录页 列表操作 动态图 ...
- linux 快速安装LAMP教程
最近学习linux,安装lamp遇到一些问题,记录下来分享一下: ------------------------------------------------------------------- ...
- oracle exp/imp命令使用parfile实现参数文件调用
优先使用数据泵(expdp/impdp)方式,更高效,问题少. 关于exp/imp工具的使用请参考我的另一篇文章: https://blog.csdn.net/IndexMan/article/det ...
- spring boot 2.0集成并使用redis
项目地址:https://gitee.com/indexman/spring_boot_in_action 前面一章介绍了spring boot自带的缓存,下面讲一下如何在2.0版本中集成并使用red ...
- python基础语法知识
1.多组输入没有结束标志的两种表示形式 #method1: try: while True: #代码 except EOFError: pass #method2: while True: try: ...
- 《深入理解Java虚拟机》(五) JVM调优案例
目录 问题 排查问题经过了如下的过程: 排除是否数据库卡顿造成 任务管理器 与客户沟通 至此开始通过JVM排查问题: JVM参数介绍 第一次Full GC 第二次Full GC截图 第三次Full G ...
- pymysql基本语法,sql注入攻击,python操作pymysql,数据库导入导出及恢复数据---day38
1.pymysql基本语法 # ### python操作mysql import pymysql ''' # ### 1.基本语法 #(1) 创建连接 host user password datab ...
- 【生成对抗网络学习 其二】GAN(keras实现)代码阅读笔记
想来想去还是记录一下吧,主要是怕以后时间长忘了 好记性不如烂笔头 代码来自eriklindernoren的开源GAN实现:https://github.com/eriklindernoren/Kera ...