在 Spring Boot 项目中,JdbcTemplate 是介于原始 JDBC 和完整 ORM 之间的轻量级数据库访问方案。它不隐藏 SQL,也不会强迫开发者学习复杂的映射规则,但需要自己处理结果集到对象的转换。想要用好它,第一步并不是直接写 DAO,而是搞清楚 Spring Boot 如何自动装配 DataSource 和 JdbcTemplate,以及手动配置时哪些参数真正影响长期运行的稳定性。

一、Spring Boot 自动配置 JdbcTemplate 的原理
Spring Boot 对 JdbcTemplate 的支持来自 spring-boot-starter-jdbc。这个 starter 会传递引入 spring-jdbc 以及默认的连接池 HikariCP。当应用启动时,如果 classpath 中存在 DataSource 的实现,并且只有一个候选 DataSource,自动配置类就会创建一个 JdbcTemplate Bean 并交给容器管理。也就是说,大多数单数据源项目根本不需要手动 new JdbcTemplate。
引入依赖的方式非常简单,在 Maven 的 pom.xml 中加入以下内容即可。这里选择 XML 形式展示,实际项目中也可以使用 Gradle 配置。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
除了 starter 之外,还需要根据目标数据库引入对应的 JDBC 驱动。以 MySQL 为例,需要额外添加 mysql-connector-j。随后在 application.yml 中配置数据源连接信息。Spring Boot 会自动识别这些前缀,并创建 HikariDataSource。
spring:
datasource:
url: jdbc:mysql://localhost:3306/app_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
这里需要特别注意 URL 中的 & 在 YAML 文件里必须写为 &,否则解析会出错。如果项目中存在多个 DataSource,或者你想使用自定义的 JdbcTemplate 设置,可以手动创建配置类。例如指定查询超时、最大行数或自定义异常翻译器,都可以在手动装配时完成。
@Configuration
public class JdbcConfig {
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setQueryTimeout(10);
jdbcTemplate.setMaxRows(5000);
return jdbcTemplate;
}
}
手动定义 Bean 后,Spring Boot 的自动配置会退让,因此不会产生冲突。但要注意,如果自定义 DataSource 不是唯一的候选 Bean,自动配置的条件会失效,此时必须像上面这样显式声明 JdbcTemplate。
二、从零实现增删改查与结果映射
假设有一张用户表,表结构如下。实际项目中通常通过 Flyway 或 Liquibase 管理建表脚本,这里直接展示 SQL 方便理解字段设计。
CREATE TABLE app_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
对应的 Java 实体可以设计为纯粹的 POJO。字段名建议和数据库列名保持对应关系,如果采用下划线转驼峰,需要自行在 RowMapper 中处理。下面是一个最简单的实体类示例。
public class AppUser {
private Long id;
private String username;
private String email;
private LocalDateTime createdAt;
// 省略 getter/setter
}
接着编写 Repository 类。JdbcTemplate 提供了 update、queryForObject、query 等常用方法。对于单个对象查询,需要传入 RowMapper 接口的实现,可以使用 lambda 表达式简化代码。这里用 created_at 映射到 LocalDateTime,需要调用 getObject 方法。
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public int insert(AppUser user) {
String sql = "INSERT INTO app_user(username, email) VALUES (?, ?)";
return jdbcTemplate.update(sql, user.getUsername(), user.getEmail());
}
public AppUser findById(Long id) {
String sql = "SELECT id, username, email, created_at FROM app_user WHERE id = ?";
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
AppUser user = new AppUser();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setCreatedAt(rs.getObject("created_at", LocalDateTime.class));
return user;
}, id);
}
public List<AppUser> findAll() {
String sql = "SELECT id, username, email, created_at FROM app_user";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
AppUser user = new AppUser();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setCreatedAt(rs.getObject("created_at", LocalDateTime.class));
return user;
});
}
}
这里有一个容易忽略的问题:queryForObject 在查询不到结果时会抛出 EmptyResultDataAccessException。如果不希望用异常控制流程,可以改用 query 返回列表后再判断,或者使用 Optional 封装。对于参数较多的情况,问号占位符容易因为顺序错误导致映射混乱,这时可以改用 NamedParameterJdbcTemplate,它支持具名参数,可读性会好很多。
@Repository
public class NamedUserRepository {
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public NamedUserRepository(NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
}
public AppUser findByEmail(String email) {
String sql = "SELECT id, username, email, created_at FROM app_user WHERE email = :email";
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("email", email);
List<AppUser> users = namedParameterJdbcTemplate.query(sql, params, (rs, rowNum) -> {
AppUser user = new AppUser();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
user.setCreatedAt(rs.getObject("created_at", LocalDateTime.class));
return user;
});
return users.isEmpty() ? null : users.get(0);
}
}
批量操作是 JdbcTemplate 的一大优势,尤其在做数据导入、报表计算或迁移任务时,batchUpdate 能减少网络往返次数,提升吞吐量。下面的示例演示了批量插入用户记录。
public int[] batchInsert(List<AppUser> users) {
String sql = "INSERT INTO app_user(username, email) VALUES (?, ?)";
List<Object[]> batchArgs = users.stream()
.map(user -> new Object[]{user.getUsername(), user.getEmail()})
.collect(Collectors.toList());
return jdbcTemplate.batchUpdate(sql, batchArgs);
}
需要注意的是,批量操作默认会一次性提交所有参数,如果数据量特别大,建议按固定大小分组执行,避免单次请求占用过多内存。对于需要返回自增主键的场景,可以使用 GeneratedKeyHolder 配合 update 方法,这是很多初学者容易忽略的细节。
三、事务管理与连接池调优
Spring Boot 默认启用事务管理,业务层使用 @Transactional 即可。JdbcTemplate 获取连接时并不是直接向 DataSource 要一个新连接,而是通过 DataSourceUtils 获取与当前线程绑定的连接,因此可以加入由 Spring 管理的事务。下面是一个简单的服务层示例。
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional(rollbackFor = Exception.class)
public void createTwoUsers(AppUser first, AppUser second) {
userRepository.insert(first);
if (second.getUsername() == null) {
throw new IllegalArgumentException("用户名不能为空");
}
userRepository.insert(second);
}
}
事务方法内一旦抛出 RuntimeException,整个操作会回滚。但如果在 DAO 内部捕获了异常而没有继续抛出,事务感知不到失败,就会造成部分提交。这是长期维护中非常隐蔽的坑,建议在 DAO 层只做数据库访问,不要在事务边界内随意吞掉异常。
连接池的稳定性直接影响系统的长期运行表现。HikariCP 作为 Spring Boot 默认连接池,性能已经足够优秀,但参数仍需根据实际负载调整。下面是一组生产环境常用的配置。
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 10000
其中 maximum-pool-size 需要和数据库端的最大连接数匹配,设置过大会导致数据库拒绝新连接,过小则会在高峰时段出现连接等待超时。leak-detection-threshold 建议开启,它能在连接被占用的时间超过阈值时输出警告日志,帮助定位连接泄漏问题。长期运行的系统还应配合慢 SQL 日志和 APM 工具,找出长时间占用连接的具体语句。
四、上手体验与长期使用感受
刚接触 JdbcTemplate 时,只要写过原生 JDBC 就能快速理解它的设计思路。相比 JPA 的自动映射和缓存管理,JdbcTemplate 更加透明,执行了什么 SQL、传入了什么参数一目了然。对于复杂报表、数据清洗和批量任务,这种透明度可以显著降低排错成本。但另一方面,它要求开发者手工处理结果集映射,表结构变化时相关代码需要同步修改,维护量并不小。
长期使用下来,最容易出问题的不是 JdbcTemplate 本身,而是 SQL 散落在各个 Repository 中,时间一长就难以维护。建议将 SQL 定义在独立的常量类中,或者集中到外部配置文件里。下面是一种简单的常量管理方式。
public final class UserSql {
private UserSql() {}
public static final String INSERT = "INSERT INTO app_user(username, email) VALUES (?, ?)";
public static final String FIND_BY_ID = "SELECT id, username, email, created_at FROM app_user WHERE id = ?";
public static final String FIND_ALL = "SELECT id, username, email, created_at FROM app_user";
}
与 MyBatis 相比,JdbcTemplate 不擅长处理动态 SQL,例如多条件组合查询需要手工拼接字符串,不仅繁琐而且容易引入 SQL 注入风险。MyBatis 的 XML 或注解方式对动态条件有更好的支持。与 JPA 相比,JdbcTemplate 缺少对象关系映射和一级缓存,但优势在于不会生成难以优化的 SQL,也不会因为会话状态管理带来额外复杂度。因此,如果项目以简单 CRUD 为主,JPA 可能更高效;如果动态 SQL 很多,MyBatis 更合适;如果结构稳定但 SQL 复杂、批处理频繁,JdbcTemplate 反而是更直接的方案。
最后必须强调,无论选择哪种持久层方案,都不要使用字符串拼接 SQL。下面的对比代码展示了错误和正确的做法。
// 错误写法:字符串拼接可能被 SQL 注入 String sql = "SELECT * FROM app_user WHERE username = '" + username + "'"; // 正确写法:使用占位符 String safeSql = "SELECT * FROM app_user WHERE username = ?";
Spring Boot 配置 JdbcTemplate 本身并不复杂,难点在于理解自动装配条件、RowMapper 设计、事务边界以及连接池参数对长期稳定性的影响。把这些基础打牢之后,JdbcTemplate 可以成为项目中轻量、高效、可控的数据访问层。
JdbcTemplateSpring Boot配置数据库访问修改时间:2026-09-24 07:50:45