1. 为什么选择Spring整合JDBC
在Java企业级应用开发中,数据持久化是核心需求之一。虽然现在有Hibernate、MyBatis等ORM框架大行其道,但JDBC作为Java官方提供的数据库连接规范,仍然是所有高级持久层框架的底层基础。Spring对JDBC的封装,完美解决了原生JDBC开发中的诸多痛点。
原生JDBC开发存在几个明显问题:首先,需要手动管理数据库连接,容易造成资源泄漏;其次,异常处理繁琐,需要捕捉SQLException并处理;再者,大量重复模板代码(如获取连接、准备语句、处理结果集、关闭资源等)。Spring的JdbcTemplate通过模板方法模式,将这些固定流程封装起来,开发者只需关注核心SQL逻辑。
我曾在金融项目中处理过百万级交易记录,当时选择Spring JDBC而非Hibernate,主要考虑到三个因素:第一,需要精细控制每条SQL的执行;第二,批量操作性能要求极高;第三,已有存储过程需要直接调用。Spring JDBC在这些场景下展现出明显优势。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与基础配置
2.1 依赖管理
使用Maven构建项目时,需要引入以下核心依赖:
xml复制<dependencies>
<!-- Spring核心容器 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<!-- Spring JDBC支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.18</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- 连接池推荐使用HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</dependency>
</dependencies>
注意:实际项目中建议使用Spring Boot的starter-parent管理版本,这里为展示原理采用手动配置。
2.2 数据源配置
数据源是JDBC操作的基础,Spring支持多种配置方式。以下是Java配置类的典型实现:
java复制@Configuration
@PropertySource("classpath:jdbc.properties")
public class DataSourceConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
return new HikariDataSource(config);
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
对应的jdbc.properties文件内容:
properties复制jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
3. CRUD操作实战
3.1 实体类与表结构
假设我们操作的用户表结构如下:
sql复制CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`email` varchar(100) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
对应的Java实体类:
java复制public class User {
private Long id;
private String username;
private String password;
private String email;
private Date createTime;
// 省略getter/setter和toString
}
3.2 增删改查实现
3.2.1 插入操作
java复制@Repository
public class UserDaoImpl implements UserDao {
private final JdbcTemplate jdbcTemplate;
@Autowired
public UserDaoImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public int insert(User user) {
String sql = "INSERT INTO user(username, password, email) VALUES(?,?,?)";
return jdbcTemplate.update(sql,
user.getUsername(),
user.getPassword(),
user.getEmail());
}
// 获取自增主键
public Long insertAndReturnId(User user) {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO user(username, password, email) VALUES(?,?,?)",
Statement.RETURN_GENERATED_KEYS);
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setString(3, user.getEmail());
return ps;
}, keyHolder);
return keyHolder.getKey().longValue();
}
}
3.2.2 查询操作
java复制// 查询单个对象
@Override
public User findById(Long id) {
String sql = "SELECT * FROM user WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setEmail(rs.getString("email"));
user.setCreateTime(rs.getTimestamp("create_time"));
return user;
});
}
// 查询列表
@Override
public List<User> findAll() {
String sql = "SELECT * FROM user";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setEmail(rs.getString("email"));
user.setCreateTime(rs.getTimestamp("create_time"));
return user;
});
}
3.2.3 更新与删除
java复制@Override
public int update(User user) {
String sql = "UPDATE user SET username=?, password=?, email=? WHERE id=?";
return jdbcTemplate.update(sql,
user.getUsername(),
user.getPassword(),
user.getEmail(),
user.getId());
}
@Override
public int delete(Long id) {
String sql = "DELETE FROM user WHERE id=?";
return jdbcTemplate.update(sql, id);
}
3.3 批量操作
Spring JDBC对批量操作有良好支持,能显著提升性能:
java复制// 批量插入
public int[] batchInsert(List<User> users) {
String sql = "INSERT INTO user(username, password, email) VALUES(?,?,?)";
return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = users.get(i);
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setString(3, user.getEmail());
}
@Override
public int getBatchSize() {
return users.size();
}
});
}
// 批量更新
public int[] batchUpdate(List<User> users) {
String sql = "UPDATE user SET username=?, password=?, email=? WHERE id=?";
return jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
User user = users.get(i);
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setString(3, user.getEmail());
ps.setLong(4, user.getId());
}
@Override
public int getBatchSize() {
return users.size();
}
});
}
4. 高级特性与最佳实践
4.1 事务管理
Spring的声明式事务对JDBC操作同样有效:
java复制@Service
public class UserServiceImpl implements UserService {
private final UserDao userDao;
@Autowired
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
@Transactional
@Override
public void createUsers(List<User> users) {
for (User user : users) {
userDao.insert(user);
// 模拟业务逻辑
if (user.getUsername().contains("admin")) {
throw new RuntimeException("不允许创建管理员账号");
}
}
}
}
4.2 命名参数支持
为避免参数位置混乱,可以使用NamedParameterJdbcTemplate:
java复制@Repository
public class UserDaoNamedImpl implements UserDao {
private final NamedParameterJdbcTemplate namedJdbcTemplate;
@Autowired
public UserDaoNamedImpl(DataSource dataSource) {
this.namedJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
@Override
public int update(User user) {
String sql = "UPDATE user SET username=:uname, password=:pwd, email=:email WHERE id=:id";
Map<String, Object> params = new HashMap<>();
params.put("uname", user.getUsername());
params.put("pwd", user.getPassword());
params.put("email", user.getEmail());
params.put("id", user.getId());
return namedJdbcTemplate.update(sql, params);
}
}
4.3 存储过程调用
Spring JDBC简化了存储过程的调用:
java复制public class UserStatsDao {
private final JdbcTemplate jdbcTemplate;
public UserStatsDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public int getActiveUserCount(Date fromDate) {
return jdbcTemplate.execute(
"{call sp_get_active_user_count(?, ?)}",
(CallableStatementCallback<Integer>) cs -> {
cs.setDate(1, new java.sql.Date(fromDate.getTime()));
cs.registerOutParameter(2, Types.INTEGER);
cs.execute();
return cs.getInt(2);
});
}
}
4.4 性能优化建议
-
合理使用批处理:对于批量插入/更新操作,批处理比单条执行效率高10倍以上。但要注意批处理大小,建议每批500-1000条。
-
正确使用RowMapper:避免在RowMapper中执行耗时操作,保持其轻量。复杂对象映射可以考虑使用BeanPropertyRowMapper。
-
SQL日志监控:开发环境可以配置DataSource拦截器打印SQL日志,但生产环境要关闭:
java复制@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
// ...其他配置
if (env.acceptsProfiles("dev")) {
return new DataSourceProxy(new HikariDataSource(config));
}
return new HikariDataSource(config);
}
- 连接池配置:根据系统负载调整连接池参数。一个经验公式:maximumPoolSize = Tn * (Cm - 1) + 1,其中Tn是线程数,Cm是每个线程需要的连接数。
5. 常见问题排查
5.1 连接泄漏问题
症状:应用运行一段时间后出现连接耗尽,抛出"Timeout waiting for connection"异常。
排查步骤:
- 检查是否所有JdbcTemplate操作都在try-with-resources或Spring事务中
- 启用连接泄漏检测:
config.setLeakDetectionThreshold(30000) - 使用监控工具查看连接获取/释放堆栈
5.2 SQL语法兼容性问题
不同数据库方言差异会导致SQL执行失败。解决方案:
- 使用Spring的DatabaseMetaData获取数据库信息
- 为不同数据库准备不同的SQL脚本
- 使用
SimpleJdbcCall自动适配存储过程调用语法
5.3 类型转换异常
常见于日期类型或枚举类型处理。推荐做法:
- 使用JdbcTemplate的
getDateTimeValue()等方法处理日期 - 自定义
SqlParameterValue处理特殊类型 - 枚举类型实现
SqlEnum接口或使用EnumTypeHandler
5.4 性能问题分析
当发现JDBC操作变慢时:
- 检查SQL执行计划,添加必要索引
- 分析连接获取时间占比
- 检查批处理是否生效
- 监控网络延迟(特别是云数据库)
6. 测试策略
6.1 单元测试
使用内存数据库H2进行快速测试:
java复制@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TestConfig.class)
public class UserDaoTest {
@Autowired
private UserDao userDao;
@Test
public void testInsertAndQuery() {
User user = new User();
user.setUsername("test");
user.setPassword("123");
user.setEmail("test@example.com");
Long id = userDao.insertAndReturnId(user);
assertNotNull(id);
User queried = userDao.findById(id);
assertEquals("test", queried.getUsername());
}
}
@Configuration
public class TestConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public UserDao userDao() {
return new UserDaoImpl(jdbcTemplate(dataSource()));
}
}
6.2 集成测试
使用Testcontainers进行真实数据库测试:
java复制@SpringBootTest
@Testcontainers
public class UserDaoIntegrationTest {
@Container
private static final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void registerPgProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private UserDao userDao;
@Test
public void testBatchInsertPerformance() {
List<User> users = generateTestUsers(1000);
long start = System.currentTimeMillis();
userDao.batchInsert(users);
long duration = System.currentTimeMillis() - start;
assertTrue(duration < 1000); // 确保批处理性能
}
}
6.3 性能测试
使用JMeter进行批量操作压力测试,重点关注:
- 不同批处理大小对TPS的影响
- 连接池大小与系统吞吐量的关系
- 长时间运行下的内存使用情况
7. 扩展与进阶
7.1 与MyBatis集成
Spring JDBC可以与MyBatis混合使用,在需要复杂SQL时使用MyBatis,简单CRUD使用JdbcTemplate:
java复制@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return sessionFactory.getObject();
}
}
// 在Service中同时注入JdbcTemplate和MyBatis Mapper
@Service
@RequiredArgsConstructor
public class HybridService {
private final JdbcTemplate jdbcTemplate;
private final UserMapper userMapper;
public void complexOperation() {
// 简单操作用JdbcTemplate
jdbcTemplate.update("UPDATE user SET last_login = NOW() WHERE id = ?", 1);
// 复杂查询用MyBatis
List<User> users = userMapper.selectByComplexCondition(...);
}
}
7.2 响应式编程支持
Spring Data R2DBC提供了响应式JDBC支持:
java复制@Repository
public class ReactiveUserRepository {
private final DatabaseClient databaseClient;
public ReactiveUserRepository(DatabaseClient databaseClient) {
this.databaseClient = databaseClient;
}
public Flux<User> findAll() {
return databaseClient.sql("SELECT * FROM user")
.map((row, meta) -> {
User user = new User();
user.setId(row.get("id", Long.class));
user.setUsername(row.get("username", String.class));
return user;
})
.all();
}
}
7.3 多数据源配置
大型项目常需要访问多个数据库:
java复制@Configuration
public class MultiDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties("app.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
使用时分数据源注入:
java复制@Service
public class MultiDbService {
private final JdbcTemplate primaryJdbcTemplate;
private final JdbcTemplate secondaryJdbcTemplate;
public MultiDbService(
@Qualifier("primaryJdbcTemplate") JdbcTemplate primaryJdbcTemplate,
@Qualifier("secondaryJdbcTemplate") JdbcTemplate secondaryJdbcTemplate) {
this.primaryJdbcTemplate = primaryJdbcTemplate;
this.secondaryJdbcTemplate = secondaryJdbcTemplate;
}
}
7.4 自定义JdbcTemplate扩展
可以继承JdbcTemplate添加自定义方法:
java复制public class ExtendedJdbcTemplate extends JdbcTemplate {
public <T> List<T> queryForList(String sql, Class<T> elementType) {
return super.query(sql, (rs, rowNum) -> {
try {
T instance = elementType.newInstance();
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(instance);
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
String columnName = rsmd.getColumnLabel(i);
if (bw.isWritableProperty(columnName)) {
bw.setPropertyValue(columnName, rs.getObject(i));
}
}
return instance;
} catch (Exception e) {
throw new DataRetrievalFailureException("Could not map result set to class", e);
}
});
}
}
