【问题标题】:How to Implement HTTP byte-range requests in Spring MVC如何在 Spring MVC 中实现 HTTP 字节范围请求
【发布时间】:2015-04-10 05:30:37
【问题描述】:

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

我发现了 http 标头的问题。

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

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

如何在支持字节范围的情况下重写以下代码?

附言

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

但是这段代码对我来说看起来很难,我无法理解。我希望在 spring mvc 中存在的方式更简单。

【问题讨论】:

    标签: java spring http spring-mvc video-streaming


    【解决方案1】:

    在提供此答案时,支持 http 字节范围的请求已打开,但在 Spring 4.2.RC1 中已修复。检查 jira SPR-10805 或 PR 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();
    

    【讨论】:

    • 我找不到具有以下类的 jat:lan.dk.podcastserver.utils.MimeTypeUtils
    • 删除了那个导入并更改为将 mimetype 固定为“video/mp4”,但整个项目发布在 github:github.com/davinkevin/Podcast-Server
    • 现在是 SO 风格。问题 - 没有多余上下文的答案。我相信它会对未来的谷歌员工有用。
    • 有什么办法可以改变spring的静态资源解析器的默认实现(4.2RC1之前的版本),用一个可以支持http范围的控制器,用于下载恢复?
    • 这在多部分分支中有一个错误。除非每次读取都标记/重置流,否则每次全部跳过流不会读取预期的字节。更好的选择是(当然)根据上次读取的位置调整偏移量。
    【解决方案2】:

    现在是 2020 年。

    部分范围默认在 Spring 中实现(我使用的是 Spring Boot 2.2.4)。我的控制器代码看起来类似于下面的代码,它适用于范围标头。

    return ResponseEntity.ok().body(new FileSystemResource(file))
    

    注意事项(详见The Almighty Source Code):

    • Spring 要求将响应设置为 200,并且在内部(如果请求中存在范围标头)将其替换为 206 Partial Content。
    • Spring 还要求响应是资源实例 except InputStreamResource 失败。对于文件,FileSystemResource 似乎是最佳选择。

    【讨论】:

    • 这是今天最好的答案。无需自定义代码。
    • 我的代码类似于 URL resource = getClass().getClassLoader().getResource("nbch_intro.mp4");返回 ResponseEntity.ok().body(new FileSystemResource(resource.getFile()));在这种情况下,服务器返回 JSON .. 当我添加 .header("Content-Type", "video/mp4") 时,我得到一个异常:No converter for [class org.springframework.core.io.FileSystemResource] with preset内容类型'video/mp4'...
    【解决方案3】:

    如果你想避开MimeTypeUtils,你可以使用Files.probeContentType(Path path)。这将使用系统的默认实现。

    【讨论】:

      【解决方案4】:

      根据您要实现的目标,将请求映射到容器的默认 servlet 可能是有意义的。

      即在初始化期间:

      ServletContext container = ...
      
      // Map *.mp4 to the default servlet to support range requests
      
      ServletRegistration servletRegistration = container.getServletRegistration("default");
      servletRegistration.addMapping("*.mp4");
      

      【讨论】:

        【解决方案5】:

        Leandro 的回答非常适合我。我只用 RandomAccessFile 更改了 InputStream,访问文件内的随机点性能更好。

        这里是 MultipartFileSender 类

        import javax.servlet.ServletOutputStream;
        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;
        
        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import org.springframework.util.StringUtils;
        
        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;
        
        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 (RandomAccessFile input = new RandomAccessFile(filepath.toFile(), "r");
                 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(RandomAccessFile 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.seek(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;
                }
            }
        }
        

        【讨论】:

          【解决方案6】:

          哇,Leandro 的回答太精彩了。只需创建服务文件跟随他 并在控制器中使用。在这里我遵循他的指导方针

          @RequestMapping(value = [(SKConstant.URL_PROJECT_API + "/video/{token}")], method = arrayOf(RequestMethod.GET)
          fun getVideo(@PathVariable("token") token: String, response: HttpServletResponse, request: HttpServletRequest): Any?{
                   val file = baseService.getByToken(token)
                   if(file != null) {
                     return MultipartFileSender.fromFile(File(fileService.getUploadFileRealPath(file.uniqueName!!)))
                                        .with(request)
                                        .with(response)
                                        .serveResource()
           }
          

          【讨论】:

            【解决方案7】:

            试试这个简单的代码。

            @GET
            @Produces({"application/octet-stream"})
            public Response getFile() {
              File file = ... 
              return Response.ok(file, MediaType.APPLICATION_OCTET_STREAM)
                  .header("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"" ) .build();
            }
            

            更新:

            它使用 spring MVC 包装 Spring Jersey jersey.java.net RESTful 实现。

            对于纯 Spring mvc,请使用以下代码。

            @RequestMapping("/video") public ResponseEntity<byte[]> getvideo() throws IOException { 
                InputStream in = ... 
                final HttpHeaders headers = new HttpHeaders(); 
                headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); 
                return new ResponseEntity<byte[]>IOUtils.toByteArray(in),headers,HttpStatus.CREATED); 
            }
            

            IOUtils 来自 apache common jar

            【讨论】:

            • 永远不要在这种情况下将InputStream 转换为byte[]。你会有严重的记忆问题。
            • 不支持 http-byte-range
            猜你喜欢
            • 1970-01-01
            • 2015-02-02
            • 2016-01-02
            • 2015-10-05
            • 1970-01-01
            • 2015-09-29
            • 2011-05-18
            • 2013-01-21
            • 1970-01-01
            相关资源
            最近更新 更多