如何在 Spring MVC 中实现 HTTP 字节范围请求

2023-12-01

我的网站上出现视频倒带问题。

我发现了 http 标头的问题。

我当前返回视频的控制器方法:

@RequestMapping(method = RequestMethod.GET, value = "/testVideo")
@ResponseBody
public FileSystemResource testVideo(Principal principal) throws IOException {
   return new FileSystemResource(new File("D:\\oceans.mp4"));
}

如何使用字节范围支持重写以下代码?

P.S.

我见过下面的例子http://balusc.blogspot.in/2009/02/fileservlet-supporting-resume-and.html

但这段代码对我来说看起来很难,我无法理解它。我希望 Spring mvc 的存在更加简单。


支持 http 字节范围的请求在本回答时已开放,但已在 Spring 4.2.RC1 中修复。检查吉拉SPR-10805或者,公关here.

但根据您在问题中链接的代码,Davin Kevin 构建了一个solution这可以满足你的要求。

MultipartFileSender 的代码:

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by kevin on 10/02/15.
 * See full code here : https://github.com/davinkevin/Podcast-Server/blob/d927d9b8cb9ea1268af74316cd20b7192ca92da7/src/main/java/lan/dk/podcastserver/utils/multipart/MultipartFileSender.java
 */
public class MultipartFileSender {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB.
    private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
    private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

    Path filepath;
    HttpServletRequest request;
    HttpServletResponse response;

    public MultipartFileSender() {
    }

    public static MultipartFileSender fromPath(Path path) {
        return new MultipartFileSender().setFilepath(path);
    }

    public static MultipartFileSender fromFile(File file) {
        return new MultipartFileSender().setFilepath(file.toPath());
    }

    public static MultipartFileSender fromURIString(String uri) {
        return new MultipartFileSender().setFilepath(Paths.get(uri));
    }

    //** internal setter **//
    private MultipartFileSender setFilepath(Path filepath) {
        this.filepath = filepath;
        return this;
    }

    public MultipartFileSender with(HttpServletRequest httpRequest) {
        request = httpRequest;
        return this;
    }

    public MultipartFileSender with(HttpServletResponse httpResponse) {
        response = httpResponse;
        return this;
    }

    public void serveResource() throws Exception {
        if (response == null || request == null) {
            return;
        }

        if (!Files.exists(filepath)) {
            logger.error("File doesn't exist at URI : {}", filepath.toAbsolutePath().toString());
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        Long length = Files.size(filepath);
        String fileName = filepath.getFileName().toString();
        FileTime lastModifiedObj = Files.getLastModifiedTime(filepath);

        if (StringUtils.isEmpty(fileName) || lastModifiedObj == null) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            return;
        }
        long lastModified = LocalDateTime.ofInstant(lastModifiedObj.toInstant(), ZoneId.of(ZoneOffset.systemDefault().getId())).toEpochSecond(ZoneOffset.UTC);
        String contentType = "video/mp4";

        // Validate request headers for caching ---------------------------------------------------

        // If-None-Match header should contain "*" or ETag. If so, then return 304.
        String ifNoneMatch = request.getHeader("If-None-Match");
        if (ifNoneMatch != null && HttpUtils.matches(ifNoneMatch, fileName)) {
            response.setHeader("ETag", fileName); // Required in 304.
            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        // If-Modified-Since header should be greater than LastModified. If so, then return 304.
        // This header is ignored if any If-None-Match header is specified.
        long ifModifiedSince = request.getDateHeader("If-Modified-Since");
        if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
            response.setHeader("ETag", fileName); // Required in 304.
            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
            return;
        }

        // Validate request headers for resume ----------------------------------------------------

        // If-Match header should contain "*" or ETag. If not, then return 412.
        String ifMatch = request.getHeader("If-Match");
        if (ifMatch != null && !HttpUtils.matches(ifMatch, fileName)) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }

        // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
        long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
        if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }

        // Validate and process range -------------------------------------------------------------

        // Prepare some variables. The full Range represents the complete file.
        Range full = new Range(0, length - 1, length);
        List<Range> ranges = new ArrayList<>();

        // Validate and process Range and If-Range headers.
        String range = request.getHeader("Range");
        if (range != null) {

            // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return;
            }

            String ifRange = request.getHeader("If-Range");
            if (ifRange != null && !ifRange.equals(fileName)) {
                try {
                    long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
                    if (ifRangeTime != -1) {
                        ranges.add(full);
                    }
                } catch (IllegalArgumentException ignore) {
                    ranges.add(full);
                }
            }

            // If any valid If-Range header, then process each part of byte range.
            if (ranges.isEmpty()) {
                for (String part : range.substring(6).split(",")) {
                    // Assuming a file with length of 100, the following examples returns bytes at:
                    // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                    long start = Range.sublong(part, 0, part.indexOf("-"));
                    long end = Range.sublong(part, part.indexOf("-") + 1, part.length());

                    if (start == -1) {
                        start = length - end;
                        end = length - 1;
                    } else if (end == -1 || end > length - 1) {
                        end = length - 1;
                    }

                    // Check if Range is syntactically valid. If not, then return 416.
                    if (start > end) {
                        response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                        return;
                    }

                    // Add range.                    
                    ranges.add(new Range(start, end, length));
                }
            }
        }

        // Prepare and initialize response --------------------------------------------------------

        // Get content type by file name and set content disposition.
        String disposition = "inline";

        // If content type is unknown, then set the default value.
        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
        // To add new content types, add new mime-mapping entry in web.xml.
        if (contentType == null) {
            contentType = "application/octet-stream";
        } else if (!contentType.startsWith("image")) {
            // Else, expect for images, determine content disposition. If content type is supported by
            // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
            String accept = request.getHeader("Accept");
            disposition = accept != null && HttpUtils.accepts(accept, contentType) ? "inline" : "attachment";
        }
        logger.debug("Content-Type : {}", contentType);
        // Initialize response.
        response.reset();
        response.setBufferSize(DEFAULT_BUFFER_SIZE);
        response.setHeader("Content-Type", contentType);
        response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
        logger.debug("Content-Disposition : {}", disposition);
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("ETag", fileName);
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME);

        // Send requested file (part(s)) to client ------------------------------------------------

        // Prepare streams.
        try (InputStream input = new BufferedInputStream(Files.newInputStream(filepath));
             OutputStream output = response.getOutputStream()) {

            if (ranges.isEmpty() || ranges.get(0) == full) {

                // Return full file.
                logger.info("Return full file");
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total);
                response.setHeader("Content-Length", String.valueOf(full.length));
                Range.copy(input, output, length, full.start, full.length);

            } else if (ranges.size() == 1) {

                // Return single part of file.
                Range r = ranges.get(0);
                logger.info("Return 1 part of file : from ({}) to ({})", r.start, r.end);
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                response.setHeader("Content-Length", String.valueOf(r.length));
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                // Copy single part range.
                Range.copy(input, output, length, r.start, r.length);

            } else {

                // Return multiple parts of file.
                response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                // Cast back to ServletOutputStream to get the easy println methods.
                ServletOutputStream sos = (ServletOutputStream) output;

                // Copy multi part range.
                for (Range r : ranges) {
                    logger.info("Return multi part of file : from ({}) to ({})", r.start, r.end);
                    // Add multipart boundary and header fields for every range.
                    sos.println();
                    sos.println("--" + MULTIPART_BOUNDARY);
                    sos.println("Content-Type: " + contentType);
                    sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

                    // Copy single part range of multi part range.
                    Range.copy(input, output, length, r.start, r.length);
                }

                // End with multipart boundary.
                sos.println();
                sos.println("--" + MULTIPART_BOUNDARY + "--");
            }
        }

    }

    private static class Range {
        long start;
        long end;
        long length;
        long total;

        /**
         * Construct a byte range.
         * @param start Start of the byte range.
         * @param end End of the byte range.
         * @param total Total length of the byte source.
         */
        public Range(long start, long end, long total) {
            this.start = start;
            this.end = end;
            this.length = end - start + 1;
            this.total = total;
        }

        public static long sublong(String value, int beginIndex, int endIndex) {
            String substring = value.substring(beginIndex, endIndex);
            return (substring.length() > 0) ? Long.parseLong(substring) : -1;
        }

        private static void copy(InputStream input, OutputStream output, long inputSize, long start, long length) throws IOException {
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int read;

            if (inputSize == length) {
                // Write full range.
                while ((read = input.read(buffer)) > 0) {
                    output.write(buffer, 0, read);
                    output.flush();
                }
            } else {
                input.skip(start);
                long toRead = length;

                while ((read = input.read(buffer)) > 0) {
                    if ((toRead -= read) > 0) {
                        output.write(buffer, 0, read);
                        output.flush();
                    } else {
                        output.write(buffer, 0, (int) toRead + read);
                        output.flush();
                        break;
                    }
                }
            }
        }
    }
    private static class HttpUtils {

        /**
         * Returns true if the given accept header accepts the given value.
         * @param acceptHeader The accept header.
         * @param toAccept The value to be accepted.
         * @return True if the given accept header accepts the given value.
         */
        public static boolean accepts(String acceptHeader, String toAccept) {
            String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
            Arrays.sort(acceptValues);

            return Arrays.binarySearch(acceptValues, toAccept) > -1
                    || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
                    || Arrays.binarySearch(acceptValues, "*/*") > -1;
        }

        /**
         * Returns true if the given match header matches the given value.
         * @param matchHeader The match header.
         * @param toMatch The value to be matched.
         * @return True if the given match header matches the given value.
         */
        public static boolean matches(String matchHeader, String toMatch) {
            String[] matchValues = matchHeader.split("\\s*,\\s*");
            Arrays.sort(matchValues);
            return Arrays.binarySearch(matchValues, toMatch) > -1
                    || Arrays.binarySearch(matchValues, "*") > -1;
        }
    }
}

在控制器中使用:

MultipartFileSender.fromPath(pathtofile)
                .with(httprequest)
                .with(httpresponse)
                .serveResource();
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在 Spring MVC 中实现 HTTP 字节范围请求 的相关文章

随机推荐

  • 在一行与 1 和 0 矩阵之间进行异或的更快方法?

    我有一行数据 比如说A 0 1 1 1 0 0 矩阵 B 包含许多行 对于一个虚拟的例子 我们假设它只是B 1 1 1 0 1 0 1 0 0 1 0 1 我想找到 A 和 B 的行不同的列数 并使用该差异向量来查找 B 的哪一行与 A 最
  • 使用字符串创建 Red 语言的单词和路径

    我有字符串在namelist 对应于应用程序中的变量和字段名称 该函数应该从名称列表中读取字符串 添加 f 以获取 field names 然后将变量值放入相应的字段中 我尝试了以下代码 没有给出任何错误 但也不起作用 namelist v
  • Rstudio不会编织

    Rstudio 不会编织 我已经在课程中使用它几个星期了 当我尝试编织时 它会执行直到遇到一些代码并停止 代码是 ggplot 数据 gss aes x 年 填充 度 几何酒吧 消息是 找不到函数 ggplot 其他函数也会发生这种情况 注
  • C#8 接口及其中定义的属性/方法 - 显然不起作用

    这是我使用的界面 public interface IPresentism public abstract bool isPresent get public virtual bool isAbsent gt isPresent isPre
  • Oracle 中的 OVER 子句

    Oracle中的OVER子句是什么意思 The OVER子句指定分析函数运行的 分区 排序和窗口 示例 1 计算移动平均线 AVG amt OVER ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1
  • 如何通过解析 TTF 字体文件获取字形宽度?

    用于捕获 a 的字形宽度TrueType字体 我转换对应的TTF归档依据fontforge into AFM 它是文本格式 不是二进制 然后 解析文本文件以捕获字形宽度 应该有更简单的方法来直接解析二进制文件TTF文件来捕获字形宽度 我很欣
  • C# 索引属性?

    我使用 Visual Basic 已经有一段时间了 最 近决定开始学习 C 作为学习更复杂语言的一个步骤 作为这次跳跃的一部分 我决定将一些旧的 VB 项目手动转换为 C 我遇到的问题是转换一个具有使用带有参数 索引的属性的类的库 在 VB
  • 如何调试IE11 APPCACHE

    我有一个适用于 CHROME 和 SAFARI 的 HTML5 页面 但使用 Internet Explorer 11 不起作用 我的缓存清单根据http manifest validator com 我很沮丧 这是 AppCache 清单
  • 将 XHTML 转换为 Word ML

    将 Word HTML 转换为 Word XML 的最佳方法是什么 我无法购买工具 因此需要最好是 XSLT 它是免费的 并且适用于段落 列表 粗体和斜体等基本格式 斯蒂芬 布永写了一篇blog关于这一点 请参阅 MSDN 她提供了一个非常
  • Firebase Cloud 代码(后端逻辑)

    我正在考虑使用 Firebase 而不是 Parse 因为它即将关闭 来满足我未来的移动后端需求 我真的很喜欢它的实时数据库方面 但它没有像 Parse 与 Cloud Code 那样轻松集成后端逻辑 有没有简单的方法可以实现此功能 或者很
  • 无效的正则表达式错误

    我正在尝试检索该字符串的类别部分 property id 516 category featured properties 所以结果应该是 featured properties 我想出了一个正则表达式并在这个网站上进行了测试http gs
  • 将 Ado.net DataReader 转换为 IDataRecord 给出奇怪的结果

    我有一个针对数据库运行的查询 我可以看到有一条 2013 年 5 月 31 日的记录 当我使用 ADO NET 从 C 运行此查询 然后使用以下代码时 我丢失了 2013 年 5 月 31 日的记录 var timeSeriesList n
  • fileReader.readAsBinaryString 上传文件

    尝试使用 fileReader readAsBinaryString 通过 AJAX 将 PNG 文件上传到服务器 精简代码 fileObject 是包含我的文件信息的对象 var fileReader new FileReader fil
  • Google Sheets:具有动态变化的自定义函数

    我正在使用一个自定义功能跟踪什么为单元格着色是 但是这个函数有一个问题不更新自身如果细胞颜色改变 细胞颜色 function GetCellColorCode input var ss SpreadsheetApp getActiveSpr
  • 气流 Schedule_interval 和 start_date 使其始终触发下一个间隔

    如何配置气流 mwaa 以便它在部署 dag 时每天同一时间 太平洋标准时间上午 6 点 触发 我尝试过对我来说有意义的事情 将schedule interval设置为0 6 将开始日期设置为 now datetime utcnow now
  • javascript 中 isNaN 和 Number.isNaN 之间的混淆

    我对 NaN 的工作原理感到困惑 我已经执行了isNaN undefined 它回来了true 但如果我会使用Number isNaN undefined 它正在返回false 那么我应该使用哪一个 还有为什么结果有这么大的差异 引用一个p
  • 类型类 MonadPlus、Alternative 和 Monoid 之间的区别?

    标准库 Haskell 类型类MonadPlus Alternative and Monoid每个方法都提供了两种语义基本相同的方法 空值 mzero empty or mempty 操作员a gt a gt a将类型类中的值连接在一起 m
  • 如何在Python中从子类调用父类的方法?

    在 Python 中创建简单的对象层次结构时 我希望能够从派生类调用父类的方法 在 Perl 和 Java 中 有一个关键字 super 在 Perl 中 我可能会这样做 package Foo sub frotz return Bamf
  • Spring 会话范围 bean 与 AOP 的问题

    我想在 HomeController 类中注入 currentUser 实例 因此对于每个请求 HomeController 都会有 currentUser 对象 我的配置
  • 如何在 Spring MVC 中实现 HTTP 字节范围请求

    我的网站上出现视频倒带问题 我发现了 http 标头的问题 我当前返回视频的控制器方法 RequestMapping method RequestMethod GET value testVideo ResponseBody public