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

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

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 63w+ 字,讲解图 2808+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2200+ 小伙伴加入学习 ,欢迎点击围观

就 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 上提供了贯穿这些文章开发的 示例应用程序

相关文章