IT必备技能程序员从入门到放弃

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

2020-08-23  本文已影响0人  虾游于海

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();
        }
    }
}

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

HTTP缓存

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

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

增加了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设置,以便浏览器可以针对这些响应头实现缓存策略.

缓存测试

<!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

欢迎吐槽

上一篇下一篇

猜你喜欢

热点阅读