当前位置: 首页 > article >正文

处理Hutool的Http工具上传大文件报OOM

程序环境

  • JDK版本: 1.8
  • Hutool版本: 5.8.25

问题描述

客服端文件上传主要代码:

HttpRequest httpRequest = HttpUtil.createPost(FILE_UPLOAD_URL);
Resource urlResource = new UrlResource(url, fileName);
httpRequest.form("file", urlResource);
HttpResponse httpResponse = httpRequest.execute();

大文件上传 java.lang.OutOfMemoryError: Java heap space

java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3236) ~[na:1.8.0_275]
	at java.io.ByteArrayOutputStream.grow(ByteArrayOutputStream.java:118) ~[na:1.8.0_275]
	at java.io.ByteArrayOutputStream.ensureCapacity(ByteArrayOutputStream.java:93) ~[na:1.8.0_275]
	at java.io.ByteArrayOutputStream.write(ByteArrayOutputStream.java:135) ~[na:1.8.0_275]
	at sun.net.www.http.PosterOutputStream.write(PosterOutputStream.java:63) ~[na:1.8.0_275]
	at cn.hutool.http.MultipartOutputStream.write(MultipartOutputStream.java:108) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at java.io.OutputStream.write(OutputStream.java:116) ~[na:1.8.0_275]
	at cn.hutool.core.io.copy.StreamCopier.doCopy(StreamCopier.java:102) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.core.io.copy.StreamCopier.copy(StreamCopier.java:68) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.core.io.IoUtil.copy(IoUtil.java:162) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.core.io.IoUtil.copy(IoUtil.java:146) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.core.io.IoUtil.copy(IoUtil.java:132) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.core.io.IoUtil.copy(IoUtil.java:119) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.core.io.resource.Resource.writeTo(Resource.java:76) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.MultipartOutputStream.appendResource(MultipartOutputStream.java:163) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.MultipartOutputStream.write(MultipartOutputStream.java:96) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.body.MultipartBody$$Lambda$2190/568941495.accept(Unknown Source) ~[na:na]
	at cn.hutool.core.map.TableMap.forEach(TableMap.java:253) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.body.MultipartBody.write(MultipartBody.java:78) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.body.RequestBody.writeClose(RequestBody.java:27) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.HttpRequest.sendMultipart(HttpRequest.java:1402) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.HttpRequest.send(HttpRequest.java:1340) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.HttpRequest.doExecute(HttpRequest.java:1188) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.HttpRequest.execute(HttpRequest.java:1051) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at cn.hutool.http.HttpRequest.execute(HttpRequest.java:1027) ~[hutool-all-5.8.25.jar!/:5.8.25]
	at com.mbzj.ai.third.RhzClient.execute(RhzClient.java:270) ~[classes!/:1.0-SNAPSHOT]
	at com.mbzj.ai.third.RhzClient.uploadKnowledgeFile(RhzClient.java:184) ~[classes!/:1.0-SNAPSHOT]
	at com.mbzj.ai.third.RhzService.uploadKnowledgeFile(RhzService.java:132) ~[classes!/:1.0-SNAPSHOT]
	at com.mbzj.ai.listener.KnowledgeFileListener.handleAddKnowledgeFileEvent(KnowledgeFileListener.java:64) ~[classes!/:1.0-SNAPSHOT]
	at com.mbzj.ai.listener.KnowledgeFileListener$$FastClassBySpringCGLIB$$beafef7e.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.28.jar!/:5.3.28]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793) ~[spring-aop-5.3.28.jar!/:5.3.28]

分析问题

从异常堆栈信息中可以看出这里使用了 java.io.ByteArrayOutputStream 。实际上就是把文件全部都加载到了Byte数组中,如果上传的文件过大必定会导致OOM。

hutool httpRequest执行流程

在这里插入图片描述
这里实际上是使用的 java.net.HttpURLConnection

解决方案

java.net.HttpURLConnection 是支持 StreamingMode 传输HTTP请求的,有两种方式开启:

  • setFixedLengthStreamingMode
    当预先知道内容长度时,该方法用于使得能够在没有内部缓冲的情况下流式传输HTTP请求主体。
    如果应用程序尝试写入比指示的content-length更多的数据,或者如果应用程序在写入指示的数量之前关闭OutputStream,则将引发异常。
  • setChunkedStreamingMode
    当内容长度为不提前知道。在这种模式下,使用分块传输编码来发送请求正文。请注意,并非所有HTTP服务器都支持此模式。
    启用输出流时,无法自动处理身份验证和重定向。如果需要身份验证或重定向,则读取响应时将引发HttpRetryException。

Hutool 的 HttpRequest中只提供了 setChunkedStreamingMode方式,setFixedLengthStreamingMode 方式其实感觉上会更好,不会出现服务端不支持的情况,作者表示下一版本中将会支持setFixedLengthStreamingMode

先来测试一下 setChunkedStreamingMode 的效果。

这里自己写一个服务端的接口看看StreamingMode的header有什么区别。

@PostMapping("test")
public void test(MultipartFile file, HttpServletRequest request) {
    System.out.println("fileSize" + file.getSize());
    // 打印所有header
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
        String name = headerNames.nextElement();
        System.out.println(name + ":" + request.getHeader(name));
    }
}

这是修改前会出现OOM的客户端代码

HttpRequest httpRequest = HttpUtil.createPost("http://127.0.0.1:8064/test");
URL fileUrl = new URL("https://xxxx/1a67c727f8a845dd8b0b9825026349dd.mp4");
UrlResource urlResource = new UrlResource(fileUrl, "test.mp4");
httpRequest.form("file", urlResource);
System.out.println(httpRequest);
HttpResponse httpResponse = httpRequest.execute();
System.out.println(httpResponse);

堆内存明显增高
在这里插入图片描述

服务端日志输出:

accept:text/html,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
user-agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Hutool
accept-encoding:gzip, deflate
content-type:multipart/form-data; boundary=--------------------Hutool_rV0KKNQCkTkwywrQ
cache-control:no-cache
pragma:no-cache
host:127.0.0.1:8064
connection:keep-alive
content-length:128553150

客户端上传日志:

Request Url: http://127.0.0.1:8064/ai/knowledge/test
Request Headers: 
    Accept: text/html,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Hutool
    Accept-Encoding: gzip, deflate
Request Body: 
    file=https%3A%2F%2Fcos-uclass.lconrise.cn%2Fbiz%2Fai%2Fknowledge%2Ffile%2F1a67c727f8a845dd8b0b9825026349dd.mp4

Response Headers: 
    Keep-Alive=[timeout=60]
    X-Frame-Options=[DENY]
    null=[HTTP/1.1 200]
    Cache-Control=[no-cache, no-store, max-age=0, must-revalidate]
    X-Content-Type-Options=[nosniff]
    Connection=[keep-alive]
    Expires=[0]
    Pragma=[no-cache]
    Content-Length=[0]
    X-XSS-Protection=[1; mode=block]
    Date=[Wed, 11 Sep 2024 01:59:55 GMT]
Response Body: 

客户端通过 setChunkedStreamingMode 开启 StreamingMode:

HttpRequest httpRequest = HttpUtil.createPost("http://127.0.0.1:8064/ai/knowledge/test");
URL fileUrl = new URL("https://cos-uclass.lconrise.cn/biz/ai/knowledge/file/1a67c727f8a845dd8b0b9825026349dd.mp4");
UrlResource urlResource = new UrlResource(fileUrl, "test.mp4");
httpRequest.form("file", urlResource);
httpRequest.setChunkedStreamingMode(1024 * 8);
System.out.println(httpRequest);
HttpResponse httpResponse = httpRequest.execute();
System.out.println(httpResponse);

上传文件时堆内存无明细变化:
在这里插入图片描述

服务端日志输出:

accept:text/html,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
user-agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Hutool
accept-encoding:gzip, deflate
content-type:multipart/form-data; boundary=--------------------Hutool_Zn5eac5m74pQH1IJ
cache-control:no-cache
pragma:no-cache
host:127.0.0.1:8064
connection:keep-alive
transfer-encoding:chunked

客户端上传日志:

Request Url: http://127.0.0.1:8064/ai/knowledge/test
Request Headers: 
    Accept: text/html,application/json,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36 Hutool
    Accept-Encoding: gzip, deflate
Request Body: 
    file=https%3A%2F%2Fcos-uclass.lconrise.cn%2Fbiz%2Fai%2Fknowledge%2Ffile%2F1a67c727f8a845dd8b0b9825026349dd.mp4

Response Headers: 
    Keep-Alive=[timeout=60]
    X-Frame-Options=[DENY]
    null=[HTTP/1.1 200]
    Cache-Control=[no-cache, no-store, max-age=0, must-revalidate]
    X-Content-Type-Options=[nosniff]
    Connection=[keep-alive]
    Expires=[0]
    Pragma=[no-cache]
    Content-Length=[0]
    X-XSS-Protection=[1; mode=block]
    Date=[Wed, 11 Sep 2024 02:02:28 GMT]
Response Body: 
   

正常上传请求包含 content-lengt header, 来告诉服务端当前请求主体内容的字节数。
StreamingMode 中 没有 content-length ,而是新增了 transfer-encoding:chunked

扩展

  1. Transfer-Encoding: chunked

    • 这是一种 HTTP 传输编码,允许服务器在不知道整个响应内容长度的情况下,分批次发送数据。
    • 每个数据块前会有一个指定大小的头部,表明该块的大小,直到遇到大小为 0 的块,表示传输结束。
  2. 服务端处理

    • 服务端(如 Tomcat)在接收到 chunked 编码的请求时,会按照分块传输编码的规则来读取数据。
    • 服务端会持续读取数据块,直到检测到一个大小为 0 的块,这表示输入流已经结束。
  3. Tomcat 配置

    • Tomcat 允许通过配置 <Connector> 标签的 maxPostSize 属性来限制请求体的最大大小。
    • fileSizeThreshold 参数定义了上传文件写入磁盘的阈值,这对于处理大文件上传尤为重要。
  4. 流式上传

    • Tomcat 支持流式上传,这意味着数据可以边读边写,不需要将整个文件内容一次性加载到内存中。
    • 流式上传适用于大文件或实时数据传输,如视频流。
  5. 异步处理

    • Tomcat 支持 Servlet 3.0 规范中的异步处理机制,允许长时间运行的操作在单独的线程中执行。
    • 这可以提高 Tomcat 的并发处理能力和系统吞吐量。
  6. 异常处理

    • 在文件上传过程中,如果出现异常(如文件大小超出限制),Tomcat 会抛出相应的异常。
    • 开发者需要在代码中妥善处理这些异常,并在必要时进行异常捕获和处理。
  7. 请求结束

    • 处理完所有数据块后,Tomcat 会关闭输入流,并根据请求的内容执行相应的业务逻辑。

用了这么久HTTP, 你是否了解Content-Length和Transfer-Encoding

用了这么久HTTP, 你是否了解Content-Length和Transfer-Encoding

HTTP响应字段Transfer-Encoding含义及作用详解


http://www.kler.cn/news/365323.html

相关文章:

  • 设计师的新宠:7款不容错过的界面设计软件
  • python-PyQt项目实战案例:制作一个视频播放器
  • 基于Python+SQL Server2008实现(GUI)快递管理系统
  • 为Windows Terminal 配置zsh + Oh-My-Zsh!
  • java springboot项目如何计算经纬度在围栏内以及坐标点距离
  • Excel功能区变灰是什么原因造成?怎么解决?
  • 回顾复习1:
  • pycharm导出环境安装包列表
  • 在 Qt 中实现一个数据采集程序
  • 如何在算家云搭建GPT-SOVITS(语音转换)
  • redis5.0 cluster一个机器维修迁移到另外一个机器
  • 合合信息亮相PRCV大会,探讨生成式AI时代的内容安全与系统构建加速
  • 超越OpenAI GPT-4o,Yi-Lightning指南:中国AI大模型新巅峰
  • Ubuntu 20.04上安装Docker-CE社区版
  • 使用 rbenv 安装 Ruby 2.7.5
  • 华为HCIE-OpenEuler认证详解
  • 快速创建一个微信小程序,详细步骤以及示范程序代码
  • python项目实战——多线程爬虫
  • java如何部署web后端服务
  • Vue3 学习笔记(五)Vue3 模板语法详解
  • 【揭秘】图像算法工程师岗位如何进入?
  • Java:数据结构-二叉树
  • pta-java-6-1 jmu-Java-04面向对象进阶-01-接口-匿名内部类ActionListener
  • SpringBoot实现mysql多数据源配置(Springboot+Mybatis)
  • 模拟信号采集显示器+GPS同步信号发生器制作全过程(焊接、问题、代码、电路)
  • Java调用大模型 - Spring AI 初体验