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-Type和https://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é)蘸鲸,可以參考
- HTTP 緩存(在MDN上)
- Hypertext Transfer Protocol (HTTP/1.1): Caching
- HTTP Cache-Control Extensions for Stale Content
而在下載在實(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)求流程如下
- 瀏覽器發(fā)送請(qǐng)求残制,服務(wù)器響應(yīng)資源立砸,并在響應(yīng)頭中返回Cache-Control,ETags,Last-Modified
- 當(dāng)瀏覽器再次訪問同一資源時(shí),瀏覽器首先檢測(cè)本地已經(jīng)存在的資源的Cache-Control,確定本地資源是否過期初茶,如果沒有過期颗祝,則直接使用本地已經(jīng)下載的資源
- 如果緩存已經(jīng)過期,瀏覽器重新發(fā)起請(qǐng)求,并附加該資源的請(qǐng)求頭Etag,If-Modified-Since螺戳。
- 服務(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();
}
}
}
以上代碼主要引入了如下變化
- 參數(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ù)器類型浅妆。
- 在返回請(qǐng)求體前增加了checkNotModified校驗(yàn)望迎。這樣當(dāng)資源沒有改變時(shí),會(huì)直接返回http 304.
- 在響應(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è)面上。
持續(xù)刷新頁(yè)面,觀察后續(xù)的圖片請(qǐng)求玻淑,會(huì)發(fā)現(xiàn)它的響應(yīng)頭為來自內(nèi)存緩存.
等待一分鐘之后,再刷新瀏覽器嗽冒,這時(shí)服務(wù)端會(huì)返回狀態(tài)碼304,告訴瀏覽器圖片沒有發(fā)生改變。
至此一個(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)文件下載
歡迎吐槽