Spring Boot项目以jar包形式启动,下载resources目录中文件为空的解决方案及原因分析

Spring Boot项目以jar包形式启动,下载resources目录中文件为空的解决方案及原因分析,第1张

Spring Boot项目以jar包形式启动,下载resources目录中文件为空的解决方案及原因分析 背景

项目中有一个下载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(文件大小)

以上给出了解决方案及原理分析,但并不建议将下载的文件放到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地址

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5681114.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存