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创建单元测试:生成测试数据初始化数据库用于测试 ...
随机推荐
- NC19832 1408
题目链接 题目 题目描述 小m曾经给小t和小s讲过一个奇怪的故事.这个故事叫做1408.故事的大体内容如下. 主人公迈克·安瑟林(约翰·库萨克饰)是一个恐怖小说家.将装神弄鬼作为本职工作的迈克,平日里 ...
- NC16562 [NOIP2012]开车旅行
题目链接 题目 题目描述 小 A 和小 B 决定利用假期外出旅行,他们将想去的城市从 1 到 N 编号,且编号较小的城市在编号较大的城市的西边,已知各个城市的海拔高度互不相同,记城市 i 的海拔高度为 ...
- 利用LiveReload插件实现vscode和谷歌浏览器实时刷新
说明 最近在研究CSS希望可以提升一个层次.在写DEMO练习的时候老是需要去谷歌浏览器手动刷新页面才能看到更改后的效果次数多了 我也受不了,这不我又请来了个帮手: LiveReload,名如其人,这家 ...
- spring boot整合poi实现excel文件导入导出实战
今天科比离去,今天肺炎病毒持续肆虐... 意识到生命的脆弱,今天我继续前行,比以往更加坚定和紧迫,这辈子不活好自己就算白来一趟. 1.项目介绍 最近帮朋友做了一个小工具,就是实现:上传一个excel文 ...
- 在Android开发中如何使用OpenSL ES库播放解码后的pcm音频文件?
一.认识OpenSL ES OpenSL ES的全称是Open Sound Library For Embedded Systems,即应用于嵌入式系统的开源音频库.Android从2.3版本起就开始 ...
- 文心一言 VS 讯飞星火 VS chatgpt (199)-- 算法导论15.2 1题
一.用go语言,对矩阵规模序列(5,10,3,12,5,50,6),求矩阵链最优括号化方案. 文心一言,代码正常运行: 在Go语言中,为了找到矩阵链乘法的最优括号化方案,我们通常会使用动态规划(Dyn ...
- Java缓存框架整理
Spring Cache 为基于Spring框架的应用提供了一套完整的缓存API抽象,具体的缓存实现可以对接如Ehcache,Redis等. https://docs.spring.io/spring ...
- git修改地址三种方法
1.修改命令 git remote set-url origin [NEW_URL] 2.先删后加 git remote rm origin git remote add origin [url]3. ...
- 【Azure 微服务】面对Service Fabric中节点状态不正常(Disabling/Warning/RemoveNode)的几种尝试解决方案
问题描述 发现 Service Fabric 的节点状态异常,如出现 Disabling, Warning,或者 RemoveNode的情况,并且持续很长时间都没有变化(2小时以上).如何来缓解这种问 ...
- spring重点后置处理器
1. DefaultListableBeanFactory的作用: 默认实现了ListableBeanFactory和BeanDefinitionRegistry接口,基于bean definitio ...