You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过javax.servlet.http.Part+Hibernate低内存上传大文件至java.sql.Blob

实现大文件流式存入Blob字段(低内存占用)

当然可以实现!你之前的代码问题在于用ByteArrayOutputStream把整个文件读进了内存,这对大文件来说肯定会导致内存溢出。咱们换一种流式处理的方式,完全不用把整个文件加载到内存里就能完成写入,内存占用只和你设置的缓冲区大小有关。

核心思路

Hibernate本身支持通过JDBC的底层能力来操作Blob的流,我们可以直接把Part的输入流分块写入数据库的Blob输出流,全程不需要把文件内容加载到内存中。

步骤1:配置实体类

首先你的实体类要正确映射Blob字段,用@Lob注解标记:

@Entity
@Table(name = "files")
public class FileEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 映射数据库的Blob字段(比如MySQL的LONGBLOB)
    @Lob
    private Blob fileBlob;

    // 其他辅助字段
    private String fileName;
    private String contentType;

    // Getter和Setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public Blob getFileBlob() { return fileBlob; }
    public void setFileBlob(Blob fileBlob) { this.fileBlob = fileBlob; }
    public String getFileName() { return fileName; }
    public void setFileName(String fileName) { this.fileName = fileName; }
    public String getContentType() { return contentType; }
    public void setContentType(String contentType) { this.contentType = contentType; }
}

步骤2:流式写入Blob的核心代码

这里关键是通过Hibernate获取JDBC连接,创建空Blob后直接获取它的输出流,然后把Part的输入流分块复制进去:

@Transactional
public void saveLargeFile(Part part) throws SQLException, IOException {
    // 获取当前Hibernate Session
    Session session = SessionFactoryUtils.getCurrentSession(sessionFactory);

    // 获取底层JDBC连接
    Connection jdbcConnection = session.doReturningWork(Connection::get);

    // 创建一个空的Blob对象,准备写入数据
    Blob blob = jdbcConnection.createBlob();
    
    // 使用try-with-resources自动关闭流,避免资源泄漏
    try (OutputStream blobOutputStream = blob.setBinaryStream(1);
         InputStream partInputStream = part.getInputStream()) {

        // 用小块缓冲区(比如8KB)分块复制流,内存占用极低
        byte[] buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = partInputStream.read(buffer)) != -1) {
            blobOutputStream.write(buffer, 0, bytesRead);
        }
    }

    // 构建实体并保存
    FileEntity fileEntity = new FileEntity();
    fileEntity.setFileName(part.getSubmittedFileName());
    fileEntity.setContentType(part.getContentType());
    fileEntity.setFileBlob(blob);
    
    session.persist(fileEntity);
}

为什么这样能低内存运行?

  • 我们没有把整个文件转换成byte[]存储在内存里,而是通过流对拷的方式,每次只加载一小块数据(缓冲区大小)到内存,写完就释放。
  • 缓冲区大小可以根据你的需求调整(比如4KB、8KB、16KB都可以),内存占用始终只和缓冲区大小有关,和文件大小无关。

额外:流式读取Blob(避免加载整个文件到内存)

如果之后需要读取这个文件,同样可以用流式方式,避免把整个Blob加载到内存:

@Transactional(readOnly = true)
public void writeBlobToResponse(Long fileId, HttpServletResponse response) throws SQLException, IOException {
    Session session = SessionFactoryUtils.getCurrentSession(sessionFactory);
    FileEntity fileEntity = session.get(FileEntity.class, fileId);

    if (fileEntity == null) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
        return;
    }

    // 设置响应头
    response.setContentType(fileEntity.getContentType());
    response.setHeader("Content-Disposition", "attachment; filename=\"" + fileEntity.getFileName() + "\"");

    // 流式读取Blob并写入响应输出流
    try (InputStream blobInputStream = fileEntity.getFileBlob().getBinaryStream();
         OutputStream responseOutputStream = response.getOutputStream()) {

        byte[] buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = blobInputStream.read(buffer)) != -1) {
            responseOutputStream.write(buffer, 0, bytesRead);
        }
    }
}

注意事项

  1. 确保数据库支持足够大的Blob类型:比如MySQL用LONGBLOB(最大4GB),PostgreSQL用BYTEA或者BLOB,根据你的数据库调整表字段类型。
  2. 不要使用Hibernate的Session.createBlob(byte[])方法,因为它会把整个byte数组加载到内存,大文件场景下会OOM。
  3. 务必用try-with-resources管理流,确保输入输出流自动关闭,避免资源泄漏。

内容的提问来源于stack exchange,提问作者Peter Penzov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:19:59