项目中有一个下载docx模板文件的功能。开发同学反馈:本地测试可以正常下载;部署到测试服务器后,下载的文件为空。
定位问题开发环境和测试环境有哪些差异呢?
- 环境差异
- 开发环境为windows
- 测试环境为linux
- 因java跨平台,这个差异基本可排除
- 运行方式差异
- 开发环境直接从IDE中run
- 测试环境是打包为jar后在run
- 本地打包jar后run,可复现
下载文件为空的代码
@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
@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("
ClassLoader: ")
.append(classLoader.getClass())
.append("
InputStream: ")
.append(inputStream.getClass())
.append("
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("
length: " + total);
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
解决方案
经测试及分析,注释掉res.addHeader(“Content-Length”, String.valueOf(inputStream.available()));即可;经验证,下载文件正常,代码如下:
@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()方法获取到文件的大小。测试代码如下:
@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("
ClassLoader: ")
.append(classLoader.getClass())
.append("
InputStream: ")
.append(inputStream.getClass())
.append("
inputStream.available: ")
.append(inputStream.available());
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
查看源码时候,也得到了验证。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;
@RestController
@RequestMapping(value = "/")
public class DemoController {
@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("
ClassLoader: ")
.append(classLoader.getClass())
.append("
InputStream: ")
.append(inputStream.getClass())
.append("
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("
length: " + total);
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
@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("
ClassLoader: ")
.append(classLoader.getClass())
.append("
InputStream: ")
.append(inputStream.getClass())
.append("
inputStream.available: ")
.append(inputStream.available());
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
@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();
}
}
@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();
}
}
}
Demo地址


