如何通过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); } } }
注意事项
- 确保数据库支持足够大的Blob类型:比如MySQL用
LONGBLOB(最大4GB),PostgreSQL用BYTEA或者BLOB,根据你的数据库调整表字段类型。 - 不要使用Hibernate的
Session.createBlob(byte[])方法,因为它会把整个byte数组加载到内存,大文件场景下会OOM。 - 务必用
try-with-resources管理流,确保输入输出流自动关闭,避免资源泄漏。
内容的提问来源于stack exchange,提问作者Peter Penzov
相关产品推荐
相关产品推荐

