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

Spring Boot使用JdbcTemplate读取100列时出现ResultSet已关闭异常

问题描述

在Spring Boot项目中使用JdbcTemplate执行查询,将100列数据映射到SimpleProjection接口的匿名实现类时,出现部分列读取失败,报错信息为This ResultSet is closed。测试发现仅读取前50列时正常,读取第41列及之后的部分列会触发异常。


相关代码与配置

仓库代码(MyJDBCRepository)

package com.org.repository;

import com.org.config.datasourceconfig.DataSourceConfig;
import com.org.domain.transformer.SimpleMapper;
import com.org.infra.projection.SimpleProjection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.*;

@Slf4j
@Repository
public class MyJDBCRepository extends AbstractJdbcRepository {

    public MyJDBCRepository(DataSourceConfig dataSourceConfig) {
        super(dataSourceConfig);
    }

    public SimpleProjection findBy(
            Integer search1,
            Integer search2){
        String query = SQL_QUERY;

        boolean hasCondition = query.toLowerCase(Locale.ROOT).contains("where");
        StringBuilder queryBuilder = new StringBuilder(query);

        hasCondition = appendEqualToClause(queryBuilder, hasCondition,  "a.column1", search1);
        hasCondition = appendEqualToClause(queryBuilder, hasCondition,  "a.column2 ", search2);
        
        queryBuilder.append(" limit 1");//TODO: This is temp change since we dont have `TimeStamp` type of column in reporting tables.

        log.info("createPreparedStatement [SQL Query]: {} ", queryBuilder);

        List<Object> params = new ArrayList<>();
        if (search1!= null) {
            params.add(search1);
        }
        if (search1 != null) {
            params.add(search2);
        }
        
        try {
            return getReadJdbcTemplate().queryForObject(queryBuilder.toString(), params.toArray(), new SimpleMapper());
        } catch (Exception e) {
            log.error("Exception in searchApplications Query: {}", e.getMessage(), e);
        }
        return null;
    }
}

public static boolean appendEqualToClause(StringBuilder queryBuilder, boolean hasCondition, String columnName, Object value) {
    if (value != null) {
        queryBuilder.append(hasCondition ? " AND " : " WHERE ")
                .append(columnName)
                .append(" = ?");
        return true; // Return true to update hasCondition
    }
    return hasCondition; // If no condition is added, return the same value
}

RowMapper实现(SimpleMapper)

package com.org.domain.transformer;

import com.org.infra.projection.SimpleProjection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

@Slf4j
public class SimpleMapper implements RowMapper<SimpleProjection> {

    @Override
    public SimpleProjection mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new SimpleProjection() {
            @Override
            public Long getColumn1() {
                try {
                    return rs.getLong("column1");
                } catch (SQLException e) {
                    log.error("1. Reading column name failed : {}", e.getMessage());
                    return null;
                }
            }

            @Override
            public String getColumn2() {
                try {
                    return rs.getString("column2");
                } catch (SQLException e) {
                    log.error("2. Reading column name failed : {}", e.getMessage());
                    return null;
                }
            }

            // ... 省略中间getColumn3到getColumn98的实现

            @Override
            public Integer getColumn99() {
                try {
                    return rs.getInt("column99");
                } catch (SQLException e) {
                    log.error("99. Reading column name failed : {}", e.getMessage());
                    return null;
                }
            }

            @Override
            public Integer getColumn100() {
                try {
                    return rs.getInt("column100");
                } catch (SQLException e) {
                    log.error("100. Reading column name failed : {}", e.getMessage());
                    return null;
                }
            }
        };
    }
}

Projection接口(SimpleProjection)

package com.org.infra.projection;

public interface SimpleProjection {

    Integer getColumn1();
    Integer getColumn2();
    
    // ... 省略中间getColumn3到getColumn98的方法
    
    Integer getColumn99();
    Integer getColumn100();
}

数据源配置

public DataSource readDataSource() {
    HikariConfig config = new HikariConfig();
    config.setDriverClassName(dataBaseDriver);
    config.setJdbcUrl(readDataBaseUrl);
    config.setUsername(readDataBaseUser);
    config.setPassword(readDataBasePassword);
    config.setMaximumPoolSize(readMaximumPoolSize);
    config.setIdleTimeout(readIdleTimeout);
    config.setMinimumIdle(readMinimumIdle);
    config.setIdleTimeout(readIdleTimeout);
    config.setConnectionTimeout(readConnectionTimeout);
    config.setMaxLifetime(readMaxLifeTime);
    return new HikariDataSource(config);
}

数据源配置属性

spring.datasource.read.hikari.maximum-pool-size=10
spring.datasource.read.hikari.minimum-idle=5
spring.datasource.read.hikari.idle-timeout=10000
spring.datasource.read.hikari.connection-timeout=30000
spring.datasource.read.hikari.max-lifetime=60000

报错信息

74. Reading column name failed : This ResultSet is closed.
46. Reading column name failed : This ResultSet is closed.

原因分析
  1. ResultSet生命周期不匹配:你在mapRow方法中返回的匿名内部类实现了SimpleProjection,但这个类的getXXX方法是延迟执行的——只有当外部调用这些方法时才会去读取ResultSet。而JdbcTemplate在mapRow执行完成后,会立即关闭ResultSet、Statement并归还数据库连接到连接池,此时再读取ResultSet就会触发关闭异常。
  2. 列读取顺序的偶然性:前50列能正常读取,是因为这些列的getXXX方法在ResultSet关闭前就被调用(比如业务逻辑提前访问、序列化或日志打印),而第41列之后的部分列是在ResultSet关闭后才被访问,因此触发异常。

解决方案

方案1:改用实体类一次性读取所有列(最可靠)

创建普通POJO实体类,在mapRow方法中一次性读取所有列的值并赋值给实体字段,确保所有ResultSet操作都在mapRow执行期间完成(此时ResultSet处于打开状态)。

示例代码:

// 实体类
public class SimpleEntity {
    private Long column1;
    private String column2;
    // ... 其他98个字段
    private Integer column99;
    private Integer column100;

    // 全参/无参构造方法、getter、setter
}

// 修改后的RowMapper
@Slf4j
public class SimpleEntityMapper implements RowMapper<SimpleEntity> {
    @Override
    public SimpleEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
        SimpleEntity entity = new SimpleEntity();
        try {
            entity.setColumn1(rs.getLong("column1"));
            entity.setColumn2(rs.getString("column2"));
            // ... 依次读取所有列并赋值
            entity.setColumn99(rs.getInt("column99"));
            entity.setColumn100(rs.getInt("column100"));
        } catch (SQLException e) {
            log.error("读取列失败: {}", e.getMessage(), e);
            throw e; // 按业务需求处理异常
        }
        return entity;
    }
}

方案2:使用Spring官方的Projection动态代理(推荐)

如果坚持使用Projection接口,不要自己实现匿名类,改用Spring Data提供的动态代理支持。Spring会在mapRow期间一次性读取所有需要的列并缓存,避免延迟读取问题。

修改方式:

  1. 确保SimpleProjection接口的Getter方法与查询列名匹配(当前接口已符合规范)。
  2. 替换自定义SimpleMapper为BeanPropertyRowMapper:
// 在Repository中替换RowMapper
return getReadJdbcTemplate().queryForObject(queryBuilder.toString(), params.toArray(), 
    BeanPropertyRowMapper.newInstance(SimpleProjection.class));

提示:如果列名与方法名不匹配,可在SQL中给列起别名,例如SELECT column_name AS columnName FROM ...。

方案3:临时调整连接池配置(仅缓解,不推荐)

当前HikariCP的max-lifetime=60000(1分钟)可能导致连接提前回收,但这不是根本原因。可临时将max-lifetime调大(如30分钟),但无法解决延迟读取ResultSet的核心问题。


总结

核心问题是延迟读取ResultSet导致的生命周期不匹配,最可靠的解决方案是改用实体类一次性读取所有列,或使用Spring官方的Projection动态代理实现,避免自行实现延迟加载的匿名类。

内容的提问来源于stack exchange,提问作者Dnyaneshwar Jadhav

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 22:40:10