此文要从SpringBoot打包后不能读取classpath下文件说起

简介: 您好,我是码农飞哥,感谢您阅读本文!如果此文对您有所帮助,请毫不犹豫的给个一键三连吧。

您好,我是码农飞哥,感谢您阅读本文!如果此文对您有所帮助,请毫不犹豫的给个一键三连吧。

此文要从SpringBoot打成jar之后不能读取classpath下文件说起,并由此作为一个切入点,思考如何正确的读取jar包中的文件。

问题复现

事情是这样的,昨天快下班了时候,测试小姐姐突然说(PS: 有些测试小姐姐上班的时候不提啥BUG,下班的时候给你提一堆BUG,不知道大家有没有这种感觉)。

有个图片下载的接口不能用了。害,本想6点钟下班走人的我瞬间懵逼了,这下走不了。老规矩,先查错误日志。

还是那个熟悉的FileNotFoundException异常。

2021-04-21 09:43:59.715  INFO 16896 --- [nio-8383-exec-1] com.jay.ImgDownloadController            : qr_code-icon path is: file:/D:/workspace/file_dow
nload_demo/target/file_download_demo-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/img/qr_code-icon.png
java.io.FileNotFoundException: file:\D:\workspace\file_download_demo\target\file_download_demo-0.0.1-SNAPSHOT.jar!\BOOT-INF\classes!\img\qr_code-icon.
png (文件名、目录名或卷标语法不正确。)
        at java.io.FileInputStream.open0(Native Method) ~[na:1.8.0_60]
        at java.io.FileInputStream.open(Unknown Source) ~[na:1.8.0_60]
        at java.io.FileInputStream.<init>(Unknown Source) ~[na:1.8.0_60]
  at com.jay.ImgDownloadController.downloadImage(ImgDownloadController.java:35) ~[classes!/:0.0.1-SNAPSHOT]

很明显这是一个无效的文件路径,根据这个错误路径程序肯定不能找到指定的文件咯。

再回头定位到报错的代码

//读取文件的路径
        String path = Thread.currentThread().getContextClassLoader().getResource("").getPath()
                + "img/qr_code-icon.png";
        LOGGER.info("qr_code-icon path is: " + path);
  InputStream is = new FileInputStream(new File(path));

这段代码在本地调试的时候明明是没有问题的呀,单元测试都跑过了,本地调试时输入的路径如下:。

13df44613c3417aa7764d478cf16240c_20210421103609496.png

打印的路径地址是:D:/workspace/file_download_demo/target/classes/img/qr_code-icon.png

这个路径是一个有效的路径。

这又是一个我本地明明没问题,到服务器就有问题了。锅是甩不出去了

问题思考

我们都知道JAVA是一门静态语言,先编译再运行也就是先将java文件编译成class文件,然后在用虚拟机来执行class文件的。SpringBoot在编译打包后会生成target目录,class文件,资源文件还有jar包都会被放在这个目录下。如下图所示:

0042bcdbd13e80a4c2c75d36ed2d9298_20210421105019635.png

其中所有的class文件以及资源文件都放在了classes文件夹中。在本地运行时 Thread.currentThread().getContextClassLoader().getResource("").getPath()

其中Thread.currentThread().getContextClassLoader()返回的是当前线程的类加载器(默认是AppClassLoader类加载器),类加载器可以加载类也可以加载资源。类加载器有很多,具体可以参考双亲委派模型以及SpringFactoriesLoader详解(最全最简单的介绍)

读取到的路径是D:/workspace/file_download_demo/target/classes,classes文件夹所在的路径也就是我们熟悉的classpath 路径 。

而通过jar包来运行时,上面的代码读取的是jar的绝对路径,而jar是一个压缩包,直接读取其包内的绝对路径是有问题的。 也就是会报上面的错误。

问题解决

既然不能通过路径的方式来获取jar中文件,那么该通过何种方式来获取呢?这里有两种写法。

1.通过ClassPathResource获取输入流的方式

InputStream is = new ClassPathResource("img/qr_code-icon.png").getInputStream();

2.通过getResourceAsStream方法获取输入流

InputStream is = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("img/qr_code-icon.png");

可以看出上面两种都是直接获取文件流的方式获取文件,那么问题来了,为啥这种方式可以呢?因为在jar文件中不能直接通过资源路径的方式获取文件,但是可以在jar包中拿到文件流。

测试结果

总结

本文从SpringBoot打成jar之后不能读取classpath下文件说起,介绍了为啥打成jar之后不能通过路径的方式访问classpath下的文件,接着说明了如何处理这个问题,最后介绍了通过流的方式来处理这个问题。

结尾彩蛋

1.Thread.currentThread().getContextClassLoader().getResource("").getPath() 这种写法在通过War运行的项目(比如一个Sping MVC项目)中获取classpath下的文件有没有问题呢?欢迎知道的小伙伴积极留言。

2.通过ResouceUtils.getFile()的方式能不能获取到classpath下文件呢?

源码

@RestController
public class ImgDownloadController {
    private static final Logger LOGGER = LoggerFactory.getLogger(ImgDownloadController.class);
    /**
     * 图片下载接口
     *
     * @param response
     * @Author xiang.wei
     */
    @RequestMapping("/download/image")
    public void errorDownloadImage(HttpServletResponse response) throws IOException {
        //读取文件的路径
        String path = Thread.currentThread().getContextClassLoader().getResource("").getPath()
                + "img/qr_code-icon.png";
        LOGGER.info("qr_code-icon path is: " + path);
        InputStream is = new FileInputStream(new File(path));
        downloadFile(is, "qr_code-icon.png", response);
    }
    @RequestMapping("/correct/download/image1")
    public void correctDownloadImage1(HttpServletResponse response) throws IOException {
        InputStream is = new ClassPathResource("img/qr_code-icon.png").getInputStream();
        downloadFile(is, "qr_code-icon.png", response);
    }
    @RequestMapping("/correct/download/image2")
    public void correctDownloadImage2(HttpServletResponse response) throws IOException {
        InputStream is = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("img/qr_code-icon.png");
        downloadFile(is, "qr_code-icon.png", response);
    }
    private void downloadFile(InputStream is, String name, HttpServletResponse response) throws IOException {
        //设置响应头  "application/octet-stream"
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        response.setHeader("Content-Disposition", "attachment;filename=" + java.net.URLDecoder.decode(name, "ISO-8859-1"));
        //输出流自动关闭
        try (OutputStream os = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
                os.flush();
            }
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }
}


相关文章
|
11天前
|
XML Java Maven
springboot-多环境配置文件
本文介绍了如何创建开发和生产环境的配置文件,并在IDEA和Maven中进行配置。开发环境中,通过设置profile为`dev`来指定配置文件;生产环境中,使用Maven命令参数`-Pprod`打包并指定配置文件。公共配置可放在`application.yml`中统一管理。日志配置需确保`logback-spring.xml`中的profile正确,以保证日志正常输出。
springboot-多环境配置文件
|
1月前
|
XML Java 应用服务中间件
SpringBoot-打包&部署
SpringBoot 项目支持两种打包方式:WAR 包和 JAR 包。JAR 包内置 Tomcat,可直接运行;WAR 包需部署在外部 Tomcat 上。JAR 包通过 `mvn clean package` 打包并用 `java -jar` 运行,支持后台运行和 JVM 参数配置。WAR 包需修改 pom.xml 为 war 类型,移除嵌入式 Tomcat 依赖,添加 servlet-api,并继承 `SpringBootServletInitializer`。配置文件可通过外部 application.yml 覆盖,默认优先级高于 JAR 内部配置。
81 17
SpringBoot-打包&部署
|
1月前
|
存储 前端开发 Java
Springboot静态资源映射及文件映射
在Spring Boot项目中,为了解决前端访问后端存储的图片问题,起初尝试通过静态资源映射实现,但发现这种方式仅能访问打包时已存在的文件。对于动态上传的图片(如头像),需采用资源映射配置,将特定路径映射到服务器上的文件夹,确保新上传的图片能即时访问。例如,通过`addResourceHandler(&quot;/img/**&quot;).addResourceLocations(&quot;file:E:\\myProject\\forum_server\\&quot;)`配置,使前端可通过URL直接访问图片。
Springboot静态资源映射及文件映射
|
24天前
|
Java 应用服务中间件 Maven
SpringBoot项目打包成war包
通过上述步骤,我们成功地将一个Spring Boot应用打包成WAR文件,并部署到外部的Tomcat服务器中。这种方式适用于需要与传统Servlet容器集成的场景。
42 8
|
2月前
|
Java 测试技术 应用服务中间件
Spring Boot 如何测试打包部署
本文介绍了 Spring Boot 项目的开发、调试、打包及投产上线的全流程。主要内容包括: 1. **单元测试**:通过添加 `spring-boot-starter-test` 包,使用 `@RunWith(SpringRunner.class)` 和 `@SpringBootTest` 注解进行测试类开发。 2. **集成测试**:支持热部署,通过添加 `spring-boot-devtools` 实现代码修改后自动重启。 3. **投产上线**:提供两种部署方案,一是打包成 jar 包直接运行,二是打包成 war 包部署到 Tomcat 服务器。
55 10
|
2月前
|
Java 应用服务中间件
SpringBoot工程打包部署
SpringBoot工程打包部署简介:SpringBoot项目可通过三种方式运行:可执行Jar包、可执行War包和标准War包。其中,可执行Jar/War包可独立运行,标准War包需部署在Tomcat中。具体步骤包括:1. 修改pom.xml添加构建依赖;2. 执行`mvn clean package`命令打包;3. 运行生成的Jar/War包(如`java -jar xxx.jar`)。对于标准War包,还需修改启动类并配置Tomcat依赖。
|
4月前
|
Java 应用服务中间件
SpringBoot获取项目文件的绝对路径和相对路径
SpringBoot获取项目文件的绝对路径和相对路径
207 1
SpringBoot获取项目文件的绝对路径和相对路径
|
5月前
|
XML Java Kotlin
springboot + minio + kkfile实现文件预览
本文介绍了如何在容器中安装和启动kkfileviewer,并通过Spring Boot集成MinIO实现文件上传与预览功能。首先,通过下载kkfileviewer源码并构建Docker镜像来部署文件预览服务。接着,在Spring Boot项目中添加MinIO依赖,配置MinIO客户端,并实现文件上传与获取预览链接的接口。最后,通过测试验证文件上传和预览功能的正确性。
338 4
springboot + minio + kkfile实现文件预览
|
4月前
|
网络协议 Java
springboot配置hosts文件
springboot配置hosts文件
79 11
|
4月前
|
存储 前端开发 JavaScript