爆款云主机2核4G限时秒杀,88元/年起!
查看详情

活动

天翼云最新优惠活动,涵盖免费试用,产品折扣等,助您降本增效!
热门活动
  • 618智算钜惠季 爆款云主机2核4G限时秒杀,88元/年起!
  • 免费体验DeepSeek,上天翼云息壤 NEW 新老用户均可免费体验2500万Tokens,限时两周
  • 云上钜惠 HOT 爆款云主机全场特惠,更有万元锦鲤券等你来领!
  • 算力套餐 HOT 让算力触手可及
  • 天翼云脑AOne NEW 连接、保护、办公,All-in-One!
  • 中小企业应用上云专场 产品组合下单即享折上9折起,助力企业快速上云
  • 息壤高校钜惠活动 NEW 天翼云息壤杯高校AI大赛,数款产品享受线上订购超值特惠
  • 天翼云电脑专场 HOT 移动办公新选择,爆款4核8G畅享1年3.5折起,快来抢购!
  • 天翼云奖励推广计划 加入成为云推官,推荐新用户注册下单得现金奖励
免费活动
  • 免费试用中心 HOT 多款云产品免费试用,快来开启云上之旅
  • 天翼云用户体验官 NEW 您的洞察,重塑科技边界

智算服务

打造统一的产品能力,实现算网调度、训练推理、技术架构、资源管理一体化智算服务
智算云(DeepSeek专区)
科研助手
  • 算力商城
  • 应用商城
  • 开发机
  • 并行计算
算力互联调度平台
  • 应用市场
  • 算力市场
  • 算力调度推荐
一站式智算服务平台
  • 模型广场
  • 体验中心
  • 服务接入
智算一体机
  • 智算一体机
大模型
  • DeepSeek-R1-昇腾版(671B)
  • DeepSeek-R1-英伟达版(671B)
  • DeepSeek-V3-昇腾版(671B)
  • DeepSeek-R1-Distill-Llama-70B
  • DeepSeek-R1-Distill-Qwen-32B
  • Qwen2-72B-Instruct
  • StableDiffusion-V2.1
  • TeleChat-12B

应用商城

天翼云精选行业优秀合作伙伴及千余款商品,提供一站式云上应用服务
进入甄选商城进入云市场创新解决方案
办公协同
  • WPS云文档
  • 安全邮箱
  • EMM手机管家
  • 智能商业平台
财务管理
  • 工资条
  • 税务风控云
企业应用
  • 翼信息化运维服务
  • 翼视频云归档解决方案
工业能源
  • 智慧工厂_生产流程管理解决方案
  • 智慧工地
建站工具
  • SSL证书
  • 新域名服务
网络工具
  • 翼云加速
灾备迁移
  • 云管家2.0
  • 翼备份
资源管理
  • 全栈混合云敏捷版(软件)
  • 全栈混合云敏捷版(一体机)
行业应用
  • 翼电子教室
  • 翼智慧显示一体化解决方案

合作伙伴

天翼云携手合作伙伴,共创云上生态,合作共赢
天翼云生态合作中心
  • 天翼云生态合作中心
天翼云渠道合作伙伴
  • 天翼云代理渠道合作伙伴
天翼云服务合作伙伴
  • 天翼云集成商交付能力认证
天翼云应用合作伙伴
  • 天翼云云市场合作伙伴
  • 天翼云甄选商城合作伙伴
天翼云技术合作伙伴
  • 天翼云OpenAPI中心
  • 天翼云EasyCoding平台
天翼云培训认证
  • 天翼云学堂
  • 天翼云市场商学院
天翼云合作计划
  • 云汇计划
天翼云东升计划
  • 适配中心
  • 东升计划
  • 适配互认证

开发者

开发者相关功能入口汇聚
技术社区
  • 专栏文章
  • 互动问答
  • 技术视频
资源与工具
  • OpenAPI中心
开放能力
  • EasyCoding敏捷开发平台
培训与认证
  • 天翼云学堂
  • 天翼云认证
魔乐社区
  • 魔乐社区

支持与服务

为您提供全方位支持与服务,全流程技术保障,助您轻松上云,安全无忧
文档与工具
  • 文档中心
  • 新手上云
  • 自助服务
  • OpenAPI中心
定价
  • 价格计算器
  • 定价策略
基础服务
  • 售前咨询
  • 在线支持
  • 在线支持
  • 工单服务
  • 建议与反馈
  • 用户体验官
  • 服务保障
  • 客户公告
  • 会员中心
增值服务
  • 红心服务
  • 首保服务
  • 客户支持计划
  • 专家技术服务
  • 备案管家

了解天翼云

天翼云秉承央企使命,致力于成为数字经济主力军,投身科技强国伟大事业,为用户提供安全、普惠云服务
品牌介绍
  • 关于天翼云
  • 智算云
  • 天翼云4.0
  • 新闻资讯
  • 天翼云APP
基础设施
  • 全球基础设施
  • 信任中心
最佳实践
  • 精选案例
  • 超级探访
  • 云杂志
  • 分析师和白皮书
  • 天翼云·创新直播间
市场活动
  • 2025智能云生态大会
  • 2024智算云生态大会
  • 2023云生态大会
  • 2022云生态大会
  • 天翼云中国行
天翼云
  • 活动
  • 智算服务
  • 产品
  • 解决方案
  • 应用商城
  • 合作伙伴
  • 开发者
  • 支持与服务
  • 了解天翼云
      • 文档
      • 控制中心
      • 备案
      • 管理中心

      Spring之秒传、断点续传、分片上传和压缩

      首页 知识中心 软件开发 文章详情页

      Spring之秒传、断点续传、分片上传和压缩

      2024-09-25 10:14:09 阅读次数:56

      Spring

      一、大文件上传简介

      1、秒传

      通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了

      2、分片上传

      2.1 介绍

      分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件

      2.2 应用场景

      • 大文件上传

      • 网络环境环境不好,存在需要重传风险的场景

      3、断点续传

      3.1 介绍

      断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。

      3.2 应用场景

      断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传

      3.3 核心逻辑

      在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。

      3.4 实现流程步骤

      方案一,常规步骤

      • 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;

      • 初始化一个分片上传任务,返回本次分片上传唯一标识;

      • 按照一定的策略(串行或并行)发送各个分片数据块;

      • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

      方案二、更高效

      • 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小

      • 服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)

      • 服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件。

      二、普通方式

      1、整体思路

      1.1 前端部分思路

      所有请求都使用ajax

      • 文件控件选择后,计算文件唯一码,调用接口查询文件是否存在。文件存在则判断分片是否上传完成,已完成显示秒传信息;

      • 点击上传按钮后,再查询一次文件是否存在,来获取文件分片信息。文件不存在,那么起始分片为1;文件存在,那么获取起始分片为已上传+1;

      • ajax串行调用分片上传方法,成功后进行分片序号+1的分片上传,直到最终已上传分片序号和总分片数量相同。

      1.2 后端部分思路

      • 首先利用数据库存储文件信息,包括文件物理地址,分片接收进程和对应的md5码。利用md5码可以判断当前上传文件是否在服务器中存在(实现秒传),利用分片接收Index可以判断现在应该上传。

      • 前端ajax获取文件存在与否的信息,几种情况:

        • 不存在,则创建数据库记录,成功后调用分片1的上传

        • 存在,Index和总分片数量相同,秒传成功显示结果

        • 存在,但index小于总分片数量,调用分片index的上传

      • 分片在前端根据分片Index计算起点末尾,slice切割,ajax调用上传传到服务器并存储。当前分片传递成功,ajax接收success信息,串行进行index+1的分片的上传

      2、环境准备

      本次Demo项目是前后端一起,前端部分使用了内嵌的thymeleaf,根据链接跳转自动访问resource/static/下的静态文件,如果前后端分离可以参考,首先引入依赖

      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
      </dependency>
      
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
      </dependency>
      
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      
      <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
      </dependency>
      
      <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
      </dependency>
      
      <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
      </dependency>
      

      配置文件

      spring:
        datasource:
          url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&autoReconnect=true&useUnicode=true&characterEncoding=utf-8&useSSL=false
          username: root
          password: root
          driverClassName: com.mysql.cj.jdbc.Driver
        thymeleaf:
          cache: false
      
        servlet:
          multipart:
            max-file-size: -1
            max-request-size: -1
        mvc:
          static-path-pattern: /**
        session:
          store-type: jdbc
          jdbc:
            initialize-schema: always
      
      file:
        save-path: F:/file/
        temp: F:/file/temp/
        segment: 2*1024*1024
        max-file-size: 500
      
      logging:
        level:
          root: INFO
      

      数据库设计

      DROP TABLE IF EXISTS `segment_file`;
      CREATE TABLE `segment_file`  (
        `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
        `file_path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件保存位置(用处不大)',
        `file_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文件名',
        `size` bigint NULL DEFAULT NULL COMMENT '文件大小,单位B',
        `segment_index` int NULL DEFAULT NULL COMMENT '已上传分片位置',
        `segment_size` int NULL DEFAULT NULL COMMENT '分片大小',
        `segment_total` int NULL DEFAULT NULL COMMENT '分片数量',
        `md5_key` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'MD5用来识别文件的唯一码',
        PRIMARY KEY (`id`) USING BTREE
      ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
      

      项目结构图

      3、前端实现

      上传界面

      <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>segment upload</title>
              <script src="http:///libs/jquery/2.1.4/jquery.min.js"></script>
              <script type="text/javascript" src="/js/md5.js"></script>
              <script type="text/javascript" src="/js/tool.js"></script>
              <script src="/js/upload.js"></script>
          </head>
          <body>
              <div id="top" class="center">
                  <p id="message"></p>
              </div>
              <div id="upload" class="center">
                  <h1>Segment File Upload</h1>
                  <input type="file" name="filename" id="filename" onchange="checkFile()"/>
                  <input type="button" id="submit" onclick="upload()" value="submit"/>
                  <span id="output">等待中</span>
              </div>
              <span id="uuid">uuid_name:</span>
              <span id="md5" style="margin-left:20px;">md5_key:</span>
          </body>
      </html>
      

      上传Js代码

      // 控制文件分片和上传
      // 不要忘记控制前端的显示结果
      // 简单尝试直接使用串行
      
      var key = ''
      var segmentIndex = 0
      var segmentSize = 2 * 1024 * 1024;  // 先2MB用着
      
      // 文件key计算
      function calFileKey(file){
          //把文件的信息存储为一个字符串
          var filedetails= file.name + file.size + file.type + file.lastModifiedDate;
          //使用当前文件的信息用md5加密生成一个key
          var key = hex_md5(filedetails);
          console.log(key)
          var key10 = parseInt(key,16);
          console.log(key10)
          //把加密的信息 转为一个62位的
          var key62 = Tool._10to62(key10);
          console.log("cal key:" + key62)
          return key62
      }
      
      // 计算分片数量
      // 注意分片序号从1开始
      function calTotalSegmentSize(file){
          var size = file.size
          var segmentTotal = Math.ceil(size / segmentSize)
          return segmentTotal;
      }
      
      // 计算分片的开始
      function calSegmentStartAndEnd(segmentIndex, file){
          var start = (segmentIndex - 1) * segmentSize;
          var end = Math.min(start + segmentSize, file.size);
          return [start, end];
      }
      
      // 检测当前文件是否存在,存在且完成上传则输出秒传信息
      // 存在但未完成,则将upload的segmentIndex修改,等待后续上传(把前端信息也修改一下)
      // 不存在则md5码(key),等待后续上传(把前端信息也修改一下)
      function checkFile(){
          var file = $('#filename').get(0).files[0]
          key = calFileKey(file)
          $('#md5').html('md5_key: ' + key)
          console.log(file.name)
      
          // ajax请求找下数据库中该文件是否存在
          $.ajax({
              url:"/checkFile",
              type:"post",
              cache: false,
              data: {
                  'key': key
              },
              dataType: 'json',
              success:function(data){
                  var result = data.success
                  if(!result){
                      $('#uuid').html('uuid_name:')
                      $('#output').html('该文件未上传')
                  }else{
                      var segmentFile = JSON.parse(data.message)
                      var segmentIndexNow = segmentFile.segmentIndex
                      var segmentTotal = segmentFile.segmentTotal
                      var uuid = segmentFile.fileName
                      $('#uuid').html('uuid_name: ' + uuid)
                      if(segmentIndexNow===segmentTotal){
                          // 完成上传
                          $('#output').html('该文件已完成上传')
                      }else{
                          $('#output').html(segmentIndexNow + '/' +segmentTotal)
                          segmentIndex = segmentIndexNow + 1
                      }
                  }
              },
              error:function(){
                  $('#output').html("check请求错误")
                  console.log("check请求错误")
              }
          })
      }
      
      // 总的上传方法,中间递归上传分片
      function upload(){
          var file = $('#filename').get(0).files[0]
          key = calFileKey(file)
          $('#md5').html('md5_key:' + key)
      
          // ajax请求找下数据库中该文件是否存在
          $.ajax({
              url:"/checkFile",
              type:"post",
              cache: false,
              data: {
                  'key': key
              },
              dataType: 'json',
              success:function(data){
                  var result = data.success
                  if(!result){
                      var segmentIndexNow = 0
                      var segmentTotal = calTotalSegmentSize(file)
                      $('#uuid').html('uuid_name:')
                      $('#output').html(segmentIndexNow + '/' +segmentTotal)
                      var segmentIndex = segmentIndexNow + 1
                      // 开始上传分片
                      uploadSegment(segmentIndex, file, key)
                  }else{
                      var segmentFile = JSON.parse(data.message)
                      var segmentIndexNow = segmentFile.segmentIndex
                      var segmentTotal = segmentFile.segmentTotal
                      var uuid = segmentFile.fileName
                      $('#uuid').html('uuid_name: ' + uuid)
                      if(segmentIndexNow==segmentTotal){
                          // 完成上传
                          $('#output').html('该文件已完成上传')
                      }else{
                          $('#output').html(segmentIndexNow + '/' +segmentTotal)
                          var segmentIndex = segmentIndexNow + 1
                          // 开始上传分片
                          uploadSegment(segmentIndex, file, key)
                      }
                  }
              },
              error:function(){
                  console.log("check请求错误")
              }
          })
      }
      
      
      // 上传分片
      function uploadSegment(segmentIndex, file, key){
          var fd = new FormData();
          var segmentIndex = segmentIndex;
          var sAe = calSegmentStartAndEnd(segmentIndex, file)
          var segmentStart = sAe[0]
          var segmentEnd = sAe[1]
          var segment = file.slice(segmentStart, segmentEnd)
          var segmentTotal = calTotalSegmentSize(file)
          var originFileName = file.name
      
          fd.append('file', segment)
          fd.append('fileSize', file.size)
          fd.append('segmentIndex', segmentIndex)
          fd.append('key', key)
          fd.append('segmentSize', segmentSize)
          fd.append('originFileName', originFileName)
      
          $.ajax({
              url:"/uploadSegment",
              type:"post",
              cache: false,
              data:fd,
              processData: false,
              contentType: false,
              success:function(data){
                  var result = data.success
                  if(!result){
                      $('#output').html(data.message)
                  }else{
                      var segmentFile = JSON.parse(data.message)
                      var uuid = segmentFile.fileName
                      $('#uuid').html('uuid_name: ' + uuid)
                      // 递归调用
                      $('#output').html(segmentIndex + "/" + segmentTotal)
                      if(segmentIndex < segmentTotal)
                          uploadSegment(segmentIndex+ 1, file, key)
                  }
              },error:function(){
                  console.log("分片" + segmentIndex + "上传失败")
              }
          })
      }
      

      4、后端实现

      4.1 持久化类与全局返回类

      @Data
      public class SegmentFile  {
          
          private int id;
          private String filePath;
          private String fileName;
          private long size;
          private int segmentIndex;
          private int segmentSize;
          private int segmentTotal;
          private String md5Key;
       }
       
       
      
      public class ReturnResult {
          private boolean success;
          private String message;
          
          public ReturnResult(boolean success, String message){
              this.success = success;
              this.message = message;
          }
      }
      

      4.2 Mapper接口

      Mapper接口,这边使用注解进行mybatis sql语句的配置

      @Mapper
      public interface SegmentFileMapper {
      
          // 获取对应的分片文件实体类
          @Select("select * from segment_file where md5_key = #{key}")
          @Results(id="segmentFileResult",value={
                  @Result(id=true, column = "id",property = "id"),
                  @Result(column = "file_path",property = "filePath"),
                  @Result(column = "file_name",property = "fileName"),
                  @Result(column = "size",property = "size"),
                  @Result(column = "segment_index",property = "segmentIndex"),
                  @Result(column = "segment_size",property = "segmentSize"),
                  @Result(column = "segment_total",property = "segmentTotal"),
                  @Result(column = "md5_key",property = "md5Key")
          })
          List<SegmentFile> getSegmentFileByKey(String key);
      
          // 添加对应的文件实体类
          @Insert("insert into segment_file(id,file_path,file_name," +
                  "size,segment_index,segment_size,segment_total,md5_key) " +
                  "values(#{id},#{filePath},#{fileName},#{size},#{segmentIndex}," +
                  "#{segmentSize},#{segmentTotal},#{md5Key})")
          int insertSegmentFile(SegmentFile segmentFile);
      
          // 主要用来更新分片信息
          @Update({"update segment_file set " +
                  "file_path = #{filePath},file_name = #{fileName},size = #{size}," +
                  "segment_index = #{segmentIndex}, segment_size = #{segmentSize}," +
                  "segment_total = #{segmentTotal}, md5_key = #{md5Key}" +
                  "where id = #{id}" })
          int updateSegmentFile(SegmentFile segmentFile);
      }
      

      4.3 文件工具类

      // 工具类
      // 文件名生成
      public class FileUtil {
          public static String getFileNameWithoutSuffix(String fileName){
              int suffixIndex = fileName.lastIndexOf('.');
              if(suffixIndex<0) {
                  return fileName;
              }
              return fileName.substring(0, suffixIndex);
          }
      
          public static String getFileSuffix(String fileName){
              int suffixIndex = fileName.lastIndexOf('.');
              if(suffixIndex<0) {
                  return "";
              }
              return fileName.substring(suffixIndex+1);
          }
      
          public static String getSegmentName(String fileName, int segmentIndex){
              return fileName + "#" + segmentIndex;
          }
      
          public static String createSaveFileName(String key, String fileName){
              String suffix = getFileSuffix(fileName);
              return key + "." + suffix;
          }
      
          public static String createUUIDFileName(String fileName){
              String suffix = getFileSuffix(fileName);
              String name = UUID.randomUUID().toString();
              return name + "." + suffix;
          }
      }
      

      4.4 Service分片服务类

      比较关键的业务类,包括文件存在确认,文件记录创建,文件信息更新,分片存储,分片合并和分片文件删除功能的实现

      // 分片存储
      // 文件存在确认
      // 文件整合
      @Service
      @Slf4j
      public class SegmentFileService {
      
          @Value("${file.temp}")
          private String tempFileDir;
      
      
          private final SegmentFileMapper segmentFileMapper;
      
          public SegmentFileService(SegmentFileMapper segmentFileMapper) {
              this.segmentFileMapper = segmentFileMapper;
          }
      
          /**
           * 该文件存在,返回数据
          */
          public SegmentFile checkSegmentFile(String key){
              List<SegmentFile> segmentFiles = segmentFileMapper.getSegmentFileByKey(key);
              if(segmentFiles!=null&&segmentFiles.size()>0) {
                  return segmentFiles.get(0);
              } else {
                  return null;
              }
          }
          /**
           * 第一次出现的文件,把数据存到数据库中
           * savePath为文件夹绝对位置
          */
          public boolean createSegmentFile(String originFileName, String savePath, long size, int segmentSize, String key){
              String saveFileName = FileUtil.createSaveFileName(key, originFileName);
              SegmentFile segmentFile = new SegmentFile();
              // filepath为完整路径
              segmentFile.setFilePath(savePath + saveFileName);
              segmentFile.setFileName(saveFileName);
              segmentFile.setSize(size);
              segmentFile.setSegmentIndex(0);
              segmentFile.setSegmentSize(segmentSize);
      
              int total = (int) (size / segmentSize);
              if(size % segmentSize != 0) {
                  total++;
              }
              segmentFile.setSegmentTotal(total);
              segmentFile.setMd5Key(key);
      
              return segmentFileMapper.insertSegmentFile(segmentFile) > 0;
          }
      
          /**
           * 存储分片到服务器
          */
          public boolean saveSegment(MultipartFile file,String key, String originFileName, int segmentIndex){
              String saveFileName = FileUtil.createSaveFileName(key, originFileName);
              String segmentFileName = FileUtil.getSegmentName(saveFileName, segmentIndex);
              // 存储分片,方便之后使用
              boolean saveSuccess = upload(file,  tempFileDir +segmentFileName);
              if(saveSuccess){
                  // 修改数据库中分片记录
                  SegmentFile segmentFile = segmentFileMapper.getSegmentFileByKey(key).get(0);
                  segmentFile.setSegmentIndex(segmentFile.getSegmentIndex()+1);
                  // 文件信息更新
                   int row = segmentFileMapper.updateSegmentFile(segmentFile);
                  return row > 0;
              }else {
                  return false;
              }
          }
      
          /**
           * 将所有的分片联合成同一文件
          */
          public boolean mergeSegment(String key) {
              SegmentFile segmentFile = segmentFileMapper.getSegmentFileByKey(key).get(0);
              int segmentCount = segmentFile.getSegmentTotal();
              FileInputStream fileInputStream = null;
              FileOutputStream outputStream = null;
              byte[] byt = new byte[10 * 1024 * 1024];
              try {
                  // 整合结果文件
                  File newFile = new File(segmentFile.getFilePath());
                  outputStream = new FileOutputStream(newFile, true);
                  int len;
                  for (int i = 0; i < segmentCount; i++) {
                      String segmentFilePath = FileUtil.getSegmentName(tempFileDir + segmentFile.getFileName(), i + 1);
                      fileInputStream = new FileInputStream(segmentFilePath);
                      while ((len = fileInputStream.read(byt)) != -1) {
                          outputStream.write(byt, 0, len);
                      }
                  }
              } catch (IOException e) {
                  log.error("分片合并异常,异常原因:",e);
                  return false;
              } finally {
                  try {
                      if (fileInputStream != null) {
                          fileInputStream.close();
                      }
                      if (outputStream != null) {
                          outputStream.close();
                      }
                      log.info("IO流正常关闭");
                  } catch (Exception e) {
                      log.error("IO流关闭异常,异常原因:",e);
                  }
              }
              log.info("分片合并成功");
              return true;
          }
      
          /**
           * 完成合并,删除分片文件
          */
          public void deleteSegments(String key) throws InterruptedException {
              // 为了保证不被占用,先回收数据流对象
              System.gc();
              Thread.sleep(1000);
              SegmentFile segmentFile = segmentFileMapper.getSegmentFileByKey(key).get(0);
              int segmentCount = segmentFile.getSegmentTotal();
              List<String> remain = new ArrayList<>();
              int finished = 0;
              int[] visited = new int[segmentCount];
              for (int i = 0; i < segmentCount; i++) {
                  String segmentFilePath = FileUtil.getSegmentName(tempFileDir + segmentFile.getFileName(), i + 1);
                  remain.add(segmentFilePath);
                  File file = new File(segmentFilePath);
                  boolean result = file.delete();
                  if(result) {
                      finished++;
                      visited[i] = 1;
                  }
                  log.info("分片文件: {} 删除 {}" , segmentFilePath, result?"成功":"失败");
              }
              // visited数组,然后完成了再去除,知道count到达总数;二次确认删除
              while(finished<segmentCount){
                  System.gc();
                  Thread.sleep(1000);
                  for(int i=0;i<segmentCount;i++){
                      if(visited[i]==0){
                          String segmentFilePath = FileUtil.getSegmentName(segmentFile.getFilePath(), i + 1);
                          remain.add(segmentFilePath);
                          File file = new File(segmentFilePath);
                          boolean result = file.delete();
                          if(result){
                              visited[i] = 1;
                              finished++;
                          }
                          log.info("分片文件: {} 删除 {}" , segmentFilePath, result?"成功":"失败");
                      }
                  }
              }
          }
      
          /**
           * 存储方法
          */
          private boolean upload(MultipartFile file, String path){
              File dest = new File(path);
              //判断文件父目录是否存在
              if (!dest.getParentFile().exists()) {
                  boolean b = dest.getParentFile().mkdir();
                  if(!b){
                      return false;
                  }
              }
              //保存文件
              try {
                  file.transferTo(dest);
                  return true;
              } catch (IllegalStateException | IOException e) {
                  e.printStackTrace();
                  return false;
              }
          }
      
      }
      
      

      4.5 Controller类

      主要是两个方法,一个是判断当前文件上传状况,主要就是想在前端选中文件后就调用一下,显示文件上传状态,这样就能实现秒传功能的效果了。第二个就是上传分片功能

      /**
       * 主要实现check文件存在与否
       * 上传分片/整合分片
       * 这里一定要@Controller,否则就不会跳转到static下了
      */
      @Controller
      @Slf4j
      public class SegmentFileController {
      
          @Autowired
          SegmentFileService segmentFileService;
      
          @Autowired
          private ObjectMapper mapper;
      
          @Value("${file.save-path}")
          private String savePath;
      
      
          @RequestMapping("/index")
          public String index(){
              return "/index.html";
          }
      
          @RequestMapping("/upload")
          public String upload(){
              return "/pages/upload.html";
          }
      
      
          @RequestMapping("/checkFile")
          @ResponseBody
          // 检查文件是否已经存在,且返回segment信息
          public ReturnResult checkFileExist(String key) throws JsonProcessingException {
              SegmentFile segmentFile = segmentFileService.checkSegmentFile(key);
              if(segmentFile==null) {
                  log.warn("该文件未上传,md5:{}",key);
                  return new ReturnResult(false, "该文件未上传");
              } else{
                  // 转成json回去用
                  String fileJson = mapper.writeValueAsString(segmentFile);
                  return new ReturnResult(true, fileJson);
              }
          }
      
      
          /**
           * 主要方法流程
           * 上传文件需要从前端取分片序号和分片大小,因为切割是前端切滴,所以文件原始大小也要返回来
           * 剩余信息在service中计算
           * 首先确认是否存在该文件,不存在就放到数据库中新建
           * 之后对segmentIndex分别处理,存储分片文件(文件分片前端完成)
           * 简化情况,认为前端都是异步请求,并且分片是按顺序请求的,只有前面的index处理了才能处理后面的分片(在前端体现)
           * 这样当segmentIndex和总count相同时,获取结果
           * 最后如果失败了,需要删除数据库的记录,这样就可以让用户再次上传
          */
          @RequestMapping("/uploadSegment")
          @ResponseBody
          public ReturnResult upLoadSegmentFile(MultipartFile file, String originFileName, long fileSize, Integer segmentIndex, Integer segmentSize, String key) throws JsonProcessingException{
              log.info("分片文件 {} 上传开始",originFileName);
              // 查找是否存在,不存在就写入
              SegmentFile segmentFile = segmentFileService.checkSegmentFile(key);
              if(segmentFile==null){
                  boolean writeSuccess = segmentFileService.createSegmentFile(originFileName, savePath, fileSize, segmentSize, key);
                  if(!writeSuccess){
                      // 写入失败,返回错误信息
                      log.warn("文件数据库记录创建失败");
                      return new ReturnResult(false, "文件数据库记录创建失败");
                  }
              }
              segmentFile = segmentFileService.checkSegmentFile(key);
              // 将当前分片存入
              boolean segmentWriteSuccess = segmentFileService.saveSegment(file, key, originFileName, segmentIndex);
              if(!segmentWriteSuccess) {
                  log.warn("分片文件存储失败");
                  // 分片存储失败
                  return new ReturnResult(false, "分片文件存储失败");
              }
      
              class deleteThread implements Runnable{
                  @Override
                  public void run() {
                      try {
                          segmentFileService.deleteSegments(key);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
      
              // 判断是否分片齐全,齐全则合并生成究极文件
              // 其实考虑这步会不会失败应该在数据库再加一个值
              if(segmentIndex==segmentFile.getSegmentTotal()){
                  boolean mergeSuccess = segmentFileService.mergeSegment(key);
                  if(mergeSuccess) {
                      // 另开线程去自旋删除
                      new Thread(new deleteThread()).start();
                      return new ReturnResult(true, mapper.writeValueAsString(segmentFile));
                  }
                  else {
                      log.warn("文件合并失败");
                      return new ReturnResult(false, "文件合并失败");
                  }
              }
              return new ReturnResult(true, mapper.writeValueAsString(segmentFile));
          }
      
      
      }
      
      

      5、总结

      因为是默认串行调用,文件已上传分片信息直接用当前上传的分片序号覆盖。如果要并行实现的话,数据库中可能需要存储一个总分片数量大小长度的字符串,用来记录上传进度(状态压缩),比如111011,表示6个分片,分片4未上传,这样就能并行上传分片了

      三、进阶方案

      1、介绍

      这篇文章写的也不错,使用的是Vue+SpringBoot大文件上传,可以参考:SpringBoot 实现大文件分片上传、断点续传及秒传

      前端部分可以使用百度或者使用第三方的上传组件;后端用两种方式实现文件写入,一种是用RandomAccessFile(参考:https:///dimudan2015/article/details/81910690);另一种是使用MappedByteBuffer(参考:https:///p/f90866dcbffc)

      使用该种方法要注意每一个分块的记录,因为通过偏移量存储文件的方式是直接操作源文件的,并不会生成一块块的分片文件,分片文件使用Mysql或者Redis进行记录,最后传输成功后记录完整的文件信息。

      2、核心代码介绍

      文件操作核心模板类代码

      @Slf4j
      public abstract class SliceUploadTemplate implements SliceUploadStrategy {
      
        public abstract boolean upload(FileUploadRequestDTO param);
      
        protected File createTmpFile(FileUploadRequestDTO param) {
      
          FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);
          param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));
          String fileName = param.getFile().getOriginalFilename();
          String uploadDirPath = filePathUtil.getPath(param);
          String tempFileName = fileName + "_tmp";
          File tmpDir = new File(uploadDirPath);
          File tmpFile = new File(uploadDirPath, tempFileName);
          if (!tmpDir.exists()) {
            tmpDir.mkdirs();
          }
          return tmpFile;
        }
      
        @Override
        public FileUploadDTO sliceUpload(FileUploadRequestDTO param) {
      
          boolean isOk = this.upload(param);
          if (isOk) {
            File tmpFile = this.createTmpFile(param);
            FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);
            return fileUploadDTO;
          }
          String md5 = FileMD5Util.getFileMD5(param.getFile());
      
          Map<Integer, String> map = new HashMap<>();
          map.put(param.getChunk(), md5);
          return FileUploadDTO.builder().chunkMd5Info(map).build();
        }
      
        /**
         * 检查并修改文件上传进度
         */
        public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) {
      
          String fileName = param.getFile().getOriginalFilename();
          File confFile = new File(uploadDirPath, fileName + ".conf");
          byte isComplete = 0;
          RandomAccessFile accessConfFile = null;
          try {
            accessConfFile = new RandomAccessFile(confFile, "rw");
            //把该分段标记为 true 表示完成
            System.out.println("set part " + param.getChunk() + " complete");
            //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127
            accessConfFile.setLength(param.getChunks());
            accessConfFile.seek(param.getChunk());
            accessConfFile.write(Byte.MAX_VALUE);
      
            //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)
            byte[] completeList = FileUtils.readFileToByteArray(confFile);
            isComplete = Byte.MAX_VALUE;
            for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
              //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
              isComplete = (byte) (isComplete & completeList[i]);
              System.out.println("check part " + i + " complete?:" + completeList[i]);
            }
      
          } catch (IOException e) {
            log.error(e.getMessage(), e);
          } finally {
            FileUtil.close(accessConfFile);
          }
          boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);
          return isOk;
        }
      
        /**
         * 把上传进度信息存进redis
         */
        private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath,
            String fileName, File confFile, byte isComplete) {
      
          RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);
          if (isComplete == Byte.MAX_VALUE) {
            redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true");
            redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5());
            confFile.delete();
            return true;
          } else {
            if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) {
              redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false");
              redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(),
                  uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf");
            }
      
            return false;
          }
        }
      
        /**
         * 保存文件操作
         */
        public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) {
      
          FileUploadDTO fileUploadDTO = null;
      
          try {
      
            fileUploadDTO = renameFile(tmpFile, fileName);
            if (fileUploadDTO.isUploadComplete()) {
              System.out
                  .println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName);
              //TODO 保存文件信息到数据库
      
            }
      
          } catch (Exception e) {
            log.error(e.getMessage(), e);
          } finally {
      
          }
          return fileUploadDTO;
        }
      
        /**
         * 文件重命名
         *
         * @param toBeRenamed 将要修改名字的文件
         * @param toFileNewName 新的名字
         */
        private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) {
          //检查要重命名的文件是否存在,是否是文件
          FileUploadDTO fileUploadDTO = new FileUploadDTO();
          if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            log.info("File does not exist: {}", toBeRenamed.getName());
            fileUploadDTO.setUploadComplete(false);
            return fileUploadDTO;
          }
          String ext = FileUtil.getExtension(toFileNewName);
          String p = toBeRenamed.getParent();
          String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName;
          File newFile = new File(filePath);
          //修改文件名
          boolean uploadFlag = toBeRenamed.renameTo(newFile);
      
          fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());
          fileUploadDTO.setUploadComplete(uploadFlag);
          fileUploadDTO.setPath(filePath);
          fileUploadDTO.setSize(newFile.length());
          fileUploadDTO.setFileExt(ext);
          fileUploadDTO.setFileId(toFileNewName);
      
          return fileUploadDTO;
        }
      
      }
      

      RandomAccessFile实现方式

      @UploadMode(mode = UploadModeEnum.RANDOM_ACCESS)
      @Slf4j
      public class RandomAccessUploadStrategy extends SliceUploadTemplate {
      
        @Autowired
        private FilePathUtil filePathUtil;
      
        @Value("${upload.chunkSize}")
        private long defaultChunkSize;
      
        @Override
        public boolean upload(FileUploadRequestDTO param) {
          RandomAccessFile accessTmpFile = null;
          try {
            String uploadDirPath = filePathUtil.getPath(param);
            File tmpFile = super.createTmpFile(param);
            accessTmpFile = new RandomAccessFile(tmpFile, "rw");
            //这个必须与前端设定的值一致
            long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024
                : param.getChunkSize();
            long offset = chunkSize * param.getChunk();
            //定位到该分片的偏移量
            accessTmpFile.seek(offset);
            //写入该分片数据
            accessTmpFile.write(param.getFile().getBytes());
            boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
            return isOk;
          } catch (IOException e) {
            log.error(e.getMessage(), e);
          } finally {
            FileUtil.close(accessTmpFile);
          }
          return false;
        }
      }
      

      MappedByteBuffer实现方式

      @UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)
      @Slf4j
      public class MappedByteBufferUploadStrategy extends SliceUploadTemplate {
      
        @Autowired
        private FilePathUtil filePathUtil;
      
        @Value("${upload.chunkSize}")
        private long defaultChunkSize;
      
        @Override
        public boolean upload(FileUploadRequestDTO param) {
      
          RandomAccessFile tempRaf = null;
          FileChannel fileChannel = null;
          MappedByteBuffer mappedByteBuffer = null;
          try {
            String uploadDirPath = filePathUtil.getPath(param);
            File tmpFile = super.createTmpFile(param);
            tempRaf = new RandomAccessFile(tmpFile, "rw");
            fileChannel = tempRaf.getChannel();
      
            long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024
                : param.getChunkSize();
            //写入该分片数据
            long offset = chunkSize * param.getChunk();
            byte[] fileData = param.getFile().getBytes();
            mappedByteBuffer = fileChannel
                .map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
            mappedByteBuffer.put(fileData);
            boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
            return isOk;
      
          } catch (IOException e) {
            log.error(e.getMessage(), e);
          } finally {
      
            FileUtil.freedMappedByteBuffer(mappedByteBuffer);
            FileUtil.close(fileChannel);
            FileUtil.close(tempRaf);
      
          }
      
          return false;
        }
      
      }
      

      四、Gzip 压缩超大 json 对象上传(加餐)

      1、概述

      1.1 业务背景

      一个通过Json传值的接口需要传大量数据,例如一个广告接口,内部系统有一个广告保存接口,需要ADX那边将投放的广告数据进行保存供后续使用,其中一个字段存放了广告渲染的HTML代码,因此,对与请求数据那么大的接口我们肯定是需要作一个优化,否则太大的数据传输有以下几个弊端:

      • 占用网络带宽,而有些云产品就是按照带宽来计费的,间接浪费了钱
      • 传输数据大导致网络传输耗时

      1.2 实现思路

      请求广告保存接口时先将Json对象字符串进行GZIP压缩,那请求时传入的就是压缩后的数据,而GZIP的压缩效率是很高的,因此可以大大减小传输数据,而当数据到达广告保存接口前再将传来的数据进行解压缩,还原成JSON对象就完成了整个GZIP压缩数据的请求以及处理流程

      • 对与需要占用而外的CPU计算资源来说,内部系统属于IO密集型应用,因此用一些CPU资源来换取更快的网络传输其实是很划算的
      • 使用过滤器在请求数据到达Controller之前对数据进行解压缩处理后重新写回到Body中,避免影响Controller的逻辑,代码零侵入
      • 而对于改造接口的同时是否会影响到原来的接口这一点可以通过 HttpHeader 的Content-Encoding=gzip属性来区分是否需要对请求数据进行解压缩

      1.3 前置知识

      • Http 请求结构以及Content-Encoding 属性
      • gzip压缩方式
      • Servlet Filter
      • HttpServletRequestWrapper
      • Spring Boot
      • Java 输入输出流

      1.4 实现流程

      Spring之秒传、断点续传、分片上传和压缩

      2、基础知识介绍

      过滤器和拦截器的文章可以参考:
      SpringBoot 过滤器、拦截器、监听器对比及使用场景!
      SpringBoot的过滤器和拦截器和全局异常处理
      Filter过滤器和Interceptor拦截器配置和生命周期

      2.1 过滤器与拦截器介绍

      • Listener 监听

        Listener 可以监听 web 服务器中某一个事件操作,并触发注册的回调函数。通俗的语言就是在 application,session,request 三个对象创建/消亡或者增删改属性时,自动执行代码的功能组件。

      • Servlet

        Servlet 是一种运行服务器端的 java 应用程序,具有 独立于平台和协议的特性,并且可以动态的生成 web 页面,它工作在 客户端请求与服务器响应 的中间层。

      • 过滤器 Filter

        Filter对用户请求进行预处理,接着将请求交给 Servlet 进行处理并生成响应,最后 Filter 再对服务器响应进行后处理。Filter 是可以复用的代码片段,常用来转换 HTTP 请求、响应和头信息。Filter 不像 Servlet,它不能产生响应,而是只修改对某一资源的请求或者响应。

      • 拦截器 Interceptor

        类似面向切面编程(AOP)中的切面和通知,我们通过动态代理对一个 service() 方法添加通知进行功能增强。比如说在方法执行前进行初始化处理,在方法执行后进行后置处理。拦截器的思想和AOP 类似,区别就是拦截器只能对 Controller 的 HTTP 请求进行拦截

      2.2 Filter 与 Interceptor 区别

      • Filter 是基于函数回调的,而 Interceptor 则是基于 Java 反射 和 动态代理。
      • Filter 依赖于 Servlet 容器,遵循Servlet规范,而 Interceptor 依赖于spring容器,遵循Spring规范。
      • Filter 对几乎 所有的请求 起作用,但过滤器的控制比较粗,只能在请求进来时进行处理,对请求和响应进行包装,而 Interceptor 只对 Controller 对请求起作用,但更精细的控制,可以在controller对请求处理之前或之后被调用,也可以在渲染视图呈现给用户之后调用

      2.3 执行顺序

      • Filter 过滤请求处理
      • Interceptor 拦截请求处理
      • Aspect(切面) 拦截处理请求(这里不赘述)
      • 对应的 HandlerAdapter 处理请求
      • Aspect(切面) 拦截响应请求(这里不赘述)
      • Interceptor 拦截响应处理
      • Interceptor 的最终处理
      • Filter 过滤响应处理

      3、核心代码

      3.1 配置controller

      创建一个SpringBoot项目,先编写一个接口,功能很简单就是传入一个Json对象并返回,以模拟将广告数据保存到数据库

      @Slf4j
      @RestController
      public class AdvertisingController {
      
          @PostMapping("/save")
          public Advertising saveProject(@RequestBody Advertising advertising) {
              log.info("获取内容"+ advertising);
              return advertising;
          }
      }
      
      
      
      @Data
      public class Advertising {
          private String adName;
          private String adTag;
      }
      

      3.2 压缩工具类

      public class GZIPUtils {
          
          // 这是我自己的测试,然后生成文件访问
          public static void main(String[] args) throws Exception {
              byte[] compress = compress("{\n" +
                      "    \"adName\":\"1123\",\n" +
                      "    \"adTag\":\"balabalbalalalallala\"\n" +
                      "}");
              saveFile("test",compress);
      
      
          }
      
          public static final String GZIP_ENCODE_UTF_8 = "UTF-8";
      
          /**
           * 字符串压缩为GZIP字节数组
           *
           * @param str
           * @return
           */
          public static byte[] compress(String str) {
              return compress(str, GZIP_ENCODE_UTF_8);
          }
      
          /**
           * 字符串压缩为GZIP字节数组
           *
           * @param str
           * @param encoding
           * @return
           */
          public static byte[] compress(String str, String encoding) {
              if (str == null || str.length() == 0) {
                  return null;
              }
              ByteArrayOutputStream out = new ByteArrayOutputStream();
              GZIPOutputStream gzip;
              try {
                  gzip = new GZIPOutputStream(out);
                  gzip.write(str.getBytes(encoding));
                  gzip.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
              return out.toByteArray();
          }
      
          /**
           * GZIP解压缩
           *
           * @param bytes
           * @return
           */
          public static byte[] uncompress(byte[] bytes) {
              if (bytes == null || bytes.length == 0) {
                  return null;
              }
              ByteArrayOutputStream out = new ByteArrayOutputStream();
              ByteArrayInputStream in = new ByteArrayInputStream(bytes);
              try {
                  GZIPInputStream ungzip = new GZIPInputStream(in);
                  byte[] buffer = new byte[256];
                  int n;
                  while ((n = ungzip.read(buffer)) >= 0) {
                      out.write(buffer, 0, n);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
              return out.toByteArray();
          }
      
          /**
           * 解压并返回String
           *
           * @param bytes
           * @return
           */
          public static String uncompressToString(byte[] bytes) throws IOException {
              return uncompressToString(bytes, GZIP_ENCODE_UTF_8);
          }
      
          /**
           * @param bytes
           * @return
           */
          public static byte[] uncompressToByteArray(byte[] bytes) throws IOException {
              return uncompressToByteArray(bytes, GZIP_ENCODE_UTF_8);
          }
      
          /**
           * 解压成字符串
           *
           * @param bytes    压缩后的字节数组
           * @param encoding 编码方式
           * @return 解压后的字符串
           */
          public static String uncompressToString(byte[] bytes, String encoding) throws IOException {
              byte[] result = uncompressToByteArray(bytes, encoding);
              return new String(result);
          }
      
          /**
           * 解压成字节数组
           *
           * @param bytes
           * @param encoding
           * @return
           */
          public static byte[] uncompressToByteArray(byte[] bytes, String encoding) throws IOException {
              if (bytes == null || bytes.length == 0) {
                  return null;
              }
              ByteArrayOutputStream out = new ByteArrayOutputStream();
              ByteArrayInputStream in = new ByteArrayInputStream(bytes);
              try {
                  GZIPInputStream ungzip = new GZIPInputStream(in);
                  byte[] buffer = new byte[256];
                  int n;
                  while ((n = ungzip.read(buffer)) >= 0) {
                      out.write(buffer, 0, n);
                  }
                  return out.toByteArray();
              } catch (IOException e) {
                  e.printStackTrace();
                  throw new IOException("解压缩失败!");
              }
          }
      
          /**
           * 将字节流转换成文件
           *
           * @param filename
           * @param data
           * @throws Exception
           */
          public static void saveFile(String filename, byte[] data) throws Exception {
              if (data != null) {
                  String filepath = "/" + filename;
                  File file = new File(filepath);
                  if (file.exists()) {
                      file.delete();
                  }
                  FileOutputStream fos = new FileOutputStream(file);
                  fos.write(data, 0, data.length);
                  fos.flush();
                  fos.close();
                  System.out.println(file);
              }
          }
      }
      

      3.3 编写过滤器

      首先编写一个自定义过滤器

      @Slf4j
      @Component
      public class GZIPFilter implements Filter {
      
          private static final String CONTENT_ENCODING = "Content-Encoding";
          private static final String CONTENT_ENCODING_TYPE = "gzip";
      
          @Override
          public void init(FilterConfig filterConfig) {
              log.info("init GZIPFilter");
          }
      
          @Override
          public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
              long start = System.currentTimeMillis();
              HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
      
              String encodeType = httpServletRequest.getHeader(CONTENT_ENCODING);
              if (CONTENT_ENCODING_TYPE.equals(encodeType)) {
                  log.info("请求:{} 需要解压", httpServletRequest.getRequestURI());
                  UnZIPRequestWrapper unZIPRequestWrapper = new UnZIPRequestWrapper(httpServletRequest);
                  filterChain.doFilter(unZIPRequestWrapper,servletResponse);
              }
              else {
                  log.info("请求:{} 无需解压", httpServletRequest.getRequestURI());
                  filterChain.doFilter(servletRequest,servletResponse);
              }
              log.info("耗时:{}ms", System.currentTimeMillis() - start);
          }
      
          @Override
          public void destroy() {
              log.info("destroy GZIPFilter");
          }
      

      实现RequestWrapper实现解压和写回Body的逻辑

      /**
       * @Description: JsonString经过压缩后保存为二进制文件 -> 解压缩后还原成JsonString转换成byte[] 写回body中
       */
      @Slf4j
      public class UnZIPRequestWrapper extends HttpServletRequestWrapper {
      
          private final byte[] bytes;
      
          public UnZIPRequestWrapper(HttpServletRequest request) throws IOException {
              super(request);
              try (BufferedInputStream bis = new BufferedInputStream(request.getInputStream());
                   ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                  final byte[] body;
                  byte[] buffer = new byte[1024];
                  int len;
                  while ((len = bis.read(buffer)) > 0) {
                      baos.write(buffer, 0, len);
                  }
                  body = baos.toByteArray();
                  if (body.length == 0) {
                      log.info("Body无内容,无需解压");
                      bytes = body;
                      return;
                  }
                  this.bytes = GZIPUtils.uncompressToByteArray(body);
              } catch (IOException ex) {
                  log.info("解压缩步骤发生异常!");
                  ex.printStackTrace();
                  throw ex;
              }
          }
      
          @Override
          public ServletInputStream getInputStream() throws IOException {
              final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
              return new ServletInputStream() {
      
                  @Override
                  public boolean isFinished() {
                      return false;
                  }
      
                  @Override
                  public boolean isReady() {
                      return false;
                  }
      
                  @Override
                  public void setReadListener(ReadListener readListener) {
      
                  }
      
                  @Override
                  public int read() throws IOException {
                      return byteArrayInputStream.read();
                  }
              };
          }
      
          @Override
          public BufferedReader getReader() throws IOException {
              return new BufferedReader(new InputStreamReader(this.getInputStream()));
          }
      
      }
      
      

      注册过滤器,也可以用注解@WebFilter(value = "/*",filterName ="AFilter" )实现

      @Configuration
      public class FilterRegistration {
      
          @Resource
          private GZIPFilter gzipFilter;
      
          @Bean
          public FilterRegistrationBean<GZIPFilter> gzipFilterRegistrationBean() {
              FilterRegistrationBean<GZIPFilter> registration = new FilterRegistrationBean<>();
              //Filter可以new,也可以使用依赖注入Bean
              registration.setFilter(gzipFilter);
              //过滤器名称
              registration.setName("gzipFilter");
              //拦截路径
              registration.addUrlPatterns("/*");
              //设置顺序
              registration.setOrder(1);
              return registration;
          }
      }
      

      4、测试

      注意一个大坑:千万不要直接将压缩后的byte[]当作字符串进行传输,否则你会发现压缩后的请求数据竟然比没压缩后的要大得多🐶!一般有两种传输压缩后的byte[]的方式:

      • 将压缩后的byet[]进行base64编码再传输字符串,这种方式会损失掉一部分GZIP的压缩效果,适用于压缩结果要存储在Redis中的情况
      • 将压缩后的byte[]以二进制的形式写入到文件中,请求时直接在body中带上文件即可,用这种方式可以不损失压缩效果

      测试的时候注意在请求头Headers里带上Content-Type=application/json和Content-Encoding=gzip,带上gzip就是就会进行解压,不带就正常

      版权声明:本文内容来自第三方投稿或授权转载,原文地址:https://blog.csdn.net/lemon_TT/article/details/127500473,作者:魅Lemon,版权归原作者所有。本网站转在其作品的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如因作品内容、版权等问题需要同本网站联系,请发邮件至ctyunbbs@chinatelecom.cn沟通。

      上一篇:Linux:ansible-playbook配置文件(剧本)(进阶)

      下一篇:使用java底层实现邮件的发送(含测试,源码)

      相关文章

      2025-05-16 09:15:10

      52.介绍AOP有几种实现方式

      52.介绍AOP有几种实现方式

      2025-05-16 09:15:10
      gt , lt , Spring
      2025-05-14 10:02:58

      java项目多端数据同步解决方案

      多端数据同步是指在多个设备(例如桌面应用、移动应用、Web应用)之间保持数据的一致性。

      2025-05-14 10:02:58
      java , Spring , WebSocket , 同步 , 数据 , 版本号
      2025-05-09 08:50:35

      SpringBoot学习(1)

      Spring Boot是Spring提供的一个子项目,用于快速构建Spring应用程序。

      2025-05-09 08:50:35
      Spring , 依赖 , 应用程序 , 部署 , 配置 , 配置文件
      2025-05-09 08:20:32

      系统架构——Spring Framework

      Spring Framework 是 Spring 生态圈中最基础的项目。其它所有的项目的都是在它的基础上运行使用。

      2025-05-09 08:20:32
      AOP , Data , Spring , 学习 , 模块
      2025-05-06 09:19:51

      springboot系列教程(三十二):SpringBoot 教程之处理异步请求

      springboot系列教程(三十二):SpringBoot 教程之处理异步请求

      2025-05-06 09:19:51
      Spring , 异步 , 方法 , 返回值
      2025-05-06 09:19:12

      Spring多线程事务 能否保证事务的一致性(同时提交、同时回滚)?

      Spring的事务信息是存在ThreadLocal中的Connection, 所以一个线程永远只能有一个事务

      2025-05-06 09:19:12
      Spring , 事务 , 多线程 , 线程
      2025-04-22 09:28:19

      61. Spring事务传播行为实现原理

      61. Spring事务传播行为实现原理

      2025-04-22 09:28:19
      Spring , ThreadLocal , 事务
      2025-04-18 07:10:30

      深入解析 Spring Security —— 打造高效安全的权限管理体系

      Spring Security 是一个专注于认证与授权的安全框架,致力于保护 Web 应用程序的资源安全。通过高度灵活的配置和模块化的设计,它能够满足各种复杂的安全需求。

      2025-04-18 07:10:30
      Security , Spring , 权限 , 用户 , 角色 , 认证 , 配置
      2025-04-18 07:09:19

      深入理解Spring中的Bean循环依赖与解决机制

      Bean循环依赖是指两个或多个Bean之间相互依赖,形成依赖闭环的情况。例如,Bean A依赖Bean B,而Bean B又依赖Bean A。这种情况下,如果没有特殊处理,容器将无法正确初始化这些Bean,从而导致应用启动失败。

      2025-04-18 07:09:19
      Bean , Spring , 依赖 , 初始化 , 循环 , 缓存 , 解决
      2025-04-01 10:28:07

      Spring Boot 多环境开发配置详解:Profiles 的使用指南

      Spring Boot 多环境开发配置详解:Profiles 的使用指南

      2025-04-01 10:28:07
      application , profile , Spring , yml , 文件 , 环境 , 配置
      查看更多
      推荐标签

      作者介绍

      天翼云小翼
      天翼云用户

      文章

      33561

      阅读量

      5222070

      查看更多

      最新文章

      52.介绍AOP有几种实现方式

      2025-05-16 09:15:10

      java项目多端数据同步解决方案

      2025-05-14 10:02:58

      系统架构——Spring Framework

      2025-05-09 08:20:32

      springboot系列教程(三十二):SpringBoot 教程之处理异步请求

      2025-05-06 09:19:51

      Spring多线程事务 能否保证事务的一致性(同时提交、同时回滚)?

      2025-05-06 09:19:12

      61. Spring事务传播行为实现原理

      2025-04-22 09:28:19

      查看更多

      热门文章

      使用Spring的AOP时报错“ClassCastException: com.sun.proxy.$Proxy5 cannot be cast to com.demo.aop.Target“

      2023-06-14 09:12:07

      Ajax续

      2023-07-11 08:55:43

      Spring-AOP

      2023-05-25 14:43:25

      《Spring Cloud Config官方文档》之推送通知和Spring Cloud总线

      2022-11-08 07:33:17

      《Spring Boot官方指南》28.安全

      2023-02-15 10:01:11

      Spring-AOP的通知类型

      2023-07-06 09:42:12

      查看更多

      热门标签

      java Java python 编程开发 代码 开发语言 算法 线程 Python html 数组 C++ 元素 javascript c++
      查看更多

      相关产品

      弹性云主机

      随时自助获取、弹性伸缩的云服务器资源

      天翼云电脑(公众版)

      便捷、安全、高效的云电脑服务

      对象存储

      高品质、低成本的云上存储服务

      云硬盘

      为云上计算资源提供持久性块存储

      查看更多

      随机文章

      如何优化Spring Boot应用的性能?

      重看Spring聚焦Environment分析

      《Spring 5 官方文档》整合EJB

      看山聊并发:Java 中 Vector 和 SynchronizedList 的区别

      Spring依赖注入(DI)详解

      结合RBAC模型讲解权限管理系统需求及表结构创建

      • 7*24小时售后
      • 无忧退款
      • 免费备案
      • 专家服务
      售前咨询热线
      400-810-9889转1
      关注天翼云
      • 旗舰店
      • 天翼云APP
      • 天翼云微信公众号
      服务与支持
      • 备案中心
      • 售前咨询
      • 智能客服
      • 自助服务
      • 工单管理
      • 客户公告
      • 涉诈举报
      账户管理
      • 管理中心
      • 订单管理
      • 余额管理
      • 发票管理
      • 充值汇款
      • 续费管理
      快速入口
      • 天翼云旗舰店
      • 文档中心
      • 最新活动
      • 免费试用
      • 信任中心
      • 天翼云学堂
      云网生态
      • 甄选商城
      • 渠道合作
      • 云市场合作
      了解天翼云
      • 关于天翼云
      • 天翼云APP
      • 服务案例
      • 新闻资讯
      • 联系我们
      热门产品
      • 云电脑
      • 弹性云主机
      • 云电脑政企版
      • 天翼云手机
      • 云数据库
      • 对象存储
      • 云硬盘
      • Web应用防火墙
      • 服务器安全卫士
      • CDN加速
      热门推荐
      • 云服务备份
      • 边缘安全加速平台
      • 全站加速
      • 安全加速
      • 云服务器
      • 云主机
      • 智能边缘云
      • 应用编排服务
      • 微服务引擎
      • 共享流量包
      更多推荐
      • web应用防火墙
      • 密钥管理
      • 等保咨询
      • 安全专区
      • 应用运维管理
      • 云日志服务
      • 文档数据库服务
      • 云搜索服务
      • 数据湖探索
      • 数据仓库服务
      友情链接
      • 中国电信集团
      • 189邮箱
      • 天翼企业云盘
      • 天翼云盘
      ©2025 天翼云科技有限公司版权所有 增值电信业务经营许可证A2.B1.B2-20090001
      公司地址:北京市东城区青龙胡同甲1号、3号2幢2层205-32室
      • 用户协议
      • 隐私政策
      • 个人信息保护
      • 法律声明
      备案 京公网安备11010802043424号 京ICP备 2021034386号