java下载⽹络⼤⽂件之内存不够的解决办法(包含异步线程池分⽚上传分⽚下
载)
⼀、背景
2020年11⽉份的时候,我做过⼀个项⽬(我是中间接⼿的),涉及到⽹络⽂件,⽐如第三⽅接⼝提供⼀个⽂件的下载地址,使⽤java去下载,当时我全部加在到JVM内存⾥⾯,话说,单单是80M的下载单线程没问题,但是当时处于开发阶段,没注意到该问题,到了上线,同事负责测试,也没问题(主要的当时是4个⼈测试,也没发现内存泄漏问题,原因在于⽤户了少,占的内存也⼩),所以当时直接测试通过,并且上线。
客户那边进⾏验收测试,当时应该测试的⼈也不多,但是他们选择的⽂件100M以内的,⽽且是进⾏了⼀个,在等待是,⼜进⾏⼀个,也即是说类似于压测。顿时爆发问题。⼀查询⽇志显⽰,内存泄漏,俗称JVM:OutOfMemorry。
⼆、解决办法
⾄于针对这种情况,我提出有两种办法解决(下⾯会分别讲解这两种解决办法的源代码)
第⼀:分⽚下载⽂件、分⽚上传⽂件
第⼆:把⽂件下载到磁盘(在linux系统也是⼀样,指定下载到⽬录,再分⽚读取上传)
第三:另外我⾃⾏增加异步线程池来处理并发问题。也即每个⽂件都进⾏异步线程池处理(异步线程池这⾥不讲解,太简单了,⼤伙⾃⾏百度:springboot异步线程池配置(最好⾃⼰配置⼀下,默认的虽然不⽤配置,但是不太好,⽐如等待队列数量设置,队列满的策略怎么设置等))
三、分⽚下载⽂件、分⽚上传⽂件解决⽅案以及源码
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()
.String())
.size(fileTotalSize)
.build();
//把分⽚放进list⾥⾯
detailList.add(spliceDetail);
}
Integer totalSplice = IntExact(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()
.String())
.size(endSpliceSize - startSpliceSize + 1)
.build();
//把分⽚放进list⾥⾯
detailList.add(spliceDetail);
}
FileSpliceResultVo resultVo = FileSpliceResultVo.builder()
.
totalSplice(totalSplice)
.spliceDetail(detailList)
.build();
return resultVo;
}
FileSpliceResultVo.java类如下定义:
//分⽚结果集
@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 = hange(downloadUrl, HttpMethod.GET, httpEntity, byte[].class);
Body();
} catch (Exception e) {
log.info("请求pcDownloadFile下载阶段抛出异常fileName={},phone={},exception={}", fileName, phone, e);
}
return null;
}
注意:这⾥我请求第三⽅⽂件下载接⼝,增加了atch,是为了捕获异常,有些情况下会连接超时⽽导致不能记录⽇志,⽽且程序直接中断
3、接下来看分⽚上传代码
/**
* bytes参数:⽂件的⼆进制流,如果你是File⽂件,转为⼆进制流的话,可以通过jdk⾃带的:adFileToByteArray(File)转换
*pcUploadFileVo 这⾥是我根据⾃⾏的业务封装的实体类,⼤家不必跟我的⼀模⼀样
* range 这个参数也是分⽚,根据第三步的分⽚⽅法计算出来上传的分⽚⼤⼩。
* rangeType 我的这个参数是⽤来识别是否分⽚上传完成,有的接⼝是这样做,有的不是。可能对⼤家
没多⼤意义
* contentLength 本次上传的分⽚⼤⼩,有的分⽚上传接⼝也不需要,都是看业务。
* 特别注意:header请求头会根据你的不同业务,⽽设计不同,都是根据⾃⼰的需求⽽定义。我这⾥展⽰的也只是⼀部分,让⼤家好有个参考
/
public String spliceUploadFile(byte[] bytes, PcUploadFileVo pcUploadFileVo, String range, String rangeType, Long contentLength){
String fileName = FileName(), "UTF-8");
HttpHeaders headers = new HttpHeaders();
headers.set("Range", "bytes=" + range);
headers.set("contentSize", FileSize()); //整个⽂件⼤⼩
headers.set("rangeType", rangeType);
headers.set("Content-Length", String.valueOf(contentLength));  //本⽚⽂件的⼤⼩
//⽤HttpEntity封装整个请求报⽂
HttpEntity httpEntity = new HttpEntity<>(bytes, headers);
try {
log.info("⽂件分⽚上传:fileName={},headers={}", FileName(), JsonStr(headers));
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, httpEntity, String.class);
log.info("⽂件分⽚上传:{},结果:{}", FileName(), Body()));
Body();
} catch (Exception e) {
<("⽂件分⽚上传出错抛出异常:fileName={}", FileName(), e);
}
return null;
}
⾄此:针对⽹络⽂件,分⽚上传,分⽚下载的代码⼤概演⽰完成。接下来带⼤家进⼊⽅案⼆:把⽹络⽂件下载到磁盘(速度极快且占内存⼩)
四、下载⽹络⽂件到磁盘
直接上源码:
/**
* ⽂件下载
*
* @param downloadUrl 下载地址
* @param targetPath  ⽂件保存⽬标路径,这⾥的组成是:路径+⽂件名,如:/opt/upload/我的报告.docx
* @return 下载结果
public boolean downloadFile (String downloadUrl, String targetPath)
{
// 请求头设置为APPLICATION_OCTET_STREAM,表⽰以流的形式进⾏数据加载
RequestCallback requestCallback = request -> Headers ()
.setAccept (Arrays.asList (MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
// RequestCallback 结合py保证了接收到⼀部分⽂件内容,就向磁盘写⼊⼀部分内容。⽽不是全部加载到内存,最后再写⼊磁盘⽂件。
// 对响应进⾏流式处理⽽不是将其全部加载到内存中
try
{
return true;
});
}
catch (Exception e)
{
< ("downloadFile exception! downloadUrl={}  targetPath={}", downloadUrl, targetPath, e);
return false;
}
return true;
}
对,没错,不⽤怀疑,就是这么简单。但是保存到磁盘,如果还需要对该⽂件上传,优化上传的话还需要分⽚处理上传,稍后会再整理怎么读取本地⽂件进⾏分⽚上传以及对分⽚的⽂件进⾏合并完整的⽂件五、对分⽚的⽂件进⾏合并
/**
* 合并⽂件(针对⽂件的分割后进⾏合并)
*
* @param srcFile
* srcFile 分⽚⽂件
* fileSubfixx ⽂件后缀
* targetFileName 保存为⽬标⽂件的⽂件名
*/
private static void mergeFile(File srcFile,int totalSplice,String fileSubfixx,String targetFileName) throws IOException {
ArrayList<FileInputStream> al = new ArrayList<FileInputStream>();
//这⾥的for循环就是有多少个分⽚的⽂件,这⾥的变量⾃⾏控制哈,⽽且x变量需要根据⾃⼰分⽚保存的下标来决定开始变量
for (int x = 0; x <= totalSplice; x++) {
// 将要合并的碎⽚封装成对象
al.add(new FileInputStream(new File(srcFile, x + fileSubfixx)));
}
Enumeration<FileInputStream> en = umeration(al);
SequenceInputStream sis = new SequenceInputStream(en);
// 将合成的⽂件封装成⼀个⽂件对象
FileOutputStream fos = new FileOutputStream(new File(srcFile, targetFileName));
try {
int len = 0;
byte buf[] = new byte[1024 * 1024];
while ((len = ad(buf)) != -1) {
fos.write(buf, 0, len);
}
} catch (Exception e) {
} finally {
fos.close();
sis.close();
}
log4j2 异步写文件}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。