就 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 的出现肯定会带来更多的改进和技术,比如优先级。
编写下载服务器
- 第一部分:始终流式传输,永远不会完全保留在内存中
- 第二部分:标头:Last-Modified、ETag 和 If-None-Match
- 第三部分:标题:内容长度和范围
-
第四部分:实现
HEAD
操作(高效) - 第五部分:节流下载速度
- 第六部分:描述您发送的内容(内容类型等)
GitHub 上提供了贯穿这些文章开发的 示例应用程序 。