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.
原因分析
- ResultSet生命周期不匹配:你在
mapRow方法中返回的匿名内部类实现了SimpleProjection,但这个类的getXXX方法是延迟执行的——只有当外部调用这些方法时才会去读取ResultSet。而JdbcTemplate在mapRow执行完成后,会立即关闭ResultSet、Statement并归还数据库连接到连接池,此时再读取ResultSet就会触发关闭异常。 - 列读取顺序的偶然性:前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期间一次性读取所有需要的列并缓存,避免延迟读取问题。
修改方式:
- 确保
SimpleProjection接口的Getter方法与查询列名匹配(当前接口已符合规范)。 - 替换自定义
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
相关产品推荐
相关产品推荐

