spring boot 文件下载的预览和缓存

Spring boot实现上传文件的预览和http缓存

续前节文件的简单上传和下载

如何实现图片在浏览器中的显示

在之前的简单示例中,实现了文件的上传和下载,但随之而来的另外一个问题发生了。
我向服务器上传了一个图片,然后在浏览器中输入相应的下载链接,会发现文件直接被下载到了本地,而当我们使用其它静态服务器,或者spring-boot/tomcat/apache server的静态资源时,我们输入对应的图片地址,浏览器会直接将图片显示出来,而不是下载到本地。
为什么会出现这样的差异呢?这涉及到http的Content-Type响应头。
该请求头用于指示资源的MIME类型 media type 。浏览器会根据不同的响应类型,来判定如何处理响应。比如当检测到响应头为text/html时,浏览器会执行html渲染,当检测到该响应头为video/mp4时,会执行视频播放。
更详细的响应头可以参考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

自定义响应头

在代码中如何定义Content-Type响应头?其实在前面的代码中已经有过示例。

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

.contentType()方法就是用于指定Content-Type响应头的代码。
我们只需要根据文件类型返回不同的响应头即可。我们重新定义一个controller来实现下载逻辑

@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");
        // ...这里添加更多的扩展名和contentType对应关系
    }

    @GetMapping("{filename}")
    public ResponseEntity<InputStreamSource> download(@PathVariable("filename") String filename)
            throws FileNotFoundException {
        // 构建下载路径
        File target = new File(path + File.separator + filename);
        // 构建响应体
        if (target.exists()) {
            // 获取文件扩展名
            String ext = filename.substring(filename.lastIndexOf(".") + 1);
            // 根据文件扩展名获取mediaType
            String mediaType = mediaTypes.get(ext);
            // 如果没有找到对应的扩展名,使用默认的mediaType
            if (Objects.isNull(mediaType)) {
                mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
            }
            InputStreamSource resource = new FileSystemResource(target);
            return ResponseEntity.ok()
                    // 指定文件的contentType
                    // contentType方法只能支持Spring内置的一些mediaType类型
                    // 但我们会由一些其它的MediaType类型,比如video/mp4等,这时我们需要直接通过字符串设置响应头
                    // .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header("Content-Type", mediaType)
                    .body(resource);
        } else {
            // 如果文件不存在,返回404响应
            return ResponseEntity.notFound().build();
        }
    }
}

代码解析
对比之前的简单上传,其实只修改了两个地方

  • 使用map存储文件扩展名和Content-Type的对应关系
  • 在返回http header的时候,根据不同的扩展名响应不同Content-Type
    浏览器是根据响应头中的Content-Type来确定浏览器行为的。而不是根据文件扩展名。比如请求的地址是http://xxx.xxx.com/video.mp4但响应的Content-Type是text/html,浏览器也会将内容作为html渲染,而不是作为mp4播放(哪怕真实内容不是html).

HTTP缓存

当我们的上传服务会作为图片文件服务器存在时。就会存在文件预览问题。但如果每次浏览都发生实际的服务请求,对服务器的压力是比较大的。这时,http缓存机制就派上了用场。
关于缓存的更多细节,可以参考

而在下载在实现中,通常考虑三组请求头

  • Cache-Control
    Cache-Control是一个通用消息头字段,被用于在http请求和响应中,通过指定指令来实现缓存机制。缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中(以上解析来源于MDN)。
    意思是这个头可以用在请求头上,也可以用在响应头上。请求头可以通过一些指令来要求服务器进行响应的缓存操作。而服务器也可以通过响应头高速浏览器你可以按照我的返回信息进行资源的缓存操作。但这些都不是必须的,也就是说对于浏览器的指令,服务器可以不予理睬。而对于客户端来说,客户端也可以忽略这些指令。
    通常情况下,我们只需要设置服务端响应头的Cache-control就可以达到有效控制浏览器缓存的目的。
  • ETags和If-None-Match
    响应头ETag 系统中对资源的签名,根据http协议,ETag是按字节计算,即当资源中的某个字节发生改变时,Etag也应该随之改变。
    请求头If-None-Match 当浏览器第一次请求某个资源时,如果资源响应包含了ETag响应头,则浏览器会保存该请求头。当浏览器第二次请求该请求该资源时,服务器会校验该值,如果该值和服务器保存的该值一致,则服务器直接返回304状态码,而不返回完整的响应体
  • Last-Modified和If-Modified-Since
    响应头Last-Modified含源头服务器认定的资源做出修改的日期及时间。它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比ETag要低,所以这是一个备用机制
    请求头If-Modified-Since是一个条件式请求头,服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为200。当浏览器第一次请求资源时,会返回Last-Modified响应头。浏览器会保存该值,并在第二次请求该资源时将该值作为If-Modified-Since的值提交到服务器,服务器应该验证该值,如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的304响应。

增加了http缓存模型的请求流程如下

  1. 浏览器发送请求,服务器响应资源,并在响应头中返回Cache-Control,ETags,Last-Modified
  2. 当浏览器再次访问同一资源时,浏览器首先检测本地已经存在的资源的Cache-Control,确定本地资源是否过期,如果没有过期,则直接使用本地已经下载的资源
  3. 如果缓存已经过期,浏览器重新发起请求,并附加该资源的请求头Etag,If-Modified-Since。
  4. 服务器首先对比客户端的Etag,if-Modified-Since,如果Etag值一致,或者服务端资源在if-Modified-Since之后未发生变化。则直接返回http 304状态码,否则返回200状态码,并返回完整的内容和新的Etag和Last-Modified值。注意:Etag,if-Modified-Since两个头同时存在时,服务器应该忽略if-Modified-Since值。

针对以上的流程,我们重新改造一下之前的服务器端代码。
以下是完整的代码清单

/**
 * 增加了缓存的下载
 * 
 * @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 {
        // 构建下载路径
        File target = new File(path + File.separator + filename);
        // 构建响应体
        if (target.exists()) {
            // 获取文件的最后修改时间
            long lastModified = target.lastModified();
            if (request.checkNotModified(lastModified)) {
                return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
            }
            // 获取文件扩展名
            String ext = filename.substring(filename.lastIndexOf(".") + 1);
            // 根据文件扩展名获取mediaType
            String mediaType = mediaTypes.get(ext);
            // 如果没有找到对应的扩展名,使用默认的mediaType
            if (Objects.isNull(mediaType)) {
                mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
            }
            InputStreamSource resource = new FileSystemResource(target);
            return ResponseEntity.ok()
                    // 指定文件的缓存时间,这里指定60秒,高速浏览器在60秒之内不用重新请求
                    .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
                    // 返回文件的最后修改时间
                    .lastModified(lastModified)
                    // 指定文件的contentType
                    // contentType方法只能支持Spring内置的一些mediaType类型
                    // 但我们会由一些其它的MediaType类型,比如video/mp4等,这时我们需要直接通过字符串设置响应头
                    // .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .header("Content-Type", mediaType)
                    .body(resource);
        } else {
            // 如果文件不存在,返回404响应
            return ResponseEntity.notFound().build();
        }
    }
}

以上代码主要引入了如下变化

  1. 参数中引入了WebRequest,它包装了一些通用的请求信息,在某些文章中可能会使用HttpServletRequest来获取相关的信息,但对于应用来说,一般不建议这么做,因为现在Spring的webflux技术,可能在我们的服务端中不会存在HttpServletRequest,而WebRequest是针对web请求的一个通用封装,并不依赖于特定的服务器类型。
  2. 在返回请求体前增加了checkNotModified校验。这样当资源没有改变时,会直接返回http 304.
  3. 在响应头中增加了cacheControl和lastModified设置,以便浏览器可以针对这些响应头实现缓存策略.

缓存测试

  • 为了测试缓存是否生效,我们首先上传一个图片到指定位置(其实直接复制一个图片到上传目标文件夹即可)。
  • 在src\main\resources\static中新建一个简单的index.html,使用img标签引入我们上传的文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <img src="/files3/123.png">
</body>
</html>

启动服务器,并启动浏览器,按f12打开开发者窗口,打开网络栏,输入http://localhost:8080/,观察对 files3/123.png的请求

第一次请求时,会向服务器请求图片,服务器返回200请求,并将图片渲染到页面上。

1.png

持续刷新页面,观察后续的图片请求,会发现它的响应头为来自内存缓存.
2.png

等待一分钟之后,再刷新浏览器,这时服务端会返回状态码304,告诉浏览器图片没有发生改变。
3.png

至此一个简单的缓存逻辑就做好了。
在本示例中没有计算Etag,但基本逻辑就是使用某种算法计算资源的hash值(或其它特征值),在响应的时候将etag值返回给浏览器。而在返回响应体时,会先行校验一个客户端的传入值是否和服务器当前资源的特征值是否一致,如果一致,就返回304.可以自行尝试一下。

项目代码
https://github.com/ldwqh0/file-uploader

相关文章
spring boot 三两行代码实现文件的上传和下载
spring boot 文件下载的预览和缓存
Spring配合Nginx实现文件下载

欢迎吐槽

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342