此文要從SpringBoot打包后不能讀取classpath下文件說(shuō)起

文章目錄

    問(wèn)題復(fù)現(xiàn)
    問(wèn)題思考
    問(wèn)題解決
    測(cè)試結(jié)果
    總結(jié)
    結(jié)尾彩蛋
    源碼

問(wèn)題復(fù)現(xiàn)

事情是這樣的,昨天快下班了時(shí)候,測(cè)試小姐姐突然說(shuō)(PS: 有些測(cè)試小姐姐上班的時(shí)候不提啥BUG,下班的時(shí)候給你提一堆BUG,不知道大家有沒(méi)有這種感覺(jué))。
在這里插入圖片描述

有個(gè)圖片下載的接口不能用了。害,本想6點(diǎn)鐘下班走人的我瞬間懵逼了,這下走不了。老規(guī)矩,先查錯(cuò)誤日志。
還是那個(gè)熟悉的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 (文件名、目錄名或卷標(biāo)語(yǔ)法不正確。)
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.(Unknown Source) ~[na:1.8.0_60]
at com.jay.ImgDownloadController.downloadImage(ImgDownloadController.java:35) ~[classes!/:0.0.1-SNAPSHOT]

很明顯這是一個(gè)無(wú)效的文件路徑,根據(jù)這個(gè)錯(cuò)誤路徑程序肯定不能找到指定的文件咯。
再回頭定位到報(bào)錯(cuò)的代碼

//讀取文件的路徑
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));

這段代碼在本地調(diào)試的時(shí)候明明是沒(méi)有問(wèn)題的呀,單元測(cè)試都跑過(guò)了,本地調(diào)試時(shí)輸入的路徑如下:。

打印的路徑地址是:D:/workspace/file_download_demo/target/classes/img/qr_code-icon.png
這個(gè)路徑是一個(gè)有效的路徑。
這又是一個(gè)我本地明明沒(méi)問(wèn)題,到服務(wù)器就有問(wèn)題了。鍋是甩不出去了
問(wèn)題思考

我們都知道JAVA是一門靜態(tài)語(yǔ)言,先編譯再運(yùn)行也就是先將java文件編譯成class文件,然后在用虛擬機(jī)來(lái)執(zhí)行class文件的。SpringBoot在編譯打包后會(huì)生成target目錄,class文件,資源文件還有jar包都會(huì)被放在這個(gè)目錄下。如下圖所示:

其中所有的class文件以及資源文件都放在了classes文件夾中。在本地運(yùn)行時(shí) Thread.currentThread().getContextClassLoader().getResource("").getPath()
其中Thread.currentThread().getContextClassLoader()返回的是當(dāng)前線程的類加載器(默認(rèn)是AppClassLoader類加載器),類加載器可以加載類也可以加載資源。類加載器有很多,具體可以參考雙親委派模型以及SpringFactoriesLoader詳解(最全最簡(jiǎn)單的介紹)
讀取到的路徑是D:/workspace/file_download_demo/target/classes,classes文件夾所在的路徑也就是我們熟悉的classpath 路徑 。
而通過(guò)jar包來(lái)運(yùn)行時(shí),上面的代碼讀取的是jar的絕對(duì)路徑,而jar是一個(gè)壓縮包,直接讀取其包內(nèi)的絕對(duì)路徑是有問(wèn)題的。 也就是會(huì)報(bào)上面的錯(cuò)誤。
問(wèn)題解決

既然不能通過(guò)路徑的方式來(lái)獲取jar中文件,那么該通過(guò)何種方式來(lái)獲取呢?這里有兩種寫法。

通過(guò)ClassPathResource獲取輸入流的方式

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

1

通過(guò)getResourceAsStream方法獲取輸入流

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

可以看出上面兩種都是直接獲取文件流的方式獲取文件,那么問(wèn)題來(lái)了,為啥這種方式可以呢?因?yàn)樵趈ar文件中不能直接通過(guò)資源路徑的方式獲取文件,但是可以在jar包中拿到文件流。
在這里插入圖片描述
測(cè)試結(jié)果

總結(jié)

本文從SpringBoot打成jar之后不能讀取classpath下文件說(shuō)起,介紹了為啥打成jar之后不能通過(guò)路徑的方式訪問(wèn)classpath下的文件,接著說(shuō)明了如何處理這個(gè)問(wèn)題,最后介紹了通過(guò)流的方式來(lái)處理這個(gè)問(wèn)題。
結(jié)尾彩蛋

Thread.currentThread().getContextClassLoader().getResource("").getPath() 這種寫法在通過(guò)War運(yùn)行的項(xiàng)目(比如一個(gè)Sping MVC項(xiàng)目)中獲取classpath下的文件有沒(méi)有問(wèn)題呢?歡迎知道的小伙伴積極留言。
通過(guò)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 {
    //設(shè)置響應(yīng)頭  "application/octet-stream"
    response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
    response.setHeader("Content-Disposition", "attachment;filename=" + java.net.URLDecoder.decode(name, "ISO-8859-1"));
    //輸出流自動(dòng)關(guān)閉
    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();
        }
    }
}

}





作者:碼農(nóng)飛哥
微信公眾號(hào):碼農(nóng)飛哥