Spring Boot中Blob转InputStream时连接已关闭问题求助
你遇到的org.postgresql.util.PSQLException: This connection has been closed本质是:JPA事务结束后,数据库连接会被连接池回收,而JPA返回的Blob是一个连接绑定的代理对象,它依赖原数据库连接才能获取二进制流。当你在事务外部调用blob.getBinaryStream()时,连接已关闭,自然触发报错。
由于你需要处理大文件,不能将文件加载到内存,必须基于流操作,以下是几个可行的解决方案:
方案1:确保流读取在活跃事务内完成
核心思路是把Document转DocumentWrapper的操作完全放在标注了@Transactional的方法内部,不让Blob对象脱离事务边界,保证获取流时连接仍处于活跃状态。
示例代码
服务层方法(事务内完成转换)
@Service public class DocumentService { @Autowired private DocumentRepository documentRepository; @Autowired private DocumentWrapperConverter converter; @Transactional(readOnly = true) public DocumentWrapper getDocumentById(Long id) throws IOException { Document document = documentRepository.findById(id).orElseThrow(); // 转换操作必须在事务内执行,此时连接未关闭 return converter.toWrapper(document); } }
自定义MultipartFile实现(基于InputStream)
Spring默认的StandardMultipartFile依赖内存或磁盘文件,我们需要自己实现一个基于流的版本:
public class InputStreamMultipartFile implements MultipartFile { private final InputStream inputStream; private final String name; private final String originalFilename; private final String contentType; public InputStreamMultipartFile(InputStream inputStream, String name, String originalFilename, String contentType) { this.inputStream = inputStream; this.name = name; this.originalFilename = originalFilename; this.contentType = contentType; } @Override public String getName() { return name; } @Override public String getOriginalFilename() { return originalFilename; } @Override public String getContentType() { return contentType; } @Override public boolean isEmpty() { return false; } @Override public long getSize() { throw new UnsupportedOperationException("不支持获取流大小"); } @Override public byte[] getBytes() throws IOException { throw new UnsupportedOperationException("禁止加载到内存,请使用getInputStream()"); } @Override public InputStream getInputStream() throws IOException { return inputStream; } @Override public void transferTo(File dest) throws IOException { Files.copy(inputStream, dest.toPath(), StandardCopyOption.REPLACE_EXISTING); } }
转换器中构建DocumentWrapper
public DocumentWrapper toWrapper(Document document) throws IOException { DocumentWrapper wrapper = new DocumentWrapper(); wrapper.setId(document.getId()); // 事务内直接获取Blob流 Blob blob = document.getBlobData(); InputStream is = blob.getBinaryStream(); MultipartFile multipartFile = new InputStreamMultipartFile( is, "file", document.getFileName(), document.getContentType() ); wrapper.setFile(multipartFile); return wrapper; }
注意:此方案要求
MultipartFile的getInputStream()必须在事务关闭前被消费(比如控制器直接将流写入响应输出流),如果需要异步或延后读取流,会导致流失效。
方案2:临时文件中转(适合持久化流场景)
如果需要在事务关闭后仍能使用流,可以在事务内将Blob流写入临时文件,再基于临时文件构建MultipartFile,脱离对数据库连接的依赖。
示例代码
@Service public class DocumentService { @Autowired private DocumentRepository documentRepository; @Transactional(readOnly = true) public DocumentWrapper getDocumentById(Long id) throws IOException { Document document = documentRepository.findById(id).orElseThrow(); Blob blob = document.getBlobData(); InputStream is = blob.getBinaryStream(); // 创建临时文件,JVM退出时自动删除 File tempFile = File.createTempFile("doc-", "." + document.getFileExtension()); tempFile.deleteOnExit(); // 将Blob流写入临时文件 Files.copy(is, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); // 基于临时文件构建MultipartFile MultipartFile multipartFile = new MockMultipartFile( "file", document.getFileName(), document.getContentType(), new FileInputStream(tempFile) ); DocumentWrapper wrapper = new DocumentWrapper(); wrapper.setId(document.getId()); wrapper.setFile(multipartFile); return wrapper; } }
注意:需要定期清理临时文件,避免磁盘占用过高;如果文件超大,需考虑临时文件存储路径的磁盘容量。
方案3:直接用JDBC获取流(绕过JPA代理)
如果JPA的Blob代理问题无法解决,可以直接通过JDBC连接读取流,完全绕过JPA的对象绑定。
示例代码
@Service public class DocumentService { @Autowired private DataSource dataSource; public DocumentWrapper getDocumentById(Long id) throws SQLException, IOException { try (Connection conn = dataSource.getConnection()) { String sql = "SELECT id, file_name, content_type, blob_data FROM document WHERE id = ?"; try (PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setLong(1, id); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { DocumentWrapper wrapper = new DocumentWrapper(); wrapper.setId(rs.getLong("id")); // 直接从ResultSet获取流,连接此时处于打开状态 InputStream is = rs.getBinaryStream("blob_data"); MultipartFile multipartFile = new InputStreamMultipartFile( is, "file", rs.getString("file_name"), rs.getString("content_type") ); wrapper.setFile(multipartFile); return wrapper; } } } } throw new RuntimeException("文档不存在"); } }
最优简化方案(直接输出响应流)
如果你的场景是控制器返回文件给前端,完全不需要转换为DocumentWrapper,直接在事务内将Blob流写入响应输出流即可,避免中间环节的流失效问题:
@GetMapping("/documents/{id}") @Transactional(readOnly = true) public void downloadDocument(@PathVariable Long id, HttpServletResponse response) throws IOException, SQLException { Document document = documentRepository.findById(id).orElseThrow(); // 设置响应头 response.setContentType(document.getContentType()); response.setHeader("Content-Disposition", "attachment; filename=\"" + document.getFileName() + "\""); // 事务内直接传输流 try (InputStream is = document.getBlobData().getBinaryStream(); OutputStream os = response.getOutputStream()) { StreamUtils.copy(is, os); // 用Spring的StreamUtils工具类 } }
内容的提问来源于stack exchange,提问作者Aleix Mariné

