编写下载服务器。 第四部分:有效地实现HEAD操作

本文深入探讨了HTTP的HEAD方法,解释了其与GET的区别,以及如何在Spring MVC中高效地实现它。HEAD方法用于检查资源的存在和版本,避免不必要的数据传输。文章提供了具体的代码示例,展示了如何在下载服务器中正确处理HEAD请求,包括状态码、响应头和无主体的响应。

HEAD是一个经常被遗忘的HTTP方法(动词),其行为类似于GET,但不会返回正文。 您使用HEAD来检查资源的存在(如果不存在,它应该返回404),并确保您的缓存中没有陈旧的版本。 在这种情况下,您期望304 Not Modified ,而200表示服务器具有最新版本。 例如,您可以使用HEAD有效地实施软件更新。 在这种情况下, ETag是您的应用程序版本(生成,标记,提交哈希),并且您具有固定的/most_recent端点。 您的软件会在ETag发送具有当前版本的HEAD请求。 如果没有更新,服务器将以304答复。如果为200,则可以询问用户是否要升级而无需下载软件。 最终请求GET /most_recent将始终下载您软件的最新版本。 HTTP的力量!

在servlet中,默认情况下, HEAD是在doHead() ,您应该重写它。 默认实现只是将委派给GET但会丢弃主体。 这效率不高,尤其是当您从外部(例如Amazon S3)加载资源时。 幸运的是(?)Spring MVC默认情况下不实现HEAD,因此您必须手动执行。 让我们从HEAD的一些集成测试开始:

def 'should return 200 OK on HEAD request, but without body'() {
    expect:
        mockMvc
            .perform(
                head('/download/' + FileExamples.TXT_FILE_UUID))
            .andExpect(
                    status().isOk())
            .andExpect(
                    content().bytes(new byte[0]))
}
 
def 'should return 304 on HEAD request if we have cached version'() {
    expect:
        mockMvc
            .perform(
                head('/download/' + FileExamples.TXT_FILE_UUID)
                        .header(IF_NONE_MATCH, FileExamples.TXT_FILE.getEtag()))
            .andExpect(
                status().isNotModified())
            .andExpect(
                header().string(ETAG, FileExamples.TXT_FILE.getEtag()))
}
 
def 'should return Content-length header'() {
    expect:
        mockMvc
            .perform(
                head('/download/' + FileExamples.TXT_FILE_UUID))
            .andExpect(
                status().isOk())
            .andExpect(
                header().longValue(CONTENT_LENGTH, FileExamples.TXT_FILE.size))
}

实际的实现非常简单,但是需要一些重构以避免重复。 下载端点现在接受GET和HEAD:

@RequestMapping(method = {GET, HEAD}, value = "/{uuid}")
public ResponseEntity<Resource> download(
        HttpMethod method,
        @PathVariable UUID uuid,
        @RequestHeader(IF_NONE_MATCH) Optional<String> requestEtagOpt,
        @RequestHeader(IF_MODIFIED_SINCE) Optional<Date> ifModifiedSinceOpt
        ) {
    return storage
            .findFile(uuid)
            .map(pointer -> new ExistingFile(method, pointer))
            .map(file -> file.handle(requestEtagOpt, ifModifiedSinceOpt))
            .orElseGet(() -> new ResponseEntity<>(NOT_FOUND));
}

我创建了一个新的抽象ExistingFile ,它封装了我们在其上调用的找到的FilePointer和HTTP动词。 ExistingFile.handle()具有通过HEAD提供文件或仅提供元数据所需的一切:

public class ExistingFile {
 
    private static final Logger log = LoggerFactory.getLogger(ExistingFile.class);
 
    private final HttpMethod method;
    private final FilePointer filePointer;
 
    public ExistingFile(HttpMethod method, FilePointer filePointer) {
        this.method = method;
        this.filePointer = filePointer;
    }
 
    public ResponseEntity<Resource> handle(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {
        if (requestEtagOpt.isPresent()) {
            final String requestEtag = requestEtagOpt.get();
            if (filePointer.matchesEtag(requestEtag)) {
                return notModified(filePointer);
            }
        }
        if (ifModifiedSinceOpt.isPresent()) {
            final Instant isModifiedSince = ifModifiedSinceOpt.get().toInstant();
            if (filePointer.modifiedAfter(isModifiedSince)) {
                return notModified(filePointer);
            }
        }
        return serveDownload(filePointer);
    }
 
    private ResponseEntity<Resource> serveDownload(FilePointer filePointer) {
        log.debug("Serving {} '{}'", method, filePointer);
        final InputStreamResource resource = resourceToReturn(filePointer);
        return response(filePointer, OK, resource);
    }
 
    private InputStreamResource resourceToReturn(FilePointer filePointer) {
        if (method == HttpMethod.GET)
            return buildResource(filePointer);
        else
            return null;
    }
 
    private InputStreamResource buildResource(FilePointer filePointer) {
        final InputStream inputStream = filePointer.open();
        return new InputStreamResource(inputStream);
    }
 
    private ResponseEntity<Resource> notModified(FilePointer filePointer) {
        log.trace("Cached on client side {}, returning 304", filePointer);
        return response(filePointer, NOT_MODIFIED, null);
    }
 
    private ResponseEntity<Resource> response(FilePointer filePointer, HttpStatus status, Resource body) {
        return ResponseEntity
                .status(status)
                .eTag(filePointer.getEtag())
                .lastModified(filePointer.getLastModified().toEpochMilli())
                .body(body);
    }
 
}

resourceToReturn()至关重要。 如果返回null ,Spring MVC将不包含任何主体作为响应。 其他所有内容均保持不变(响应标头等)

编写下载服务器

翻译自: https://www.javacodegeeks.com/2015/07/writing-a-download-server-part-iv-implement-head-operation-efficiently.html

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值