• 使用Spring Boot实现大文件断点续传及文件校验


    一、简介

    随着互联网的快速发展,大文件的传输成为了互联网应用的重要组成部分。然而,由于网络不稳定等因素的影响,大文件的传输经常会出现中断的情况,这时需要重新传输,导致传输效率低下。

    为了解决这个问题,可以实现大文件的断点续传功能。断点续传功能可以在传输中断后继续传输,而不需要从头开始传输。这样可以大大提高传输的效率。

    Spring Boot是一个快速开发的Java Web开发框架,可以帮助我们快速搭建一个Web应用程序。在Spring Boot中,我们可以很容易地实现大文件的断点续传功能。

    本文将介绍如何使用Spring Boot实现大文件的断点续传功能。

    二、Spring Boot实现大文件断点续传的原理

    实现大文件的断点续传功能,需要在客户端和服务端都进行相应的实现。

    实现示例1

    服务端如何将一个大视频文件做切分,分段响应给客户端,让浏览器可以渐进式地播放。

    Spring Boot实现HTTP分片下载断点续传,从而实现H5页面的大视频播放问题,实现渐进式播放,每次只播放需要播放的内容就可以了,不需要加载整个文件到内存中。

    文件的断点续传、文件多线程并发下载(迅雷就是这么玩的)等。

    1. cn.hutool
    2. hutool-bom
    3. 5.8.18
    4. pom
    5. import
    6. cn.hutool
    7. hutool-core
    8. org.projectlombok
    9. lombok
    10. true

    代码实现

    ResourceController

    1. package com.example.insurance.controller;
    2. import javax.servlet.http.HttpServletRequest;
    3. import javax.servlet.http.HttpServletResponse;
    4. import java.io.IOException;
    5. import java.nio.file.Files;
    6. import java.nio.file.Path;
    7. import java.nio.file.Paths;
    8. import java.util.List;
    9. import com.example.insurance.common.ContentRange;
    10. import com.example.insurance.common.MediaContentUtil;
    11. import com.example.insurance.common.NioUtils;
    12. import lombok.extern.slf4j.Slf4j;
    13. import org.springframework.http.HttpHeaders;
    14. import org.springframework.http.HttpRange;
    15. import org.springframework.http.HttpStatus;
    16. import org.springframework.util.CollectionUtils;
    17. import org.springframework.util.StopWatch;
    18. import org.springframework.web.bind.annotation.GetMapping;
    19. import org.springframework.web.bind.annotation.PathVariable;
    20. import org.springframework.web.bind.annotation.RequestHeader;
    21. import org.springframework.web.bind.annotation.RequestMapping;
    22. import org.springframework.web.bind.annotation.RestController;
    23. /**
    24. * 内容资源控制器
    25. */
    26. @SuppressWarnings("unused")
    27. @Slf4j
    28. @RestController("resourceController")
    29. @RequestMapping(path = "/resource")
    30. public class ResourceController {
    31. /**
    32. * 获取文件内容
    33. *
    34. * @param fileName 内容文件名称
    35. * @param response 响应对象
    36. */
    37. @GetMapping("/media/{fileName}")
    38. public void getMedia(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response,
    39. @RequestHeader HttpHeaders headers) {
    40. // printRequestInfo(fileName, request, headers);
    41. String filePath = MediaContentUtil.filePath();
    42. try {
    43. this.download(fileName, filePath, request, response, headers);
    44. } catch (Exception e) {
    45. log.error("getMedia error, fileName={}", fileName, e);
    46. }
    47. }
    48. /**
    49. * 获取封面内容
    50. *
    51. * @param fileName 内容封面名称
    52. * @param response 响应对象
    53. */
    54. @GetMapping("/cover/{fileName}")
    55. public void getCover(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response,
    56. @RequestHeader HttpHeaders headers) {
    57. // printRequestInfo(fileName, request, headers);
    58. String filePath = MediaContentUtil.filePath();
    59. try {
    60. this.download(fileName, filePath, request, response, headers);
    61. } catch (Exception e) {
    62. log.error("getCover error, fileName={}", fileName, e);
    63. }
    64. }
    65. // ======= internal =======
    66. private static void printRequestInfo(String fileName, HttpServletRequest request, HttpHeaders headers) {
    67. String requestUri = request.getRequestURI();
    68. String queryString = request.getQueryString();
    69. log.debug("file={}, url={}?{}", fileName, requestUri, queryString);
    70. log.info("headers={}", headers);
    71. }
    72. /**
    73. * 设置请求响应状态、头信息、内容类型与长度 等。
    74. *
    75. *
    76. * 2. Range Units
    77. * 4. Responses to a Range Request
    78. *
    79. *
    80. * 10.2.7 206 Partial Content
    81. * 14.5 Accept-Ranges
    82. * 14.13 Content-Length
    83. * 14.16 Content-Range
    84. * 14.17 Content-Type
    85. * 19.5.1 Content-Disposition
    86. * 15.5 Content-Disposition Issues
    87. *
    88. *
    89. * 2. The Content-Disposition Header Field
    90. * 2.1 The Inline Disposition Type
    91. * 2.3 The Filename Parameter
    92. *
  • *
  • * @param response 请求响应对象
  • * @param fileName 请求的文件名称
  • * @param contentType 内容类型
  • * @param contentRange 内容范围对象
  • */
  • private static void setResponse(
  • HttpServletResponse response, String fileName, String contentType,
  • ContentRange contentRange) {
  • // http状态码要为206:表示获取部分内容
  • response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
  • // 支持断点续传,获取部分字节内容
  • // Accept-Ranges:bytes,表示支持Range请求
  • response.setHeader(HttpHeaders.ACCEPT_RANGES, ContentRange.BYTES_STRING);
  • // inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名
  • response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
  • "inline;filename=" + MediaContentUtil.encode(fileName));
  • // Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
  • // Content-Range: bytes 0-10/3103,格式为bytes 开始-结束/全部
  • response.setHeader(HttpHeaders.CONTENT_RANGE, contentRange.toContentRange());
  • response.setContentType(contentType);
  • // Content-Length: 11,本次内容的大小
  • response.setContentLengthLong(contentRange.applyAsContentLength());
  • }
  • /**
  • *
  • */
  • private void download(
  • String fileName, String path, HttpServletRequest request, HttpServletResponse response,
  • HttpHeaders headers)
  • throws IOException {
  • Path filePath = Paths.get(path + fileName);
  • if (!Files.exists(filePath)) {
  • log.warn("file not exist, filePath={}", filePath);
  • return;
  • }
  • long fileLength = Files.size(filePath);
  • // long fileLength2 = filePath.toFile().length() - 1;
  • // // fileLength=1184856, fileLength2=1184855
  • // log.info("fileLength={}, fileLength2={}", fileLength, fileLength2);
  • // 内容范围
  • ContentRange contentRange = applyAsContentRange(headers, fileLength, request);
  • // 要下载的长度
  • long contentLength = contentRange.applyAsContentLength();
  • log.debug("contentRange={}, contentLength={}", contentRange, contentLength);
  • // 文件类型
  • String contentType = request.getServletContext().getMimeType(fileName);
  • // mimeType=video/mp4, CONTENT_TYPE=null
  • log.debug("mimeType={}, CONTENT_TYPE={}", contentType, request.getContentType());
  • setResponse(response, fileName, contentType, contentRange);
  • // 耗时指标统计
  • StopWatch stopWatch = new StopWatch("downloadFile");
  • stopWatch.start(fileName);
  • try {
  • // case-1.参考网上他人的实现
  • // if (fileLength >= Integer.MAX_VALUE) {
  • // NioUtils.copy(filePath, response, contentRange);
  • // } else {
  • // NioUtils.copyByChannelAndBuffer(filePath, response, contentRange);
  • // }
  • // case-2.使用现成API
  • NioUtils.copyByBio(filePath, response, contentRange);
  • // NioUtils.copyByNio(filePath, response, contentRange);
  • // case-3.视频分段渐进式播放
  • // if (contentType.startsWith("video")) {
  • // NioUtils.copyForBufferSize(filePath, response, contentRange);
  • // } else {
  • // // 图片、PDF等文件
  • // NioUtils.copyByBio(filePath, response, contentRange);
  • // }
  • } finally {
  • stopWatch.stop();
  • log.info("download file, fileName={}, time={} ms", fileName, stopWatch.getTotalTimeMillis());
  • }
  • }
  • private static ContentRange applyAsContentRange(
  • HttpHeaders headers, long fileLength, HttpServletRequest request) {
  • /*
  • * 3.1. Range - HTTP/1.1 Range Requests
  • * https://www.rfc-editor.org/rfc/rfc7233#section-3.1
  • * Range: "bytes" "=" first-byte-pos "-" [ last-byte-pos ]
  • *
  • * For example:
  • * bytes=0-
  • * bytes=0-499
  • */
  • // Range:告知服务端,客户端下载该文件想要从指定的位置开始下载
  • List httpRanges = headers.getRange();
  • String range = request.getHeader(HttpHeaders.RANGE);
  • // httpRanges=[], range=null
  • // httpRanges=[448135688-], range=bytes=448135688-
  • log.debug("httpRanges={}, range={}", httpRanges, range);
  • // 开始下载位置
  • long firstBytePos;
  • // 结束下载位置
  • long lastBytePos;
  • if (CollectionUtils.isEmpty(httpRanges)) {
  • firstBytePos = 0;
  • lastBytePos = fileLength - 1;
  • } else {
  • HttpRange httpRange = httpRanges.get(0);
  • firstBytePos = httpRange.getRangeStart(fileLength);
  • lastBytePos = httpRange.getRangeEnd(fileLength);
  • }
  • return new ContentRange(firstBytePos, lastBytePos, fileLength);
  • }
  • }
  • NioUtils

    1. package com.example.insurance.common;
    2. import javax.servlet.http.HttpServletResponse;
    3. import java.io.BufferedOutputStream;
    4. import java.io.InputStream;
    5. import java.io.OutputStream;
    6. import java.io.RandomAccessFile;
    7. import java.nio.MappedByteBuffer;
    8. import java.nio.channels.Channels;
    9. import java.nio.channels.FileChannel;
    10. import java.nio.file.Path;
    11. import java.nio.file.StandardOpenOption;
    12. import cn.hutool.core.io.IORuntimeException;
    13. import cn.hutool.core.io.IoUtil;
    14. import cn.hutool.core.io.NioUtil;
    15. import cn.hutool.core.io.StreamProgress;
    16. import cn.hutool.core.io.unit.DataSize;
    17. import lombok.extern.slf4j.Slf4j;
    18. import org.apache.catalina.connector.ClientAbortException;
    19. /**
    20. * NIO相关工具封装,主要针对Channel读写、拷贝等封装
    21. */
    22. @Slf4j
    23. public final class NioUtils {
    24. /**
    25. * 缓冲区大小 16KB
    26. *
    27. * @see NioUtil#DEFAULT_BUFFER_SIZE
    28. * @see NioUtil#DEFAULT_LARGE_BUFFER_SIZE
    29. */
    30. // private static final int BUFFER_SIZE = NioUtil.DEFAULT_MIDDLE_BUFFER_SIZE;
    31. private static final int BUFFER_SIZE = (int) DataSize.ofKilobytes(16L).toBytes();
    32. /**
    33. *
    34. *
    35. * 服务端如何将一个大的视频文件做切分,分段响应给客户端,让浏览器可以渐进式地播放。
    36. * 文件的断点续传、文件多线程并发下载(迅雷就是这么玩的)等。
    37. *
    38. *
    39. *
  • */
  • public static void copyForBufferSize(
  • Path filePath, HttpServletResponse response, ContentRange contentRange) {
  • String fileName = filePath.getFileName().toString();
  • RandomAccessFile randomAccessFile = null;
  • OutputStream outputStream = null;
  • try {
  • // 随机读文件
  • randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
  • // 移动访问指针到指定位置
  • randomAccessFile.seek(contentRange.getStart());
  • // 注意:缓冲区大小 2MB,视频加载正常;1MB时有部分视频加载失败
  • int bufferSize = BUFFER_SIZE;
  • //获取响应的输出流
  • outputStream = new BufferedOutputStream(response.getOutputStream(), bufferSize);
  • // 每次请求只返回1MB的视频流
  • byte[] buffer = new byte[bufferSize];
  • int len = randomAccessFile.read(buffer);
  • //设置此次相应返回的数据长度
  • response.setContentLength(len);
  • // 将这1MB的视频流响应给客户端
  • outputStream.write(buffer, 0, len);
  • log.info("file download complete, fileName={}, contentRange={}",
  • fileName, contentRange.toContentRange());
  • } catch (ClientAbortException | IORuntimeException e) {
  • // 捕获此异常表示用户停止下载
  • log.warn("client stop file download, fileName={}", fileName);
  • } catch (Exception e) {
  • log.error("file download error, fileName={}", fileName, e);
  • } finally {
  • IoUtil.close(outputStream);
  • IoUtil.close(randomAccessFile);
  • }
  • }
  • /**
  • * 拷贝流,拷贝后关闭流。
  • *
  • * @param filePath 源文件路径
  • * @param response 请求响应
  • * @param contentRange 内容范围
  • */
  • public static void copyByBio(
  • Path filePath, HttpServletResponse response, ContentRange contentRange) {
  • String fileName = filePath.getFileName().toString();
  • InputStream inputStream = null;
  • OutputStream outputStream = null;
  • try {
  • RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
  • randomAccessFile.seek(contentRange.getStart());
  • inputStream = Channels.newInputStream(randomAccessFile.getChannel());
  • outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
  • StreamProgress streamProgress = new StreamProgressImpl(fileName);
  • long transmitted = IoUtil.copy(inputStream, outputStream, BUFFER_SIZE, streamProgress);
  • log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
  • } catch (ClientAbortException | IORuntimeException e) {
  • // 捕获此异常表示用户停止下载
  • log.warn("client stop file download, fileName={}", fileName);
  • } catch (Exception e) {
  • log.error("file download error, fileName={}", fileName, e);
  • } finally {
  • IoUtil.close(outputStream);
  • IoUtil.close(inputStream);
  • }
  • }
  • /**
  • * 拷贝流,拷贝后关闭流。
  • *
  • *
  • *
  • *
  • * @param filePath 源文件路径
  • * @param response 请求响应
  • * @param contentRange 内容范围
  • */
  • public static void copyByNio(
  • Path filePath, HttpServletResponse response, ContentRange contentRange) {
  • String fileName = filePath.getFileName().toString();
  • InputStream inputStream = null;
  • OutputStream outputStream = null;
  • try {
  • RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
  • randomAccessFile.seek(contentRange.getStart());
  • inputStream = Channels.newInputStream(randomAccessFile.getChannel());
  • outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
  • StreamProgress streamProgress = new StreamProgressImpl(fileName);
  • long transmitted = NioUtil.copyByNIO(inputStream, outputStream,
  • BUFFER_SIZE, streamProgress);
  • log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
  • } catch (ClientAbortException | IORuntimeException e) {
  • // 捕获此异常表示用户停止下载
  • log.warn("client stop file download, fileName={}", fileName);
  • } catch (Exception e) {
  • log.error("file download error, fileName={}", fileName, e);
  • } finally {
  • IoUtil.close(outputStream);
  • IoUtil.close(inputStream);
  • }
  • }
  • /**
  • *
  • *
  • * SpringBoot 实现Http分片下载断点续传,从而实现H5页面的大视频播放问题,实现渐进式播放,每次只播放需要播放的内容就可以了,不需要加载整个文件到内存中。
  • * 二、Http分片下载断点续传实现
  • * 四、缓存文件定时删除任务
  • *
  • */
  • public static void copy(Path filePath, HttpServletResponse response, ContentRange contentRange) {
  • String fileName = filePath.getFileName().toString();
  • // 要下载的长度
  • long contentLength = contentRange.applyAsContentLength();
  • BufferedOutputStream outputStream = null;
  • RandomAccessFile randomAccessFile = null;
  • // 已传送数据大小
  • long transmitted = 0;
  • try {
  • randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
  • randomAccessFile.seek(contentRange.getStart());
  • outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
  • // 把数据读取到缓冲区中
  • byte[] buffer = new byte[BUFFER_SIZE];
  • int len = BUFFER_SIZE;
  • //warning:判断是否到了最后不足4096(buffer的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面
  • //不然会会先读取randomAccessFile,造成后面读取位置出错;
  • while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buffer)) != -1) {
  • outputStream.write(buffer, 0, len);
  • transmitted += len;
  • log.info("fileName={}, transmitted={}", fileName, transmitted);
  • }
  • //处理不足buffer.length部分
  • if (transmitted < contentLength) {
  • len = randomAccessFile.read(buffer, 0, (int) (contentLength - transmitted));
  • outputStream.write(buffer, 0, len);
  • transmitted += len;
  • log.info("fileName={}, transmitted={}", fileName, transmitted);
  • }
  • log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
  • } catch (ClientAbortException e) {
  • // 捕获此异常表示用户停止下载
  • log.warn("client stop file download, fileName={}, transmitted={}", fileName, transmitted);
  • } catch (Exception e) {
  • log.error("file download error, fileName={}, transmitted={}", fileName, transmitted, e);
  • } finally {
  • IoUtil.close(outputStream);
  • IoUtil.close(randomAccessFile);
  • }
  • }
  • /**
  • * 通过数据传输通道和缓冲区读取文件数据。
  • *
  • * 当文件长度超过{@link Integer#MAX_VALUE}时,
  • * 使用{@link FileChannel#map(FileChannel.MapMode, long, long)}报如下异常。
  • * java.lang.IllegalArgumentException: Size exceeds Integer.MAX_VALUE
  • * at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:863)
  • * at com.example.insurance.controller.ResourceController.download(ResourceController.java:200)
  • *
  • *
  • * @param filePath 源文件路径
  • * @param response 请求响应
  • * @param contentRange 内容范围
  • */
  • public static void copyByChannelAndBuffer(
  • Path filePath, HttpServletResponse response, ContentRange contentRange) {
  • String fileName = filePath.getFileName().toString();
  • // 要下载的长度
  • long contentLength = contentRange.applyAsContentLength();
  • BufferedOutputStream outputStream = null;
  • FileChannel inChannel = null;
  • // 已传送数据大小
  • long transmitted = 0;
  • long firstBytePos = contentRange.getStart();
  • long fileLength = contentRange.getLength();
  • try {
  • inChannel = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.WRITE);
  • // 建立直接缓冲区
  • MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY, firstBytePos, fileLength);
  • outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE);
  • // 把数据读取到缓冲区中
  • byte[] buffer = new byte[BUFFER_SIZE];
  • int len = BUFFER_SIZE;
  • // warning:判断是否到了最后不足4096(buffer的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面
  • // 不然会会先读取file,造成后面读取位置出错
  • while ((transmitted + len) <= contentLength) {
  • inMap.get(buffer);
  • outputStream.write(buffer, 0, len);
  • transmitted += len;
  • log.info("fileName={}, transmitted={}", fileName, transmitted);
  • }
  • // 处理不足buffer.length部分
  • if (transmitted < contentLength) {
  • len = (int) (contentLength - transmitted);
  • buffer = new byte[len];
  • inMap.get(buffer);
  • outputStream.write(buffer, 0, len);
  • transmitted += len;
  • log.info("fileName={}, transmitted={}", fileName, transmitted);
  • }
  • log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted);
  • } catch (ClientAbortException e) {
  • // 捕获此异常表示用户停止下载
  • log.warn("client stop file download, fileName={}, transmitted={}", fileName, transmitted);
  • } catch (Exception e) {
  • log.error("file download error, fileName={}, transmitted={}", fileName, transmitted, e);
  • } finally {
  • IoUtil.close(outputStream);
  • IoUtil.close(inChannel);
  • }
  • }
  • }
  • ContentRange

    1. package com.example.insurance.common;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Getter;
    4. /**
    5. * 内容范围对象
    6. *
    7. *
    8. * Content-Range: "bytes" first-byte-pos "-" last-byte-pos "/" complete-length
    9. *
    10. * For example:
    11. * Content-Range: bytes 0-499/1234
    12. *
  • *
  • * @see org.apache.catalina.servlets.DefaultServlet.Range
  • */
  • @Getter
  • @AllArgsConstructor
  • public class ContentRange {
  • /**
  • * 第一个字节的位置
  • */
  • private final long start;
  • /**
  • * 最后一个字节的位置
  • */
  • private long end;
  • /**
  • * 内容完整的长度/总长度
  • */
  • private final long length;
  • public static final String BYTES_STRING = "bytes";
  • /**
  • * 组装内容范围的响应头。
  • *
  • *
  • * Content-Range: "bytes" first-byte-pos "-" last-byte-pos "/" complete-length
  • *
  • * For example:
  • * Content-Range: bytes 0-499/1234
  • *
  • *
  • * @return 内容范围的响应头
  • */
  • public String toContentRange() {
  • return BYTES_STRING + ' ' + start + '-' + end + '/' + length;
  • // return "bytes " + start + "-" + end + "/" + length;
  • }
  • /**
  • * 计算内容完整的长度/总长度。
  • *
  • * @return 内容完整的长度/总长度
  • */
  • public long applyAsContentLength() {
  • return end - start + 1;
  • }
  • /**
  • * Validate range.
  • *
  • * @return true if the range is valid, otherwise false
  • */
  • public boolean validate() {
  • if (end >= length) {
  • end = length - 1;
  • }
  • return (start >= 0) && (end >= 0) && (start <= end) && (length > 0);
  • }
  • @Override
  • public String toString() {
  • return "firstBytePos=" + start +
  • ", lastBytePos=" + end +
  • ", fileLength=" + length;
  • }
  • }
  • StreamProgressImpl

    1. package com.example.insurance.common;
    2. import cn.hutool.core.io.StreamProgress;
    3. import lombok.AllArgsConstructor;
    4. import lombok.extern.slf4j.Slf4j;
    5. /**
    6. * 数据流进度条
    7. */
    8. @Slf4j
    9. @AllArgsConstructor
    10. public class StreamProgressImpl implements StreamProgress {
    11. private final String fileName;
    12. @Override
    13. public void start() {
    14. log.info("start progress {}", fileName);
    15. }
    16. @Override
    17. public void progress(long total, long progressSize) {
    18. log.debug("progress {}, total={}, progressSize={}", fileName, total, progressSize);
    19. }
    20. @Override
    21. public void finish() {
    22. log.info("finish progress {}", fileName);
    23. }
    24. }

    MediaContentUtil

    1. package com.example.insurance.common;
    2. import java.net.URLDecoder;
    3. import java.net.URLEncoder;
    4. import java.nio.charset.StandardCharsets;
    5. /**
    6. * 文件内容辅助方法集
    7. */
    8. public final class MediaContentUtil {
    9. public static String filePath() {
    10. String osName = System.getProperty("os.name");
    11. String filePath = "/data/files/";
    12. if (osName.startsWith("Windows")) {
    13. filePath = "D:\" + filePath;
    14. }
    15. // else if (osName.startsWith("Linux")) {
    16. // filePath = MediaContentConstant.FILE_PATH;
    17. // }
    18. else if (osName.startsWith("Mac") || osName.startsWith("Linux")) {
    19. filePath = "/home/admin" + filePath;
    20. }
    21. return filePath;
    22. }
    23. public static String encode(String fileName) {
    24. return URLEncoder.encode(fileName, StandardCharsets.UTF_8);
    25. }
    26. public static String decode(String fileName) {
    27. return URLDecoder.decode(fileName, StandardCharsets.UTF_8);
    28. }
    29. }

    实现示例2

    代码实现

    (1)客户端需要实现以下功能
    (2)服务端需要实现以下功能
    1.编写客户端代码

    在客户端中,我们需要实现以下功能:

    以下是客户端代码的实现:

    1. @RestController
    2. @RequestMapping("/file")
    3. public class FileController {
    4. @PostMapping("/upload")
    5. public ResponseEntity uploadFile(
    6. @RequestParam("file") MultipartFile file,
    7. @RequestParam("fileName") String fileName,
    8. @RequestParam("startPosition") long startPosition) {
    9. try { // 建立连接
    10. Socket socket = new Socket("localhost", 8080);
    11. OutputStream outputStream = socket.getOutputStream();
    12. ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
    13. // 分块传输文件
    14. FileInputStream fileInputStream = (FileInputStream) file.getInputStream();
    15. fileInputStream.skip(startPosition);
    16. byte[] buffer = new byte[1024];
    17. int len;
    18. while ((len = fileInputStream.read(buffer)) != -1) {
    19. outputStream.write(buffer, 0, len);
    20. }
    21. // 计算MD5值
    22. fileInputStream.getChannel().position(0);
    23. String md5 = DigestUtils.md5Hex(fileInputStream);
    24. // 与服务端比较MD5值
    25. InputStream inputStream = socket.getInputStream();
    26. ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
    27. String serverMd5 = (String) objectInputStream.readObject();
    28. if (!md5.equals(serverMd5)) {
    29. throw new RuntimeException("MD5值不匹配");
    30. }
    31. // 关闭连接
    32. objectOutputStream.close();
    33. outputStream.close();
    34. socket.close();
    35. } catch (Exception e) {
    36. e.printStackTrace();
    37. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    38. }
    39. return ResponseEntity.ok().build();
    40. }
    41. }
    2.编写服务端代码

    在服务端中,我们需要实现以下功能:

    以下是服务端代码的实现:

    1. @RestController
    2. @RequestMapping("/file")
    3. public class FileController {
    4. private final String FILE_PATH = "/tmp/upload/";
    5. @PostMapping("/upload")
    6. public ResponseEntity uploadFile(HttpServletRequest request, @RequestParam("fileName") String fileName) {
    7. try {
    8. // 建立连接
    9. ServerSocket serverSocket = new ServerSocket(8080);
    10. Socket socket = serverSocket.accept();
    11. InputStream inputStream = socket.getInputStream();
    12. ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
    13. // 接收文件
    14. String filePath = FILE_PATH + fileName;
    15. RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
    16. long startPosition = randomAccessFile.length();
    17. randomAccessFile.seek(startPosition);
    18. byte[] buffer = new byte[1024];
    19. int len;
    20. while ((len = inputStream.read(buffer)) != -1) {
    21. randomAccessFile.write(buffer, 0, len);
    22. } // 计算MD5值
    23. FileInputStream fileInputStream = new FileInputStream(filePath);
    24. String md5 = DigestUtils.md5Hex(fileInputStream);
    25. // 返回MD5值
    26. OutputStream outputStream = socket.getOutputStream();
    27. ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
    28. objectOutputStream.writeObject(md5); // 关闭连
    29. objectInputStream.close();
    30. inputStream.close();
    31. randomAccessFile.close();
    32. socket.close();
    33. serverSocket.close();
    34. } catch (Exception e) {
    35. e.printStackTrace();
    36. return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    37. }
    38. return ResponseEntity.ok().build();
    39. }
    40. }
    3. 编写前端代码

    在前端中,我们需要实现以下功能:

    以下是前端代码的实现:

    1. <html>
    2. <head>
    3. <meta charset="UTF-8">
    4. <title>Spring Boot File Uploadtitle>
    5. <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js">script>
    6. head>
    7. <body><input type="file" id="file">
    8. <button onclick="upload()">Uploadbutton>
    9. <script> var file;
    10. var startPosition = 0;
    11. $('#file').on('change', function () {
    12. file = this.files[0];
    13. });
    14. function upload() {
    15. if (!file) {
    16. alert('Please select a file!');
    17. return;
    18. }
    19. var formData = new FormData();
    20. formData.append('file', file);
    21. formData.append('fileName', file.name);
    22. formData.append('startPosition', startPosition);
    23. $.ajax({
    24. url: '/file/upload',
    25. type: 'post',
    26. data: formData,
    27. cache: false,
    28. processData: false,
    29. contentType: false,
    30. success: function () {
    31. alert('Upload completed!');
    32. },
    33. error: function (xhr) {
    34. alert(xhr.responseText);
    35. },
    36. xhr: function () {
    37. var xhr = $.ajaxSettings.xhr();
    38. xhr.upload.onprogress = function (e) {
    39. if (e.lengthComputable) {
    40. var percent = e.loaded / e.total * 100;
    41. console.log('Upload percent: ' + percent.toFixed(2) + '%');
    42. }
    43. };
    44. return xhr;
    45. }
    46. });
    47. }script>
    48. body>
    49. html>

    总结

    本文介绍了如何使用Spring Boot实现大文件断点续传。在实现中,我们使用了Java的RandomAccessFile类来实现文件的分块上传和断点续传,使用了Spring Boot的RestController注解来实现Web服务的开发,使用了jQuery的Ajax函数来实现前端页面的开发。

    在实际开发中,需要注意以下几点

  • 相关阅读:
    WWW‘22 推荐系统论文之多任务与对比学习篇
    一分钟!图片生成32种动画;Adobe绘画工具大升级;复盘Kaggle首场LLM比赛;VR科普万字长文 | ShowMeAI日报
    工业检测中物距、像距和焦距的关系&&相机视野的计算方法
    DHCP动态获取IP地址流程
    每周编辑精选|微软开源 Orca-Math 高质量数学数据集、清华大学研究团队发布条件去噪扩散模型 SPDiff
    OCR -- 非极大值抑制(NMS)算法详解
    【分享】集简云新功能:集简云浏览器机器人,将任意网站页面转换为API连接器
    亮相数字科技出海峰会,火山引擎边缘云助力数字化出海“加速度”
    Springboot如何整合Kafka
    基于BP神经网络对鸢尾花数据集分类
  • 原文地址:https://blog.csdn.net/qq_18237141/article/details/134452413