编写下载服务器 - 第六部分:描述您发送的内容

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

就 HTTP 而言,客户端下载的只是一堆字节。然而,客户真的很想知道如何解释这些字节。是图像吗?或者可能是 ZIP 文件?本系列的最后一部分描述了如何向客户提示她下载的内容。


设置内容类型响应头

内容类型描述返回的资源的 MIME 类型 。此标头指示 Web 浏览器如何处理从下载服务器流出的字节流。没有这个标头,浏览器就不知道它实际收到了什么,只会像文本文件一样显示内容。不用说二进制 PDF(见上面的屏幕截图),像文本文件一样显示的图像或视频看起来不太好。最难的部分是以某种方式实际获取媒体类型。幸运的是,Java 本身有一个工具可以根据资源的扩展名和/或内容来猜测媒体类型:


 import com.google.common.net.MediaType;
import java.io.*;
import java.time.Instant;

public class FileSystemPointer implements FilePointer {

private final MediaType mediaTypeOrNull;

public FileSystemPointer(File target) {
    final String contentType = java.nio.file.Files.probeContentType(target.toPath());
    this.mediaTypeOrNull = contentType != null ?
            MediaType.parse(contentType) :
            null;
}



请注意,将 Optional<T> 用作类字段不是惯用的,因为它不是可序列化的,我们避免了潜在的问题。知道媒体类型后,我们必须在响应中返回它。请注意,这一小段代码同时使用了 JDK 8 和 Guava 中的 Optional,以及 Spring 框架和 Guava 中的 MediaType 类。类型系统真是一团糟!



 import com.google.common.net.MediaType;
import java.io.*;
import java.time.Instant;

public class FileSystemPointer implements FilePointer {

private final MediaType mediaTypeOrNull;

public FileSystemPointer(File target) {
    final String contentType = java.nio.file.Files.probeContentType(target.toPath());
    this.mediaTypeOrNull = contentType != null ?
            MediaType.parse(contentType) :
            null;
}


保留原始文件名和扩展名

当您直接在 Web 浏览器中打开文档时,Content-type 效果很好,但假设您的用户将此文档存储在磁盘上。浏览器是否决定显示或存储下载的文件超出了本文的范围 - 但我们应该为两者做好准备。如果浏览器只是将文件存储在磁盘上,则它必须将其保存在某个名称下。默认情况下,Firefox 将使用 URL 的最后一部分,在我们的例子中恰好是资源的 UUID。不是很用户友好。 Chrome 要好一些——从 Content-type 标头中知道 MIME 类型,它将试探性地添加适当的扩展名,例如 .zip 在 application/zip 的情况下。但是文件名仍然是一个随机的 UUID,而用户上传的文件可能是 cats.zip。因此,如果您的目标是浏览器而不是自动客户端,则最好使用真实姓名作为 URL 的最后一部分。我们仍然希望使用 UUID 来区分内部资源,避免冲突并且不暴露我们内部的存储结构。但在外部我们可以重定向到用户友好的 URL,但为了安全保留 UUID。首先我们需要一个额外的端点:


 import com.google.common.net.MediaType;
import java.io.*;
import java.time.Instant;

public class FileSystemPointer implements FilePointer {

private final MediaType mediaTypeOrNull;

public FileSystemPointer(File target) {
    final String contentType = java.nio.file.Files.probeContentType(target.toPath());
    this.mediaTypeOrNull = contentType != null ?
            MediaType.parse(contentType) :
            null;
}



如果你仔细观察,甚至没有使用 {filename},它只是浏览器的一个提示。如果您想要额外的安全性,您可以将提供的文件名与映射到给定 UUID 的文件名进行比较。这里真正重要的是,只要询问 UUID 就会重定向我们:



 import com.google.common.net.MediaType;
import java.io.*;
import java.time.Instant;

public class FileSystemPointer implements FilePointer {

private final MediaType mediaTypeOrNull;

public FileSystemPointer(File target) {
    final String contentType = java.nio.file.Files.probeContentType(target.toPath());
    this.mediaTypeOrNull = contentType != null ?
            MediaType.parse(contentType) :
            null;
}



而且您需要一次额外的网络访问来获取实际文件:


 import com.google.common.net.MediaType;
import java.io.*;
import java.time.Instant;

public class FileSystemPointer implements FilePointer {

private final MediaType mediaTypeOrNull;

public FileSystemPointer(File target) {
    final String contentType = java.nio.file.Files.probeContentType(target.toPath());
    this.mediaTypeOrNull = contentType != null ?
            MediaType.parse(contentType) :
            null;
}



实现很简单,但进行了一些重构以避免重复:


 import com.google.common.net.MediaType;
import java.io.*;
import java.time.Instant;

public class FileSystemPointer implements FilePointer {

private final MediaType mediaTypeOrNull;

public FileSystemPointer(File target) {
    final String contentType = java.nio.file.Files.probeContentType(target.toPath());
    this.mediaTypeOrNull = contentType != null ?
            MediaType.parse(contentType) :
            null;
}


您甚至可以进一步使用高阶函数来避免重复:



 import com.google.common.net.MediaType;
import java.io.*;
import java.time.Instant;

public class FileSystemPointer implements FilePointer {

private final MediaType mediaTypeOrNull;

public FileSystemPointer(File target) {
    final String contentType = java.nio.file.Files.probeContentType(target.toPath());
    this.mediaTypeOrNull = contentType != null ?
            MediaType.parse(contentType) :
            null;
}


显然,一个额外的重定向是每次下载必须支付的额外费用,因此这是一种权衡。您可以考虑基于用户代理的启发式方法(如果是浏览器则重定向,如果是自动客户端则直接服务器)以避免在非人类客户端的情况下重定向。我们关于文件下载的系列到此结束。 HTTP/2 的出现肯定会带来更多的改进和技术,比如优先级。


编写下载服务器

GitHub 上提供了贯穿这些文章开发的 示例应用程序