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. 如何使用ASP.NET Core 中的 Hangfire 实现作业调度

    https://procodeguide.com/programming/hangfire-in-aspnet-core-schedule-jobs/ 如何使用ASP.NET Core 中的 Hang ...

  2. 【Android】使用Exoplayer播放视频

    1 前言 ​ ExoPlayer 是 google 开源的应用级媒体播放器,github源码 → https://github.com/google/ExoPlayer.相较于 MediaPlayer ...

  3. React虚拟DOM的理解

    React虚拟DOM的理解 Virtual DOM是一棵以JavaScript对象作为基础的树,每一个节点可以将其称为VNode,用对象属性来描述节点,实际上它是一层对真实DOM的抽象,最终可以通过渲 ...

  4. python-web:flask框架下的html实例——用户注册页面

    1.submit实现页面跳转,方法为get <h1>用户注册</h1> <!-- 使用get方式提交,method为post/get,action保存提交到哪里 --&g ...

  5. 一个Git Commit Message模板

    一个统一的commit消息模板可以约束团队成员使用一致的方式提交变更信息,这样也方便集成工具进行合规检查. 通常来讲,commit信息应该包含如下内容: <type>(<scope& ...

  6. nuxt调用weixin-js-sdk

    在nuxt中调用weixin-js-sdk与在vue中有所不同. 通常在vue中用 import wx from 'weixin-js-sdk' 调用weixin-js-sdk,但在nuxt中会出现w ...

  7. 如何渲染最原始的yuv视频数据?

    一.整体思路 我们在用纹理增加细节那篇文章中提到过,要将图片渲染在屏幕上,首先要拿到图片的像素数组数据,然后将像素数组数据通过纹理单元传递到片段着色器中,最后通过纹理采样函数将纹理中对应坐标的颜色值采 ...

  8. Jetpack Compose(1) —— Android 全新的 UI 框架

    写在前面 Jetpack Compose 已经不是什么新技术了,Google 早在 2019 年就推出 Jetpack Compose 的首个 alpha 版本,时至今日,相当大比例的国内 Andro ...

  9. 手动下载sdk及avd

    有时候下载sdk的时候报各种错误导致无法下载,如图 那么可以离线下载,到浏览器或IDM中下载图中提示的url链接,将第一个下载的包即sources的解压后放到SDK目录下的sources目录,并重命名 ...

  10. Vue 之 new vue({})与export default {} 的区别

    刚入门 vue 的,可能会有这样的困惑:什么时候用new vue({}),什么时候用export default {} 呢? 比如,我们在用 cli 创建项目时 在 main.js 入口里面用的时ne ...