一、背景

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. Simplicial principal component analysis for density functions in Bayes spaces

    目录 问题 上的PCA Hron K, Menafoglio A, Templ M, et al. Simplicial principal component analysis for densit ...

  2. 揭开“QUIC”的神秘面纱

    作者:赵咏 QUIC的发音类似于Quick,实际上也确实很快.它可以很好地解决应用在传输层和应用层面临的各种需求,包括处理更多的连接.安全性以及低延迟. 目前在互联网领域,QUIC可以说刮起了新一代互 ...

  3. linux系统内置函数

    一.action函数 在/etc/init.d/functions中有一个系统自带的函数 source /etc/init.d/functions  #启用系统函数脚本 action "输出 ...

  4. nginx worker_cpu_affinity使用方法

    Nginx默认没有开启利用多核CPU,我们可以通过增加worker_cpu_affinity配置参数来充分利用多核CPU.CPU是任务处理,计算最关键的资源,CPU核越多,性能就越好. 配置Nginx ...

  5. Sentry 企业级数据安全解决方案 - Relay 监控 & 指标收集

    内容整理自官方文档 系列 Sentry 企业级数据安全解决方案 - Relay 入门 Sentry 企业级数据安全解决方案 - Relay 运行模式 Sentry 企业级数据安全解决方案 - Rela ...

  6. 初识python: 列表(list)

    使用列表函数写一个"购物车"小程序: #!/user/bin env python # author:Simple-Sir # 20180908 ''' 需求: 1.启动程序后,让 ...

  7. Python_getattr+__import__ 实现动态加载模块、类对象或函数

    __import__() 语法 __import__(name[, globals[, locals[, fromlist[, level]]]]) 参数 name -- 字符串,模块的导入路径 说明 ...

  8. ModelForm has no model class specified

    未指定模型类,错误发生在把model拼写错误 来自为知笔记(Wiz)

  9. js 使用 substr 和substring 的区别记录

    根据 提示,已经很清楚区别了 区别都是第二个参数 ,substr 是长度 ,而substring 仍是索引数

  10. 记一次异步处理导致Jetty Request对象泄漏

    最近排查一个bug,发现了一系列有意思的东西,对「自定义线程池」.「Jetty线程模型」都有了一些新的认识. 本文预计阅读时间10分钟,包括: 问题表现 常见原因筛查 根因与源码分析 最佳实践 一些小 ...