背景
项目中有一个下载docx模板文件的功能。开发同学反馈:本地测试可以正常下载;部署到测试服务器后,下载的文件为空。
定位问题
开发环境和测试环境有哪些差异呢?
- 环境差异
- 开发环境为windows
- 测试环境为linux
- 因java跨平台,这个差异基本可排除
- 运行方式差异
- 开发环境直接从IDE中run
- 测试环境是打包为jar后在run
- 本地打包jar后run,可复现
验证问题
下载文件为空的代码
/**
* 从resources目录中下载docx文件(空文件)
*
* @return
*/
@GetMapping("/downloadEmpty")
public void downloadEmpty(HttpServletResponse res) {
String path = "templates/demo.docx";
ClassLoader classLoader = Demo3Application.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(path);
OutputStream outputStream = res.getOutputStream()) {
res.addHeader("Content-Disposition", "attachment;filename=empty.docx");
res.addHeader("Content-Length", String.valueOf(inputStream.available()));
res.setContentType("application/octet-stream");
byte[] bys = new byte[1024];
int len;
while ((len = inputStream.read(bys)) != -1) outputStream.write(bys, 0, len);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
以下代码输出docx文件的一些信息,经分析可知:读取到的inputStream正常,只是inputStream.available() == 0
/**
* 输出从resources目录下读取docx文件的信息
*
* @return
*/
@GetMapping("/docx")
public String doc() {
StringBuilder sb = new StringBuilder();
String path = "templates/demo.docx";
ClassLoader classLoader = Demo3Application.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(path)) {
sb.append("path: ")
.append(path)
.append("<br/>ClassLoader: ")
.append(classLoader.getClass())
.append("<br/>InputStream: ")
.append(inputStream.getClass())
.append("<br/>inputStream.available: ")
.append(inputStream.available());
byte[] bys = new byte[1024];
int len = 0, total = 0;
while ((len = inputStream.read(bys)) != -1) total += len;
sb.append("<br/>length: " + total);
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
/**
* 输出示例:
* path: templates/demo.docx ClassLoader: class
* org.springframework.boot.loader.LaunchedURLClassLoader InputStream: class
* org.springframework.boot.loader.data.RandomAccessDataFile$DataInputStream
* inputStream.available: 0
* length: 16044
*/
}
解决方案
经测试及分析,注释掉res.addHeader(“Content-Length”, String.valueOf(inputStream.available()));即可;经验证,下载文件正常,代码如下:
/**
* 从resources目录中下载docx文件(正常文件)
*
* @return
*/
@GetMapping("/download")
public void download(HttpServletResponse res) {
String path = "templates/demo.docx";
ClassLoader classLoader = Demo3Application.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(path);
OutputStream outputStream = res.getOutputStream()) {
res.addHeader("Content-Disposition", "attachment;filename=demo.docx");
// res.addHeader("Content-Length", String.valueOf(inputStream.available()));
res.setContentType("application/octet-stream");
byte[] bys = new byte[1024];
int len;
while ((len = inputStream.read(bys)) != -1) outputStream.write(bys, 0, len);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
原理分析
经测试及分析,看似下载文件为空的问题,实质是获取到输入流inputStream.available() == 0的问题。为什么会返回0呢?
我们观察到读取docx文件时候,返回的inputStream是org.springframework.boot.loader.data.RandomAccessDataFile$DataInputStream;翻开源码看一下,DataInputStream继承了InputStream,而InputStream的available()方法直接返回了0。
在实际验证问题的过程中并没有这么一帆风顺。起初我使用了一个txt文件来验证此问题,但是无法重现,也就是说,打包jar后也可以通过available()方法获取到文件的大小。测试代码如下:
/**
* 输出从resources目录下读取txt文件的信息
*
* @return
*/
@GetMapping("/txt")
public String txt() {
StringBuilder sb = new StringBuilder();
String path = "templates/demo.txt";
ClassLoader classLoader = Demo3Application.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(path)) {
sb.append("path: ")
.append(path)
.append("<br/>ClassLoader: ")
.append(classLoader.getClass())
.append("<br/>InputStream: ")
.append(inputStream.getClass())
.append("<br/>inputStream.available: ")
.append(inputStream.available());
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
/**
* 输出示例: path: templates/demo.txt ClassLoader: class
* org.springframework.boot.loader.LaunchedURLClassLoader InputStream: class
* org.springframework.boot.loader.jar.ZipInflaterInputStream inputStream.available: 43
*/
}
查看源码时候,也得到了验证。txt文件属于压缩文件(DEFLATED),返回inputStream是org.springframework.boot.loader.jar.ZipInflaterInputStream,
而返回inputStream是org.springframework.boot.loader.data.RandomAccessDataFile$DataInputStream的文件属于非压缩文件(STORED)
总结
- Spring Boot打包为jar后运行时,通过class org.springframework.boot.loader.LaunchedURLClassLoader读取resources目录下文件。
- 分为2种类型文件
- STORED类型
- 读取到inputStream是org.springframework.boot.loader.data.RandomAccessDataFile$DataInputStream
- input.available() == 0
- DEFLATED
- 读取到inputStream是org.springframework.boot.loader.jar.ZipInflaterInputStream
- input.available() == size(文件大小)
- STORED类型
以上给出了解决方案及原理分析,但并不建议将下载的文件放到resources目录下;可以放到分布式存储或其他文件系统中。
附
完整的测试代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/** 需要打成jar包执行 */
@RestController
@RequestMapping(value = "/")
public class DemoController {
/**
* 输出从resources目录下读取docx文件的信息
*
* @return
*/
@GetMapping("/docx")
public String doc() {
StringBuilder sb = new StringBuilder();
String path = "templates/demo.docx";
ClassLoader classLoader = Demo3Application.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(path)) {
sb.append("path: ")
.append(path)
.append("<br/>ClassLoader: ")
.append(classLoader.getClass())
.append("<br/>InputStream: ")
.append(inputStream.getClass())
.append("<br/>inputStream.available: ")
.append(inputStream.available());
byte[] bys = new byte[1024];
int len = 0, total = 0;
while ((len = inputStream.read(bys)) != -1) total += len;
sb.append("<br/>length: " + total);
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
/**
* 输出示例: path: templates/demo.docx ClassLoader: class
* org.springframework.boot.loader.LaunchedURLClassLoader InputStream: class
* org.springframework.boot.loader.data.RandomAccessDataFile$DataInputStream
* inputStream.available: 0 length: 16044
*/
}
/**
* 输出从resources目录下读取txt文件的信息
*
* @return
*/
@GetMapping("/txt")
public String txt() {
StringBuilder sb = new StringBuilder();
String path = "templates/demo.txt";
ClassLoader classLoader = Demo3Application.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(path)) {
sb.append("path: ")
.append(path)
.append("<br/>ClassLoader: ")
.append(classLoader.getClass())
.append("<br/>InputStream: ")
.append(inputStream.getClass())
.append("<br/>inputStream.available: ")
.append(inputStream.available());
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
/**
* 输出示例: path: templates/demo.txt ClassLoader: class
* org.springframework.boot.loader.LaunchedURLClassLoader InputStream: class
* org.springframework.boot.loader.jar.ZipInflaterInputStream inputStream.available: 43
*/
}
/**
* 从resources目录中下载docx文件(空文件)
*
* @return
*/
@GetMapping("/downloadEmpty")
public void downloadEmpty(HttpServletResponse res) {
String path = "templates/demo.docx";
ClassLoader classLoader = Demo3Application.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(path);
OutputStream outputStream = res.getOutputStream()) {
res.addHeader("Content-Disposition", "attachment;filename=empty.docx");
res.addHeader("Content-Length", String.valueOf(inputStream.available()));
res.setContentType("application/octet-stream");
byte[] bys = new byte[1024];
int len;
while ((len = inputStream.read(bys)) != -1) outputStream.write(bys, 0, len);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从resources目录中下载docx文件(正常文件)
*
* @return
*/
@GetMapping("/download")
public void download(HttpServletResponse res) {
String path = "templates/demo.docx";
ClassLoader classLoader = Demo3Application.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(path);
OutputStream outputStream = res.getOutputStream()) {
res.addHeader("Content-Disposition", "attachment;filename=demo.docx");
// res.addHeader("Content-Length", String.valueOf(inputStream.available()));
res.setContentType("application/octet-stream");
byte[] bys = new byte[1024];
int len;
while ((len = inputStream.read(bys)) != -1) outputStream.write(bys, 0, len);
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}