一、背景

2020年11月份的时候,我做过一个项目,涉及到网络文件,比如第三方接口提供一个文件的下载地址,使用java去下载,当时我全部加在到JVM内存里面,话说,单单是80M的下载单线程没问题,但是当时处于开发阶段,没注意到该问题,到了上线,同事负责测试,也没问题(主要的当时是4个人测试,也没发现内存泄漏问题,原因在于用户了少,占的内存也小),所以当时直接测试通过,并且上线。

客户那边进行验收测试,当时应该测试的人也不多,但是他们选择的文件100M以内的,而且是进行了一个,在等待是,又进行一个,也即是说类似于压测。顿时爆发问题。一查询日志显示,内存泄漏,俗称JVM:OutOfMemorry。

二、解决办法

至于针对这种情况,项目经理说有两种办法解决(下面会分别讲解这两种解决办法的源代码)

第一:分片下载文件、分片上传文件

第二:把文件下载到磁盘(在linux系统也是一样,指定下载到目录,再分片读取上传)

三、分片下载文件、分片上传文件解决方案以及源码

1、首先分片下载地址,计算每一片的分片大小,源码如下

/**
* @param fileTotalSize 文件总大小 kb
* @param splice 分片单位大小 kb
* 分片的结果:range=: 0-2
* 3-5
* 6-8
*/
public static FileSpliceResultVo getFileSplice(Long fileTotalSize, Long splice) {
//包装分片数据
Long startSpliceSize = 0L;
Long endSpliceSize = 0L;
List<SpliceDetail> detailList = new ArrayList<>(); //1:计算出总的分片数量
if (fileTotalSize <= 0 || splice <= 0) {
return null;
} if (splice >= fileTotalSize) {
//如果分片大小,大于实际的文件大小:
StringBuilder range = new StringBuilder()
.append(0).append("-").append(fileTotalSize-1);
//分片详情信息
SpliceDetail spliceDetail = SpliceDetail.builder()
.range(range.toString())
.size(fileTotalSize)
.build();
//把分片放进list里面
detailList.add(spliceDetail);
} Integer totalSplice = Math.toIntExact(fileTotalSize / splice);
//如果取模不为0,则分片数量+1;
if (fileTotalSize % splice != 0) {
totalSplice = totalSplice + 1;
} for (int spliceIndex = 0; spliceIndex < totalSplice; spliceIndex++) {
startSpliceSize = spliceIndex * splice;//分片是从0开始
endSpliceSize = spliceIndex * splice + splice - 1;//末端分片-1
if (endSpliceSize > fileTotalSize) {
endSpliceSize = fileTotalSize-1; //如果最后一片大于实际文件大小,那么取文件大小
}
StringBuilder range = new StringBuilder()
.append(startSpliceSize).append("-").append(endSpliceSize); //分片详情信息
SpliceDetail spliceDetail = SpliceDetail.builder()
.range(range.toString())
.size(endSpliceSize - startSpliceSize + 1)
.build();
//把分片放进list里面
detailList.add(spliceDetail);
}
FileSpliceResultVo resultVo = FileSpliceResultVo.builder()
.totalSplice(totalSplice)
.spliceDetail(detailList)
.build();
return resultVo;
}

FileSpliceResultVo.java类如下定义:
//分片结果集
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class FileSpliceResultVo {
//总共分片
private Integer totalSplice; private List<SpliceDetail> spliceDetail; }

SpliceDetail.java如下:
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SpliceDetail implements Serializable { private Long size; private String range;
}
2、分片计算好,那么就来分片下载(此处分片下载需要接口支持,否则不行)
//这里的自动注入是我在项目里面自己配置的,如果大家没有做配置,自行new 一个对象。也就是在spliceDownloadFile方法体里面:RestTemplate restTemplate=new RestTemplate()
@Autowired
private RestTemplate restTemplate;

//分片下载方法,主要是通过参数range来指定下载的分片,range参数在上面计算分片已经的出来,直接传进来该方法即可。至于fileName、phone参数传进来是为了日志关键字排查
public byte[] spliceDownloadFile(String fileName, String phone, String downloadUrl, String range) {
//下载url转义处理
HttpHeaders headers = new HttpHeaders();
headers.set("Range", "bytes=" + range);//此处的Range的Header字段是由接口提供方定义,大家自行更改,并且如果涉及鉴权,自己在header里面添加,还有的接口会涉及其他header字段需要标识。这里不多说
HttpEntity httpEntity = new HttpEntity<>(headers);
try {
log.info("请求分片下载fileName={},phone={},url={}", fileName, phone, downloadUrl);
ResponseEntity<byte[]> exchange = restTemplate.exchange(downloadUrl, HttpMethod.GET, httpEntity, byte[].class);
return exchange.getBody();
} catch (Exception e) {
log.info("请求pcDownloadFile下载阶段抛出异常fileName={},phone={},exception={}", fileName, phone, e);
}
return null;
}

注意:这里我请求第三方文件下载接口,增加了try...catch,是为了捕获异常,有些情况下会连接超时而导致不能记录日志,而且程序直接中断
3、接下来看分片上传代码
  
/**
* bytes参数:文件的二进制流,如果你是File文件,转为二进制流的话,可以通过jdk自带的:FileUtils.readFileToByteArray(File)转换
*pcUploadFileVo 这里是我根据自行的业务封装的实体类,大家不必跟我的一模一样
* range 这个参数也是分片,根据第三步的分片方法计算出来上传的分片大小。
* rangeType 我的这个参数是用来识别是否分片上传完成,有的接口是这样做,有的不是。可能对大家没多大意义
* contentLength 本次上传的分片大小,有的分片上传接口也不需要,都是看业务。
* 特别注意:header请求头会根据你的不同业务,而设计不同,都是根据自己的需求而定义。我这里展示的也只是一部分,让大家好有个参考
/
public String spliceUploadFile(byte[] bytes, PcUploadFileVo pcUploadFileVo, String range, String rangeType, Long contentLength){ String fileName = URLEncoder.encode(pcUploadFileVo.getFileName(), "UTF-8");
HttpHeaders headers = new HttpHeaders();
headers.set("Range", "bytes=" + range);
headers.set("contentSize", pcUploadFileVo.getFileSize()); //整个文件大小
headers.set("rangeType", rangeType);
headers.set("Content-Length", String.valueOf(contentLength)); //本片文件的大小
//用HttpEntity封装整个请求报文
HttpEntity httpEntity = new HttpEntity<>(bytes, headers);
try {
log.info("文件分片上传:fileName={},headers={}", pcUploadFileVo.getFileName(), JSONUtil.toJsonStr(headers));
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, httpEntity, String.class);
log.info("文件分片上传:{},结果:{}", pcUploadFileVo.getFileName(), JSONUtil.toJsonStr(responseEntity.getBody()));
return responseEntity.getBody();
} catch (Exception e) {
log.error("文件分片上传出错抛出异常:fileName={}", pcUploadFileVo.getFileName(), e);
}
return null;
}
至此:针对网络文件,分片上传,分片下载的代码大概演示完成。接下来带大家进入方案二:把网络文件下载到磁盘(速度极快且占内存小)

四、下载网络文件到磁盘

直接上源码:
/**
* 文件下载
*
* @param downloadUrl 下载地址
* @param targetPath 文件保存目标路径,这里的组成是:路径+文件名,如:/opt/upload/我的报告.docx
* @return 下载结果
*/
public boolean downloadFile (String downloadUrl, String targetPath)
{
// 请求头设置为APPLICATION_OCTET_STREAM,表示以流的形式进行数据加载
RequestCallback requestCallback = request -> request.getHeaders ()
.setAccept (Arrays.asList (MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
// RequestCallback 结合File.copy保证了接收到一部分文件内容,就向磁盘写入一部分内容。而不是全部加载到内存,最后再写入磁盘文件。 // 对响应进行流式处理而不是将其全部加载到内存中
try
{
restTemplate.execute (downloadUrl, HttpMethod.GET, requestCallback, clientHttpResponse -> {
Files.copy (clientHttpResponse.getBody (), Paths.get (targetPath));
return true;
});
}
catch (Exception e)
{
log.error ("downloadFile exception! downloadUrl={} targetPath={}", downloadUrl, targetPath, e);
return false;
}
return true;
}
对,没错,不用怀疑,就是这么简单。但是保存到磁盘,如果还需要对该文件上传,优化上传的话还需要分片处理上传,稍后会再整理怎么读取本地文件进行分片上传以及对分片的文件进行合并完整的文件

java下载网络大文件之内存不够的解决办法(包含分片上传分片下载)的更多相关文章

  1. Java中使用HttpPost上传文件以及HttpGet进行API请求(包含HttpPost上传文件)

    Java中使用HttpPost上传文件以及HttpGet进行API请求(包含HttpPost上传文件) 一.HttpPost上传文件 public static String getSuffix(fi ...

  2. pikachu 文件包含,上传,下载

    一.文件包含 1.File Inclusion(local) 我们先测试一下,选择kobe然后提交 发现url出现变化 可以猜测此功能为文件包含,包含的文件为 file1.php,所以我在此盘符的根目 ...

  3. 查找大文件 & 索引节点(inode)爆满 解决办法

    经常遇到磁盘满或者文件节点满的情况,整理如下 查找大文件 查找超过某个大小的文件, 如1G find . -type f -size +1G 查找文件大小的时候,现实文件属性 find . -type ...

  4. 逐行读取txt文件,分割,写入txt。。。上传,下载

    s = [] f  = open('querylist.txt','r') #由于我使用的pycharm已经设置完了路径,因此我直接写了文件名 for lines in f:     ls = lin ...

  5. Node.js中使用pipe拷贝大文件不能完全拷贝的解决办法

    原来的代码如下: var readable = fs.createReadStream( filepath ); var writable = fs.createWriteStream( outFil ...

  6. NET上传大文件出现网页无法显示的问题 默认的上传文件大小是4M

    需要在配置文件处进行修改 web.config中的<system.web></system.web>内加入如下代码: <httpRuntime executionTime ...

  7. python全栈开发day28-网络编程之粘包、解决粘包,上传和下载的作业

    一.昨日内容回顾 1. tcp和udp编码 2. 自定义mysocket解决编码问题 二.今日内容总结 1.粘包 1)产生粘包原因: (1).接收方不知道消息之间的边界,不知道一次性要取多少字节的数据 ...

  8. 30分钟玩转Net MVC 基于WebUploader的大文件分片上传、断网续传、秒传(文末附带demo下载)

    现在的项目开发基本上都用到了上传文件功能,或图片,或文档,或视频.我们常用的常规上传已经能够满足当前要求了, 然而有时会出现如下问题: 文件过大(比如1G以上),超出服务端的请求大小限制: 请求时间过 ...

  9. 大文件传输 分片上传 上传id 分片号 授权给第三方上传

    https://www.zhihu.com/question/39593108 作者:ZeroOne链接:https://www.zhihu.com/question/39593108/answer/ ...

随机推荐

  1. Codeforces1132A——Regular Bracket Sequence(水题)

    Regular Bracket Sequence time limit per test:1 second memory limit per test:256 megabytes input:stan ...

  2. Mind the Box: $\ell_1$-APGD for Sparse Adversarial Attacks on Image Classifiers

    目录 概 主要内容 Croce F. and Hein M. Mind the box: \(\ell_1\)-APGD for sparse adversarial attacks on image ...

  3. .NET6: 三分钟搭建WPF三维应用

    要运行本文中的示例,请先安装Vistual Studio 2022,社区版就可以了. 1 创建项目 选择创建WPF应用 给程序起一个酷酷的名字,选一个酷酷的位置: 选一下.NET6 2 配置项目 从n ...

  4. 使用 jQuery 操作页面元素的方法,实现浏览大图片的效果,在页面上插入一幅小图片,当鼠标悬停到小图片上时,在小图片的右侧出现与之相对应的大图片

    查看本章节 查看作业目录 需求说明: 使用 jQuery 操作页面元素的方法,实现浏览大图片的效果,在页面上插入一幅小图片,当鼠标悬停到小图片上时,在小图片的右侧出现与之相对应的大图片 实现思路: 在 ...

  5. InnoDB学习(八)之 聚簇索引

    InnoDB中,表数据文件本身就是以主键为索引的B+树,树的叶子节点存放一条条表数据,此索引树被称为表的聚簇索引.聚簇索引也称为聚集索引,聚类索引,簇集索引,聚簇索引确定表中数据的物理顺序. Inno ...

  6. JUC之多线程锁问题

    多线程锁 8种问题锁状态: 该部分全部围绕的是以下内容并结合相应的例子:synchronized实现同步的基础:Java中每个对象都可以作为锁. 具体表现为以下三种形式:(之前只是简单的了解) 对于普 ...

  7. 最简短的 AC 自动机!

    写在前面 仍然是写给自己的,看不懂别怪我. 最简短的 AC 自动机! AC 自动机用于多模匹配. 模式串被插入一个添加了一些转移边的 Trie 中.在匹配的时候,若失配,则使下一个字符跳到该节点的 f ...

  8. Kubernetes-Kuboard

    前言 本篇是Kubernetes第十五篇,大家一定要把环境搭建起来,看是解决不了问题的,必须实战. Kubernetes系列文章: Kubernetes介绍 Kubernetes环境搭建 Kubern ...

  9. 开源实践 | 携程在OceanBase的探索与实践

    写在前面:选型考虑 携程于1999年创立,2016-2018年全面推进应用 MySQL 数据库,前期线上业务.前端技术等以 SQL Server 为主,后期数据库逐步从 SQL Server 转到开源 ...

  10. Python3.7 发送邮件 报‘[WinError 10061] 由于目标计算机积极拒绝,无法连接’错误的解决方法

    背景: 最近在练习Python 的邮件发送功能 照着教程写了一个简单的demo 结果运行时报如下错误:[WinError 10061] 由于目标计算机积极拒绝,无法连接. 如图: 解决路径如下: St ...