背景:一次性将几十兆几百兆的文件读到内存里,然后再传给用户,服务器就爆了。
解决原则:读一点传一点。
解决方法:利用流,循环读写。
使用HttpURLConnection和bufferedInputStream 缓存流的方式来获取下载文件,读取InputStream输入流时,每次读取的大小为5M,不一次性读取完,就可避免内存溢出的情况。
/**
* BufferedInputStream 缓存流下载文件
* @param downloadUrl
* @param path
*/
public static void downloadFile(String downloadUrl, String path){
InputStream inputStream = null;
OutputStream outputStream = null;
try {
URL url = new URL(downloadUrl);
//这里没有使用 封装后的ResponseEntity 就是也是因为这里不适合一次性的拿到结果,放不下content,会造成内存溢出
HttpURLConnection connection =(HttpURLConnection) url.openConnection();
//使用bufferedInputStream 缓存流的方式来获取下载文件,不然大文件会出现内存溢出的情况
inputStream = new BufferedInputStream(connection.getInputStream());
File file = new File(path);
if (file.exists()) {
file.delete();
}
outputStream = new FileOutputStream(file);
//这里也很关键每次读取的大小为5M 不一次性读取完
byte[] buffer = new byte[1024 * 1024 * 5];// 5MB
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, len);
}
connection.disconnect();
}catch (Exception e){
e.printStackTrace();
}finally {
IOUtils.closeQuietly(outputStream);
IOUtils.closeQuietly(inputStream);
}
}
使用RestTemplate流式处理下载大文件,需要先用RequestCallback定义请求头的接收类型application/octet-stream,然后restTemplate进行请求下载时,对响应进行流式处理而不是将其全部加载到内存中。
/**
* 文件下载示例:使用restTemplate请求第三方文件服务下载文件
*
* @author Bruce.CH
* @since 2023年9月2日
*/
@RestController
public class FileController {
private static final Logger LOGGER = LoggerFactory.getLogger(FileController.class);
/**
* 第三方文件服务下载接口
*/
private static final String DOWNLOAD_URL = "http://127.0.0.1:8080/v1/file/server/download/{fileId}";
/**
* 注入restTemplate对象
*/
@Resource
private RestTemplate restTemplate;
/**
* 【方式3】
* 请求第三方文件服务下载接口下载文件id指定的文件:字节流直接绑定到响应的输出流中
*
* @param fileId 第三方文件id
* @param response 客户端响应
*/
@GetMapping("/v1/file/download3/{fileId}")
public void download3(@PathVariable("fileId") String fileId, HttpServletResponse response) {
LOGGER.info("download file:{}", fileId);
Map<String, String> uriVariables = new HashMap<>();
uriVariables.put("fileId", fileId);
ResponseExtractor<Boolean> responseExtractor = clientHttpResponse -> {
// 设置响应头,直接用第三方文件服务的响应头
HttpHeaders headers = clientHttpResponse.getHeaders();
headers.forEach((key, value) -> response.setHeader(key, value.get(0)));
// 收到响应输入流即时拷贝写出到响应输出流中: inputStream -> outputStream
StreamUtils.copy(clientHttpResponse.getBody(), response.getOutputStream());
return true;
};
Boolean execute = restTemplate.execute(DOWNLOAD_URL, HttpMethod.GET, null, responseExtractor, uriVariables);
LOGGER.info("download file success?{}", execute);
}
}
// GET请求
public static void downLargeFileByStream(String url, String savePath){
// 对响应进行流式处理而不是将其全部加载到内存中
restTemplate.execute(url, HttpMethod.GET, null, response -> {
Files.copy(response.getBody(), Paths.get(savePath));
return null;
}, httpEntity);
}
// POST请求
public static void downLargeFileByStream(String url, Map headers, MultiValueMap<String,String> body, String savePath){
headers.put("Content-Type","application/x-www-form-urlencoded");
HttpHeaders header = new HttpHeaders();
header.setAll(headers);
HttpEntity<Object> httpEntity = new HttpEntity(body,header);
//定义请求头的接收类型,无请求信息时可以设置为 null
RequestCallback requestCallback = restTemplate.httpEntityCallback(httpEntity, null);
// RequestCallback requestCallback = request -> request.getHeaders()
// .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
// 对响应进行流式处理而不是将其全部加载到内存中
restTemplate.execute(url,HttpMethod.POST,requestCallback, response -> {
Files.copy(response.getBody(), Paths.get(savePath));
return null;
}, httpEntity);
}
/**
* 下载文件
*
* @return
*/
@GetMapping("/test/downFile")
@ResponseBody
public HttpEntity<InputStreamResource> downFile() {
//将文件流封装为InputStreamResource对象
InputStream inputStream = this.getClass().getResourceAsStream("/1.txt");
InputStreamResource inputStreamResource = new InputStreamResource(inputStream);
//设置header
MultiValueMap<String, String> headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=1.txt");
HttpEntity<InputStreamResource> httpEntity = new HttpEntity<>(inputStreamResource);
return httpEntity;
}
/**
* 调用上面的接口,测试获取文件
*/
@Test
public void test7() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/chat16/test/downFile";
/**
* 文件比较大的时候,比如好几个G,就不能返回字节数组了,会把内存撑爆,导致OOM
* 需要这么玩:
* 需要使用execute方法了,这个方法中有个ResponseExtractor类型的参数,
* restTemplate拿到结果之后,会回调{@link ResponseExtractor#extractData}这个方法,
* 在这个方法中可以拿到响应流,然后进行处理,这个过程就是变读边处理,不会导致内存溢出
*/
String result = restTemplate.execute(url,
HttpMethod.GET,
null,
new ResponseExtractor<String>() {
@Override
public String extractData(ClientHttpResponse response) throws IOException {
System.out.println("状态:"+response.getStatusCode());
System.out.println("头:"+response.getHeaders());
//获取响应体流
InputStream body = response.getBody();
//处理响应体流
String content = IOUtils.toString(body, "UTF-8");
return content;
}
}, new HashMap<>());
System.out.println(result);
}
RestTemplate下载文件的3种实现方式_resttemplate 下载文件-CSDN博客
一文吃透接口调用神器RestTemplate-腾讯云开发者社区-腾讯云