如果你完整跟下了前几篇,亲手写过原生 JDBC,也配置过连接池,那你现在应该已经能顺畅地操作数据库了。

但你可能也发现了一个问题:连接池只解决了“创建连接”的代价,但每次操作数据库,该写的 try-catch-finally 一行没少,该手动映射的 rs.getString("xxx") 一个没落。

换句话说,连接池让程序跑得更快了,但开发者的手更酸了。

这一篇,我们来看 Spring 如何用 模板方法模式,把 JDBC 操作中那些“重复但不得不写”的代码彻底干掉。

学习目标

  • 理解 JdbcTemplate 如何封装了 JDBC 的重复代码(资源管理、异常转换)
  • 掌握 JdbcTemplate 的核心方法(queryupdateexecute)的使用
  • 理解 Spring 的 DataAccessException 体系——为何要包装 SQLException
  • 理解 JdbcTemplate 与 MyBatis 的定位差异

正文

一、从原生 JDBC 到 JdbcTemplate:并排对比

我们直接看代码。同样的功能——根据 ID 查询用户——用原生 JDBC 和 JdbcTemplate 分别怎么写。

左边:原生 JDBC(约 35 行)

public User getUserById(int id) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    User user = null;
    try {
        conn = DriverManager.getConnection(URL, USER, PASSWORD);
        String sql = "SELECT id, username, email, age FROM users WHERE id = ?";
        pstmt = conn.prepareStatement(sql);
        pstmt.setInt(1, id);
        rs = pstmt.executeQuery();
        if (rs.next()) {
            user = new User();
            user.setId(rs.getInt("id"));
            user.setUsername(rs.getString("username"));
            user.setEmail(rs.getString("email"));
            user.setAge(rs.getInt("age"));
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        if (rs != null) { try { rs.close(); } catch (SQLException e) {} }
        if (pstmt != null) { try { pstmt.close(); } catch (SQLException e) {} }
        if (conn != null) { try { conn.close(); } catch (SQLException e) {} }
    }
    return user;
}

右边:JdbcTemplate(约 12 行)

public User getUserById(int id) {
    String sql = "SELECT id, username, email, age FROM users WHERE id = ?";
    return jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> {
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setUsername(rs.getString("username"));
        user.setEmail(rs.getString("email"));
        user.setAge(rs.getInt("age"));
        return user;
    });
}

代码量减少了三分之二。但这只是表面。真正的变化在于:

  • 资源管理ConnectionPreparedStatementResultSet 的创建和关闭,全部由 JdbcTemplate 接管。你不再需要写 finally 块。
  • 异常处理SQLException 被转换为 Spring 的 DataAccessException(非受检异常),不再强制 try-catch
  • 结果映射:通过 RowMapper 将结果集映射逻辑集中在一处,清晰且可复用。

JdbcTemplate 并没有“消灭”JDBC,它只是把那些“每次都一样”的步骤封装了起来,让你只关注“每次不一样”的部分。

二、模板方法模式:定义骨架,让子类填充变化

JdbcTemplate 的设计背后是一个经典的设计模式——模板方法模式(Template Method Pattern)

这个模式的核心思想是:在父类中定义一个操作的整体骨架,把某些步骤延迟到子类中实现

听起来抽象,我们看 JdbcTemplate 的实际代码就明白了。

JdbcTemplate 中有一个核心方法 execute,它是所有其他方法的基础:

// JdbcTemplate 的核心骨架(简化版)
public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) 
        throws DataAccessException {
    
    // 1. 获取连接(由模板负责)
    Connection con = DataSourceUtils.getConnection(obtainDataSource());
    PreparedStatement ps = null;
    try {
        // 2. 创建 PreparedStatement(由模板负责)
        ps = psc.createPreparedStatement(con);
        // 3. 执行 SQL(由模板负责)
        ResultSet rs = ps.executeQuery();
        // 4. 处理结果集 —— 这一步留给“回调”来实现
        return action.doInPreparedStatement(ps);
    } catch (SQLException ex) {
        // 5. 异常转换(由模板负责)
        throw translateException(ex);
    } finally {
        // 6. 释放资源(由模板负责)
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(ps);
        DataSourceUtils.releaseConnection(con, getDataSource());
    }
}

模板方法模式的价值

  • 固定的步骤(获取连接、创建 Statement、释放资源)由模板类实现,不会变
  • 变化的步骤(SQL 怎么写、结果怎么映射)通过回调接口(如 PreparedStatementCallbackRowMapper)交给调用者实现
  • 调用者只需要关注“业务逻辑”,不需要关心“模板代码”

你平时用的 queryForObjectupdatebatchUpdate 等方法,底层都是基于这个 execute 骨架构建的。

三、RowMapper:结果集映射的优雅方案

在原生 JDBC 中,结果集映射是最繁琐的部分。JdbcTemplateRowMapper 解决了这个问题。

RowMapper 是一个函数式接口:

@FunctionalInterface
public interface RowMapper<T> {
    T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

使用方式一:匿名内部类(Java 8 之前的老写法)

List<User> users = jdbcTemplate.query(sql, new RowMapper<User>() {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setUsername(rs.getString("username"));
        return user;
    }
});

使用方式二:Lambda 表达式(Java 8+,推荐)

List<User> users = jdbcTemplate.query(sql, (rs, rowNum) -> {
    User user = new User();
    user.setId(rs.getInt("id"));
    user.setUsername(rs.getString("username"));
    return user;
});

使用方式三:复用 RowMapper 实例(最佳实践)

如果同一个映射逻辑在多处使用,可以定义为静态常量,避免重复创建:

public class UserRowMapper implements RowMapper<User> {
    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
        User user = new User();
        user.setId(rs.getInt("id"));
        user.setUsername(rs.getString("username"));
        user.setEmail(rs.getString("email"));
        user.setAge(rs.getInt("age"));
        user.setCreateTime(rs.getTimestamp("create_time"));
        return user;
    }
}

// 使用
List<User> users = jdbcTemplate.query(sql, new UserRowMapper());

四、异常转换体系:为什么要包装 SQLException?

如果你用过原生 JDBC,你一定对 SQLException 有复杂的情感——它什么错误都能抛,但每次抛出来都让你一脸茫然。

SQLException 的问题

  1. 受检异常:必须 try-catch,导致代码中到处都是异常处理
  2. 信息过于笼统SQLException 包含了错误码、SQLState、链式异常,但开发者往往不知道该怎么处理
  3. 无法区分“可恢复”和“不可恢复”的异常:连接超时和 SQL 语法错误都包装在同一个异常里

Spring 做了两件事来解决这个问题:

第一件事:将受检异常转换为非受检异常

SQLException(受检)→ DataAccessException(非受检,RuntimeException 的子类)

这意味着你可以选择是否处理异常。对于那些“无法恢复”的异常(比如 SQL 语法错误),你可以不处理,让异常向上抛出,由全局异常处理器统一处理。

第二件事:细化了异常层次结构

Spring 根据 SQLException错误码(vendor-specific)将其映射到不同的子类:

数据库错误 Spring 异常
连接失败 CannotGetJdbcConnectionException
SQL 语法错误 BadSqlGrammarException
数据重复(唯一约束冲突) DuplicateKeyException
查询结果为空但期望有值 EmptyResultDataAccessException
死锁 DeadlockLoserDataAccessException

这种细化的异常体系,让你可以针对不同的错误类型做不同的处理。比如:

try {
    userService.save(user);
} catch (DuplicateKeyException e) {
    // 用户名已存在,给用户友好提示
    return "用户名已被占用";
} catch (DataAccessException e) {
    // 其他数据库错误,记录日志并返回通用错误
    log.error("数据库操作失败", e);
    return "系统繁忙,请稍后重试";
}

五、JdbcTemplate 的局限:为什么还需要 ORM?

JdbcTemplate 已经很好了,但它仍然存在一些局限:

局限一:SQL 仍然是硬编码的

SQL 语句依然写在 Java 代码中。如果 SQL 需要修改,你还是得修改 Java 代码、重新编译、重新部署。

局限二:结果映射仍需手动编码

虽然 RowMapper 比原生 JDBC 优雅很多,但每个字段你还是要手动 rs.getString("xxx")。如果一张表有 50 个字段,你还是得写 50 行映射代码。

局限三:对象关系映射(ORM)能力缺失

  • 关联查询的结果无法自动封装成嵌套的对象结构(比如 User 中包含 List<Order>
  • 无法通过“对象操作”自动生成 SQL(比如 userDao.save(user)

这就是为什么还需要 MyBatisJPA/Hibernate 这样的 ORM 框架。

定位差异:

维度 JdbcTemplate MyBatis JPA/Hibernate
定位 增强型 JDBC 半自动 ORM 全自动 ORM
SQL 编写 Java 代码中写 SQL XML 或注解中写 SQL 自动生成 SQL
结果映射 手动 RowMapper 自动映射或 ResultMap 自动映射(约定配置)
控制力 ⭐⭐⭐⭐⭐ 完全控制 SQL ⭐⭐⭐⭐ 精细控制 ⭐⭐ 黑盒较多
学习曲线
适用场景 简单查询、快速开发 复杂 SQL、需要精细优化 标准 CRUD、领域模型驱动

简单说:JdbcTemplate 是“原始 JDBC 的语法糖”,而 MyBatis 是“SQL 与 Java 代码的分离工具”

代码示例

示例一:使用 JdbcTemplate 完成 CRUD

这是 Spring Boot 3.x 环境下的完整示例。

项目依赖(pom.xml 核心部分):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

配置(application.yml):

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 10

DAO 层实现:

package com.example.demo.dao;

import com.example.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.List;

@Repository
public class UserDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 查询所有用户 —— 使用 BeanPropertyRowMapper(自动映射)
     * 要求:数据库字段名和 Java 属性名一致(或开启驼峰映射)
     */
    public List<User> findAll() {
        String sql = "SELECT id, username, email, age, create_time FROM users";
        // BeanPropertyRowMapper 通过反射自动将 ResultSet 的列名映射到 User 的属性
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
    }

    /**
     * 根据 ID 查询用户 —— 使用自定义 RowMapper
     * 适用场景:字段名不一致,或需要特殊转换
     */
    public User getById(int id) {
        String sql = "SELECT id, username, email, age, create_time FROM users WHERE id = ?";
        try {
            return jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper());
        } catch (EmptyResultDataAccessException e) {
            return null;  // 查询不到返回 null
        }
    }

    /**
     * 插入用户 —— 获取自动生成的主键
     */
    public User save(User user) {
        String sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();

        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            ps.setString(1, user.getUsername());
            ps.setString(2, user.getEmail());
            ps.setInt(3, user.getAge());
            return ps;
        }, keyHolder);

        // 设置自动生成的主键
        Number key = keyHolder.getKey();
        if (key != null) {
            user.setId(key.intValue());
        }
        return user;
    }

    /**
     * 更新用户
     */
    public int update(User user) {
        String sql = "UPDATE users SET username = ?, email = ?, age = ? WHERE id = ?";
        return jdbcTemplate.update(sql, 
            user.getUsername(), 
            user.getEmail(), 
            user.getAge(), 
            user.getId()
        );
    }

    /**
     * 删除用户
     */
    public int deleteById(int id) {
        String sql = "DELETE FROM users WHERE id = ?";
        return jdbcTemplate.update(sql, id);
    }

    /**
     * 统计用户总数
     */
    public int count() {
        String sql = "SELECT COUNT(*) FROM users";
        return jdbcTemplate.queryForObject(sql, Integer.class);
    }

    /**
     * 自定义 RowMapper —— 可复用
     */
    private static class UserRowMapper implements RowMapper<User> {
        @Override
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setUsername(rs.getString("username"));
            user.setEmail(rs.getString("email"));
            user.setAge(rs.getInt("age"));
            Timestamp createTime = rs.getTimestamp("create_time");
            if (createTime != null) {
                user.setCreateTime(createTime.toLocalDateTime());
            }
            return user;
        }
    }
}

关键点:

  1. BeanPropertyRowMapper 可以自动映射大部分场景,但要求字段名完全匹配(或开启 spring.jdbc.template.auto-generate-key 的驼峰转换)
  2. queryForObject 在查询不到数据时会抛出 EmptyResultDataAccessException,需要用 try-catch 处理
  3. KeyHolder 用于获取数据库自动生成的主键(如 MySQL 的自增 ID)

示例二:批量操作 —— 一次性插入 1000 条数据

package com.example.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

@Service
public class BatchService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 批量插入 1000 条用户数据
     * 使用 batchUpdate 将多条 SQL 合并为一次网络请求发送
     */
    @Transactional
    public void batchInsert(int count) {
        String sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";

        // 准备批量参数
        List<Object[]> batchArgs = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            batchArgs.add(new Object[]{
                "user_" + i,
                "user_" + i + "@example.com",
                20 + (i % 10)
            });
        }

        // 执行批量插入 —— 返回每个 SQL 影响的行数数组
        int[] result = jdbcTemplate.batchUpdate(sql, batchArgs);
        System.out.println("批量插入完成,影响行数: " + result.length);
    }

    /**
     * 对比:单条插入 —— 慢得多
     */
    public void singleInsert(int count) {
        String sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
        for (int i = 0; i < count; i++) {
            jdbcTemplate.update(sql, "user_" + i, "user_" + i + "@example.com", 20 + (i % 10));
        }
    }
}

性能差异:单条插入 1000 条数据,每条都是一次网络往返(约 1ms),总耗时约 1 秒。批量插入将 1000 条 SQL 合并为一次网络请求发送,总耗时约 10-20ms。差距可达 50-100 倍。

新手错误 vs 正确姿势

错误表象 根本原因 正确姿势
JdbcTemplate 中拼接 SQL 字符串,使用 + 拼接参数 未使用参数化占位符,既可能导致 SQL 注入,也容易写错引号 使用 ? 占位符,参数通过数组或可变参数传递
每次查询都创建一个新的 RowMapper 匿名内部类 未复用 RowMapper 实例,造成不必要的对象创建 RowMapper 定义为静态内部类静态常量,或使用 BeanPropertyRowMapper
queryForObject 查不到数据时抛出异常,未处理导致 500 未理解 queryForObject 的语义——期望有且仅有一条数据 使用 try-catch 捕获 EmptyResultDataAccessException,或改用 query + 判空
批量操作未开启事务,部分成功部分失败 每条 SQL 独立提交,第一条成功了、第二条失败了,第一条不会回滚 使用 @Transactional 确保批量操作的原子性

疑难深度追问

Q1:JdbcTemplate 是线程安全的吗?

是。 JdbcTemplate 不持有任何状态(stateless)。它只在方法执行时从 DataSource 获取连接,执行完立即归还。所有局部变量都在方法栈中,线程之间完全隔离。所以你可以(也应该)将 JdbcTemplate 声明为单例,在多线程环境中安全共享。Spring 默认就是这么做的——注入的 JdbcTemplate 就是一个单例 Bean。

Q2:为什么 Spring 不直接把 JdbcTemplate 作为唯一的数据库访问方案?

因为 JdbcTemplate 解决的只是“JDBC 操作太繁琐”的问题,但没有解决“对象关系映射”的问题。在复杂的企业应用中:

  • 一个 Order 对象可能关联 List<OrderItem>JdbcTemplate 无法自动处理这种嵌套关系
  • 多表关联查询的结果集,用 RowMapper 手动组装非常痛苦
  • 分页查询、多租户、软删除等通用需求,JdbcTemplate 没有内置支持

这些场景需要更专业的框架(MyBatis、JPA)来处理。JdbcTemplate 的设计定位就是轻量级的 JDBC 增强工具,而不是一个完整的 ORM 框架。

Q3:BeanPropertyRowMapper 的自动映射是怎么实现的?为什么有时候映射不上?

BeanPropertyRowMapper 在底层使用 Java 内省(Introspection) 机制:

  1. 通过 ResultSetMetaData 获取查询结果的列名
  2. 将列名转换为 JavaBean 的属性名(下划线转驼峰,如 user_nameuserName
  3. 通过反射调用 setXxx() 方法完成赋值

映射不上最常见的原因:

  • 列名是 user_name,但 Java 属性是 username(少了下划线对应的驼峰)——需要开启 spring.jdbc.template.auto-generate-key 的命名策略
  • Java 类没有对应的 setXxx() 方法
  • Java 属性的类型和数据库字段类型不兼容(如 LocalDateTimeTIMESTAMP,但绝大多数场景下 BeanPropertyRowMapper 支持直接映射)

思考与延伸

  1. 对比实验:用原生 JDBC、JdbcTemplate 分别实现一个复杂的多表关联查询,对比代码行数和可读性。然后试着用 RowMapper 组装一个包含 List<Order>User 对象,感受一下 ORM 的必要性。

  2. 思考题JdbcTemplate 使用了模板方法模式,而 NamedParameterJdbcTemplate 是对 JdbcTemplate 的封装。这种“嵌套封装”的设计模式叫什么?(提示:装饰者模式)

  3. 动手改造:如果你现在用的是 MyBatis-Plus,试着在一个查询中改用 JdbcTemplate 来实现,对比两种方式的开发效率和代码可维护性。你会发现各有千秋——JdbcTemplate 写简单查询非常快,但复杂查询的 SQL 拼接远不如 MyBatis XML 优雅。

参考与延伸阅读

  • Spring Framework. JdbcTemplate API Documentation. Spring Framework Javadoc, 6.0.x
  • Spring Framework. DataAccessException Hierarchy. Spring Framework Javadoc
  • Baeldung. Spring JdbcTemplate Example. Baeldung, 2024
  • 阿里云开发者社区. JdbcTemplate 与 MyBatis 对比分析. 2023
  • 腾讯云. Spring JdbcTemplate 使用指南. 腾讯云开发者社区
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐