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

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é

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 01:55:17