spring boot 文件下載的預(yù)覽和緩存

Spring boot實(shí)現(xiàn)上傳文件的預(yù)覽和http緩存

續(xù)前節(jié)文件的簡(jiǎn)單上傳和下載

如何實(shí)現(xiàn)圖片在瀏覽器中的顯示

在之前的簡(jiǎn)單示例中据途,實(shí)現(xiàn)了文件的上傳和下載卧檐,但隨之而來的另外一個(gè)問題發(fā)生了些楣。
我向服務(wù)器上傳了一個(gè)圖片系忙,然后在瀏覽器中輸入相應(yīng)的下載鏈接蝴簇,會(huì)發(fā)現(xiàn)文件直接被下載到了本地援雇,而當(dāng)我們使用其它靜態(tài)服務(wù)器裕循,或者spring-boot/tomcat/apache server的靜態(tài)資源時(shí),我們輸入對(duì)應(yīng)的圖片地址娇昙,瀏覽器會(huì)直接將圖片顯示出來尺迂,而不是下載到本地。
為什么會(huì)出現(xiàn)這樣的差異呢?這涉及到http的Content-Type響應(yīng)頭枪狂。
該請(qǐng)求頭用于指示資源的MIME類型 media type 危喉。瀏覽器會(huì)根據(jù)不同的響應(yīng)類型宋渔,來判定如何處理響應(yīng)州疾。比如當(dāng)檢測(cè)到響應(yīng)頭為text/html時(shí),瀏覽器會(huì)執(zhí)行html渲染皇拣,當(dāng)檢測(cè)到該響應(yīng)頭為video/mp4時(shí)严蓖,會(huì)執(zhí)行視頻播放。
更詳細(xì)的響應(yīng)頭可以參考https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Typehttps://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types

自定義響應(yīng)頭

在代碼中如何定義Content-Type響應(yīng)頭氧急?其實(shí)在前面的代碼中已經(jīng)有過示例颗胡。

  return ResponseEntity.ok()
                    // 指定文件的contentType
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .body(resource);

.contentType()方法就是用于指定Content-Type響應(yīng)頭的代碼。
我們只需要根據(jù)文件類型返回不同的響應(yīng)頭即可吩坝。我們重新定義一個(gè)controller來實(shí)現(xiàn)下載邏輯

@RequestMapping("files2")
@RestController
public class FileController2 {

    private String path = "d:" + File.separator + "uploader";

    private final Map<String, String> mediaTypes;

    public FileController2() {
        mediaTypes = new HashMap<String, String>();
        mediaTypes.put("mp4", "video/mp4");
        mediaTypes.put("jpeg", "image/jpeg");
        // ...這里添加更多的擴(kuò)展名和contentType對(duì)應(yīng)關(guān)系
    }

    @GetMapping("{filename}")
    public ResponseEntity<InputStreamSource> download(@PathVariable("filename") String filename)
            throws FileNotFoundException {
        // 構(gòu)建下載路徑
        File target = new File(path + File.separator + filename);
        // 構(gòu)建響應(yīng)體
        if (target.exists()) {
            // 獲取文件擴(kuò)展名
            String ext = filename.substring(filename.lastIndexOf(".") + 1);
            // 根據(jù)文件擴(kuò)展名獲取mediaType
            String mediaType = mediaTypes.get(ext);
            // 如果沒有找到對(duì)應(yīng)的擴(kuò)展名毒姨,使用默認(rèn)的mediaType
            if (Objects.isNull(mediaType)) {
                mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
            }
            InputStreamSource resource = new FileSystemResource(target);
            return ResponseEntity.ok()
                    // 指定文件的contentType
                    // contentType方法只能支持Spring內(nèi)置的一些mediaType類型
                    // 但我們會(huì)由一些其它的MediaType類型,比如video/mp4等钉寝,這時(shí)我們需要直接通過字符串設(shè)置響應(yīng)頭
                    // .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header("Content-Type", mediaType)
                    .body(resource);
        } else {
            // 如果文件不存在弧呐,返回404響應(yīng)
            return ResponseEntity.notFound().build();
        }
    }
}

代碼解析
對(duì)比之前的簡(jiǎn)單上傳,其實(shí)只修改了兩個(gè)地方

  • 使用map存儲(chǔ)文件擴(kuò)展名和Content-Type的對(duì)應(yīng)關(guān)系
  • 在返回http header的時(shí)候嵌纲,根據(jù)不同的擴(kuò)展名響應(yīng)不同Content-Type
    瀏覽器是根據(jù)響應(yīng)頭中的Content-Type來確定瀏覽器行為的俘枫。而不是根據(jù)文件擴(kuò)展名。比如請(qǐng)求的地址是http://xxx.xxx.com/video.mp4但響應(yīng)的Content-Type是text/html,瀏覽器也會(huì)將內(nèi)容作為html渲染逮走,而不是作為mp4播放(哪怕真實(shí)內(nèi)容不是html).

HTTP緩存

當(dāng)我們的上傳服務(wù)會(huì)作為圖片文件服務(wù)器存在時(shí)鸠蚪。就會(huì)存在文件預(yù)覽問題。但如果每次瀏覽都發(fā)生實(shí)際的服務(wù)請(qǐng)求师溅,對(duì)服務(wù)器的壓力是比較大的茅信。這時(shí),http緩存機(jī)制就派上了用場(chǎng)墓臭。
關(guān)于緩存的更多細(xì)節(jié)蘸鲸,可以參考

而在下載在實(shí)現(xiàn)中,通称鸨悖考慮三組請(qǐng)求頭

  • Cache-Control
    Cache-Control是一個(gè)通用消息頭字段棚贾,被用于在http請(qǐng)求和響應(yīng)中,通過指定指令來實(shí)現(xiàn)緩存機(jī)制榆综。緩存指令是單向的妙痹,這意味著在請(qǐng)求中設(shè)置的指令,不一定被包含在響應(yīng)中(以上解析來源于MDN)鼻疮。
    意思是這個(gè)頭可以用在請(qǐng)求頭上怯伊,也可以用在響應(yīng)頭上。請(qǐng)求頭可以通過一些指令來要求服務(wù)器進(jìn)行響應(yīng)的緩存操作判沟。而服務(wù)器也可以通過響應(yīng)頭高速瀏覽器你可以按照我的返回信息進(jìn)行資源的緩存操作耿芹。但這些都不是必須的崭篡,也就是說對(duì)于瀏覽器的指令,服務(wù)器可以不予理睬吧秕。而對(duì)于客戶端來說琉闪,客戶端也可以忽略這些指令。
    通常情況下砸彬,我們只需要設(shè)置服務(wù)端響應(yīng)頭的Cache-control就可以達(dá)到有效控制瀏覽器緩存的目的颠毙。
  • ETags和If-None-Match
    響應(yīng)頭ETag 系統(tǒng)中對(duì)資源的簽名,根據(jù)http協(xié)議砂碉,ETag是按字節(jié)計(jì)算蛀蜜,即當(dāng)資源中的某個(gè)字節(jié)發(fā)生改變時(shí),Etag也應(yīng)該隨之改變增蹭。
    請(qǐng)求頭If-None-Match 當(dāng)瀏覽器第一次請(qǐng)求某個(gè)資源時(shí)滴某,如果資源響應(yīng)包含了ETag響應(yīng)頭,則瀏覽器會(huì)保存該請(qǐng)求頭滋迈。當(dāng)瀏覽器第二次請(qǐng)求該請(qǐng)求該資源時(shí)霎奢,服務(wù)器會(huì)校驗(yàn)該值,如果該值和服務(wù)器保存的該值一致杀怠,則服務(wù)器直接返回304狀態(tài)碼椰憋,而不返回完整的響應(yīng)體
  • Last-Modified和If-Modified-Since
    響應(yīng)頭Last-Modified含源頭服務(wù)器認(rèn)定的資源做出修改的日期及時(shí)間。它通常被用作一個(gè)驗(yàn)證器來判斷接收到的或者存儲(chǔ)的資源是否彼此一致赔退。由于精確度比ETag要低橙依,所以這是一個(gè)備用機(jī)制
    請(qǐng)求頭If-Modified-Since是一個(gè)條件式請(qǐng)求頭,服務(wù)器只在所請(qǐng)求的資源在給定的日期時(shí)間之后對(duì)內(nèi)容進(jìn)行過修改的情況下才會(huì)將資源返回硕旗,狀態(tài)碼為200窗骑。當(dāng)瀏覽器第一次請(qǐng)求資源時(shí),會(huì)返回Last-Modified響應(yīng)頭漆枚。瀏覽器會(huì)保存該值创译,并在第二次請(qǐng)求該資源時(shí)將該值作為If-Modified-Since的值提交到服務(wù)器,服務(wù)器應(yīng)該驗(yàn)證該值墙基,如果請(qǐng)求的資源從那時(shí)起未經(jīng)修改软族,那么返回一個(gè)不帶有消息主體的304響應(yīng)。

增加了http緩存模型的請(qǐng)求流程如下

  1. 瀏覽器發(fā)送請(qǐng)求残制,服務(wù)器響應(yīng)資源立砸,并在響應(yīng)頭中返回Cache-Control,ETags,Last-Modified
  2. 當(dāng)瀏覽器再次訪問同一資源時(shí),瀏覽器首先檢測(cè)本地已經(jīng)存在的資源的Cache-Control,確定本地資源是否過期初茶,如果沒有過期颗祝,則直接使用本地已經(jīng)下載的資源
  3. 如果緩存已經(jīng)過期,瀏覽器重新發(fā)起請(qǐng)求,并附加該資源的請(qǐng)求頭Etag,If-Modified-Since螺戳。
  4. 服務(wù)器首先對(duì)比客戶端的Etag,if-Modified-Since,如果Etag值一致搁宾,或者服務(wù)端資源在if-Modified-Since之后未發(fā)生變化。則直接返回http 304狀態(tài)碼倔幼,否則返回200狀態(tài)碼盖腿,并返回完整的內(nèi)容和新的Etag和Last-Modified值。注意:Etag,if-Modified-Since兩個(gè)頭同時(shí)存在時(shí)凤藏,服務(wù)器應(yīng)該忽略if-Modified-Since值奸忽。

針對(duì)以上的流程堕伪,我們重新改造一下之前的服務(wù)器端代碼揖庄。
以下是完整的代碼清單

/**
 * 增加了緩存的下載
 * 
 * @author LiDong
 *
 */
@RequestMapping("files3")
@RestController
public class FileController3 {

    private String path = "d:" + File.separator + "uploader";

    private final Map<String, String> mediaTypes;

    public FileController3() {
        mediaTypes = new HashMap<String, String>();
        mediaTypes.put("mp4", "video/mp4");
        mediaTypes.put("jpeg", "image/jpeg");
        mediaTypes.put("jpg", "jpg");
        mediaTypes.put("png", "image/png");
    }

    @GetMapping("{filename}")
    public ResponseEntity<InputStreamSource> download(@PathVariable("filename") String filename,
            WebRequest request)
            throws FileNotFoundException {
        // 構(gòu)建下載路徑
        File target = new File(path + File.separator + filename);
        // 構(gòu)建響應(yīng)體
        if (target.exists()) {
            // 獲取文件的最后修改時(shí)間
            long lastModified = target.lastModified();
            if (request.checkNotModified(lastModified)) {
                return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
            }
            // 獲取文件擴(kuò)展名
            String ext = filename.substring(filename.lastIndexOf(".") + 1);
            // 根據(jù)文件擴(kuò)展名獲取mediaType
            String mediaType = mediaTypes.get(ext);
            // 如果沒有找到對(duì)應(yīng)的擴(kuò)展名,使用默認(rèn)的mediaType
            if (Objects.isNull(mediaType)) {
                mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
            }
            InputStreamSource resource = new FileSystemResource(target);
            return ResponseEntity.ok()
                    // 指定文件的緩存時(shí)間欠雌,這里指定60秒蹄梢,高速瀏覽器在60秒之內(nèi)不用重新請(qǐng)求
                    .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
                    // 返回文件的最后修改時(shí)間
                    .lastModified(lastModified)
                    // 指定文件的contentType
                    // contentType方法只能支持Spring內(nèi)置的一些mediaType類型
                    // 但我們會(huì)由一些其它的MediaType類型,比如video/mp4等富俄,這時(shí)我們需要直接通過字符串設(shè)置響應(yīng)頭
                    // .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header("Content-Type", mediaType)
                    .body(resource);
        } else {
            // 如果文件不存在禁炒,返回404響應(yīng)
            return ResponseEntity.notFound().build();
        }
    }
}

以上代碼主要引入了如下變化

  1. 參數(shù)中引入了WebRequest,它包裝了一些通用的請(qǐng)求信息霍比,在某些文章中可能會(huì)使用HttpServletRequest來獲取相關(guān)的信息幕袱,但對(duì)于應(yīng)用來說,一般不建議這么做悠瞬,因?yàn)楝F(xiàn)在Spring的webflux技術(shù)们豌,可能在我們的服務(wù)端中不會(huì)存在HttpServletRequest,而WebRequest是針對(duì)web請(qǐng)求的一個(gè)通用封裝,并不依賴于特定的服務(wù)器類型浅妆。
  2. 在返回請(qǐng)求體前增加了checkNotModified校驗(yàn)望迎。這樣當(dāng)資源沒有改變時(shí),會(huì)直接返回http 304.
  3. 在響應(yīng)頭中增加了cacheControl和lastModified設(shè)置凌外,以便瀏覽器可以針對(duì)這些響應(yīng)頭實(shí)現(xiàn)緩存策略.

緩存測(cè)試

  • 為了測(cè)試緩存是否生效辩尊,我們首先上傳一個(gè)圖片到指定位置(其實(shí)直接復(fù)制一個(gè)圖片到上傳目標(biāo)文件夾即可)。
  • 在src\main\resources\static中新建一個(gè)簡(jiǎn)單的index.html,使用img標(biāo)簽引入我們上傳的文件康辑。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <img src="/files3/123.png">
</body>
</html>

啟動(dòng)服務(wù)器摄欲,并啟動(dòng)瀏覽器,按f12打開開發(fā)者窗口疮薇,打開網(wǎng)絡(luò)欄胸墙,輸入http://localhost:8080/,觀察對(duì) files3/123.png的請(qǐng)求

第一次請(qǐng)求時(shí),會(huì)向服務(wù)器請(qǐng)求圖片惦辛,服務(wù)器返回200請(qǐng)求劳秋,并將圖片渲染到頁(yè)面上。

1.png

持續(xù)刷新頁(yè)面,觀察后續(xù)的圖片請(qǐng)求玻淑,會(huì)發(fā)現(xiàn)它的響應(yīng)頭為來自內(nèi)存緩存.
2.png

等待一分鐘之后,再刷新瀏覽器嗽冒,這時(shí)服務(wù)端會(huì)返回狀態(tài)碼304,告訴瀏覽器圖片沒有發(fā)生改變。
3.png

至此一個(gè)簡(jiǎn)單的緩存邏輯就做好了补履。
在本示例中沒有計(jì)算Etag添坊,但基本邏輯就是使用某種算法計(jì)算資源的hash值(或其它特征值),在響應(yīng)的時(shí)候?qū)tag值返回給瀏覽器箫锤。而在返回響應(yīng)體時(shí)贬蛙,會(huì)先行校驗(yàn)一個(gè)客戶端的傳入值是否和服務(wù)器當(dāng)前資源的特征值是否一致,如果一致谚攒,就返回304.可以自行嘗試一下阳准。

項(xiàng)目代碼
https://github.com/ldwqh0/file-uploader

相關(guān)文章
spring boot 三兩行代碼實(shí)現(xiàn)文件的上傳和下載
spring boot 文件下載的預(yù)覽和緩存
Spring配合Nginx實(shí)現(xiàn)文件下載

歡迎吐槽

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市馏臭,隨后出現(xiàn)的幾起案子野蝇,更是在濱河造成了極大的恐慌,老刑警劉巖括儒,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绕沈,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡帮寻,警方通過查閱死者的電腦和手機(jī)乍狐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來固逗,“玉大人浅蚪,你說我怎么就攤上這事∈阊粒” “怎么了掘鄙?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嗡髓。 經(jīng)常有香客問我操漠,道長(zhǎng),這世上最難降的妖魔是什么饿这? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任浊伙,我火速辦了婚禮,結(jié)果婚禮上长捧,老公的妹妹穿的比我還像新娘嚣鄙。我一直安慰自己,他們只是感情好串结,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布哑子。 她就那樣靜靜地躺著舅列,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卧蜓。 梳的紋絲不亂的頭發(fā)上帐要,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音弥奸,去河邊找鬼榨惠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盛霎,可吹牛的內(nèi)容都是我干的赠橙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼愤炸,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼期揪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起摇幻,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤横侦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后绰姻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡引瀑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年狂芋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片憨栽。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡帜矾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屑柔,到底是詐尸還是另有隱情屡萤,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布掸宛,位于F島的核電站死陆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏唧瘾。R本人自食惡果不足惜措译,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望饰序。 院中可真熱鬧领虹,春花似錦、人聲如沸求豫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至最疆,卻和暖如春均唉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肚菠。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工舔箭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蚊逢。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓层扶,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親烙荷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子镜会,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355