乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 Spring概述与IoC容器

    • Spring概述与IoC容器
    • Spring Framework 概述
    • IoC 与 DI 核心概念
    • @Configuration 详解
    • @Component 详解
    • @ComponentScan 详解
    • @Import 详解
    • @Profile 详解
    • @PropertySource 详解
    • @Service 详解
    • @Repository 详解
  • 第2章 Bean的定义与依赖注入

    • Bean的定义与依赖注入
    • @Bean 详解
    • @Autowired 详解
    • @Qualifier 详解
    • @Primary 详解
    • @Resource 详解
    • @Inject 详解
    • @Named 详解
    • @Value 详解
    • @Scope 详解
    • @Lazy 详解
  • 第3章 Bean生命周期与作用域

    • Bean生命周期与作用域
    • Bean生命周期概述
    • @PostConstruct
    • @PreDestroy
    • InitializingBean
    • DisposableBean
    • BeanPostProcessor
    • BeanFactoryPostProcessor
  • 第4章 AOP面向切面编程

    • AOP面向切面编程
    • AOP核心概念
    • @EnableAspectJAutoProxy
    • @Aspect
    • @Pointcut
    • @Before
    • @After
    • @AfterReturning
    • @AfterThrowing
    • @Around
  • 第5章 数据访问与事务管理

    • 数据访问与事务管理
    • 数据访问概述
    • @EnableTransactionManagement
    • @Transactional
    • @Transactional 的传播行为
    • @Transactional 的隔离级别
    • @Transactional 的回滚规则
    • @Transactional 的超时与只读属性
    • @TransactionalEventListener
  • 第6章 Spring Boot自动配置基础

    • Spring Boot自动配置基础
    • @SpringBootApplication 注解
    • @EnableAutoConfiguration 注解
    • @ConfigurationProperties 注解
    • @ConditionalOnClass 注解
    • @ConditionalOnMissingBean 注解
    • @ConditionalOnProperty 注解
  • 第7章 从容器到Web: Spring MVC导引

    • Spring MVC 导引
  • 第8章 扩展阅读

    • 扩展阅读
    • Spring 事件机制 — ApplicationEvent / ApplicationListener
    • @EventListener
    • SpEL — Spring 表达式语言
    • 校验 Validation — JSR-303 / JSR-380 Bean Validation
    • 类型转换与数据绑定 — Converter / DataBinder
  • 附录

    • Spring Framework 专业术语
    • Spring 核心知识点
    • Spring 面试高频考点
    • Spring 核心注解速查表

数据访问概述

定义与作用

Spring 数据访问层的核心设计哲学是统一抽象。在传统的 Java 数据访问开发中,直接使用 JDBC、JPA 或 MyBatis 时,开发者需要重复处理连接获取、资源释放、异常转换等样板代码。Spring 通过两大机制解决这一痛点:

  • 模板方法模式(Template Method Pattern):将资源获取、清理等重复逻辑封装在模板类中,开发者只需关注核心业务 SQL 或持久化操作。
  • 统一异常体系:将各持久层技术(JDBC、JPA、Hibernate、MyBatis)的底层异常统一转换为 Spring 的 DataAccessException 层次结构,使业务层无需关心具体数据库产品的异常差异。

这种抽象带来的直接收益是:切换底层持久化技术时,业务层代码几乎无需改动。例如从 JDBC 迁移到 JPA,只需更换模板实现,Service 层的异常处理逻辑完全保持不变。

适用位置与常用组件

技术Spring 抽象作用典型使用位置
JDBCJdbcTemplate简化原生 JDBC,自动处理 Connection、Statement、ResultSet、异常转换@Repository 标注的 DAO 类
JPAJpaTemplate(已过时)/ EntityManager集成 JPA 的 EntityManager 管理JPA 环境下的 Repository
MyBatisSqlSessionTemplate集成 MyBatis,与 Spring 事务同步MyBatis 环境下的 Mapper
MongoDBMongoTemplateSpring Data MongoDB 的核心操作类NoSQL 数据访问层

在 Spring 5.x / Spring Boot 2.x 的实际项目中,JdbcTemplate 和 Spring Data JPA 是最常见的两种选择。本章以 JdbcTemplate 为切入点,因为它最能体现 Spring 的模板方法设计思想。

核心原理:模板方法模式与统一异常体系

模板方法模式的结构

Spring 的模板类(如 JdbcTemplate)将数据访问的固定流程抽象为模板,将变化点留给回调接口:

以 JdbcTemplate.query() 为例,其内部执行流程如下:

统一异常体系

Spring 将各持久化技术的异常统一映射到 org.springframework.dao.DataAccessException 下:

关键设计决策:DataAccessException 继承自 RuntimeException。这意味着开发者无需在 DAO 方法上声明 throws 子句,也无需在业务层编写繁琐的 try-catch 块。Spring 认为数据访问异常在大多数情况下是不可恢复的,应当向上传播,由上层事务管理器决定是否回滚。

完整示例:飞翔科技学生管理系统

场景简述

飞翔科技公司的后端开发 小崔 正在为公司内部培训平台开发学生选课模块。需要实现学生信息的增删改查,并确保数据库操作简洁、异常统一。白歌(架构师)要求小崔使用 JdbcTemplate 完成 DAO 层,禁止手写 try-finally 关闭资源。

操作前:数据库表结构与初始数据

-- 学生表
CREATE TABLE student (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    student_id INT NOT NULL COMMENT '学号',
    name VARCHAR(50) NOT NULL COMMENT '姓名',
    age SHORT COMMENT '年龄',
    major VARCHAR(50) COMMENT '专业',
    balance FLOAT COMMENT '账户余额(用于选课缴费)'
);

-- 初始数据
INSERT INTO student (student_id, name, age, major, balance) VALUES
(2024001, '大翔', 22, '计算机科学', 6666.66),
(2024002, '白歌', 21, '软件工程', 8888.88),
(2024003, '小崔', 20, '人工智能', 23200.00);

项目依赖(Spring Boot 2.x)

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

配置类:DataSource 与 JdbcTemplate

package com.feixiang.config;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class DataAccessConfig {

    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/feixiang_edu?useSSL=false&serverTimezone=Asia/Shanghai");
        config.setUsername("root");
        config.setPassword("secret");
        config.setMaximumPoolSize(10);
        return new HikariDataSource(config);
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

实体类:Student

package com.feixiang.entity;

public class Student {
    private Long id;
    private int studentId;
    private String name;
    private short age;
    private String major;
    private float balance;

    // 构造器、getter、setter 省略
    public Student() {}

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public int getStudentId() { return studentId; }
    public void setStudentId(int studentId) { this.studentId = studentId; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public short getAge() { return age; }
    public void setAge(short age) { this.age = age; }
    public String getMajor() { return major; }
    public void setMajor(String major) { this.major = major; }
    public float getBalance() { return balance; }
    public void setBalance(float balance) { this.balance = balance; }
}

DAO 层:StudentDao(使用 JdbcTemplate)

package com.feixiang.dao;

import com.feixiang.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class StudentDao {

    private final JdbcTemplate jdbcTemplate;
    private final RowMapper<Student> rowMapper;

    @Autowired
    public StudentDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        // BeanPropertyRowMapper 自动将列名映射到同名属性
        this.rowMapper = new BeanPropertyRowMapper<>(Student.class);
    }

    // 查询单个学生
    public Student findByStudentId(int studentId) {
        String sql = "SELECT id, student_id, name, age, major, balance FROM student WHERE student_id = ?";
        try {
            return jdbcTemplate.queryForObject(sql, rowMapper, studentId);
        } catch (EmptyResultDataAccessException e) {
            return null; // 查不到返回 null,而非抛出异常
        }
    }

    // 查询所有学生
    public List<Student> findAll() {
        String sql = "SELECT id, student_id, name, age, major, balance FROM student";
        return jdbcTemplate.query(sql, rowMapper);
    }

    // 更新余额(选课缴费场景)
    public int updateBalance(int studentId, float amount) {
        String sql = "UPDATE student SET balance = balance - ? WHERE student_id = ? AND balance >= ?";
        return jdbcTemplate.update(sql, amount, studentId, amount);
    }

    // 插入新学生
    public int insert(Student student) {
        String sql = "INSERT INTO student (student_id, name, age, major, balance) VALUES (?, ?, ?, ?, ?)";
        return jdbcTemplate.update(sql,
                student.getStudentId(),
                student.getName(),
                student.getAge(),
                student.getMajor(),
                student.getBalance());
    }

    // 删除学生
    public int deleteByStudentId(int studentId) {
        String sql = "DELETE FROM student WHERE student_id = ?";
        return jdbcTemplate.update(sql, studentId);
    }
}

Service 层:StudentService(为后续事务章节铺垫)

package com.feixiang.service;

import com.feixiang.dao.StudentDao;
import com.feixiang.entity.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class StudentService {

    private final StudentDao studentDao;

    @Autowired
    public StudentService(StudentDao studentDao) {
        this.studentDao = studentDao;
    }

    public Student getStudent(int studentId) {
        return studentDao.findByStudentId(studentId);
    }

    public List<Student> listAllStudents() {
        return studentDao.findAll();
    }

    public boolean deductBalance(int studentId, float courseFee) {
        int affected = studentDao.updateBalance(studentId, courseFee);
        return affected > 0;
    }
}

操作后:运行结果与数据变化

执行查询操作后,控制台输出:

【飞翔科技学生管理系统 - 查询结果】
学号:2024001,姓名:大翔,专业:计算机科学,余额:6666.66
学号:2024002,姓名:白歌,专业:软件工程,余额:8888.88
学号:2024003,姓名:小崔,专业:人工智能,余额:23200.00

执行扣费操作 deductBalance(2024001, 2000.00f) 后,数据库状态:

idstudent_idnameagemajorbalance
12024001大翔22计算机科学4666.66
22024002白歌21软件工程8888.88
32024003小崔20人工智能23200.00

若余额不足时执行 deductBalance(2024001, 10000.00f),update() 返回 0,Service 层返回 false,不会抛出异常。这是通过 SQL 的 AND balance >= ? 条件实现的乐观判断,而非先查后改。

易错场景与面试考点

反例一:忘记处理 EmptyResultDataAccessException

// 错误写法:直接调用 queryForObject,不处理空结果
public Student findByStudentId(int studentId) {
    String sql = "SELECT * FROM student WHERE student_id = ?";
    // 当学号不存在时,抛出 EmptyResultDataAccessException
    return jdbcTemplate.queryForObject(sql, rowMapper, studentId);
}

问题分析:queryForObject 在结果集为空时会抛出 EmptyResultDataAccessException(属于 DataAccessException 子类)。如果业务语义允许"查不到",应当捕获该异常返回 null,或改用 query() 后判断列表长度。

正确写法:

public Student findByStudentId(int studentId) {
    String sql = "SELECT * FROM student WHERE student_id = ?";
    List<Student> list = jdbcTemplate.query(sql, rowMapper, studentId);
    return list.isEmpty() ? null : list.get(0);
}

反例二:在 DAO 中手动管理 Connection

// 错误写法:回到原生 JDBC 的泥潭
public void badExample() {
    Connection conn = null;
    PreparedStatement pstmt = null;
    try {
        conn = dataSource.getConnection();
        pstmt = conn.prepareStatement("SELECT * FROM student");
        // ... 冗长的资源管理
    } catch (SQLException e) {
        throw new RuntimeException(e);
    } finally {
        // 极易遗漏关闭,且代码膨胀 3 倍以上
        if (pstmt != null) try { pstmt.close(); } catch (SQLException ignored) {}
        if (conn != null) try { conn.close(); } catch (SQLException ignored) {}
    }
}

问题分析:Spring 的 JdbcTemplate 已经通过模板方法模式将资源管理内聚到框架内部,手动管理不仅代码冗余,还极易在异常路径下泄漏连接,导致连接池耗尽。李眉(运维)凌晨三点被叫起来修服务器,十有八九是连接泄漏。

面试高频考点

Q1:Spring 为什么要将 SQLException 转换为 DataAccessException?

SQLException 是受检异常(Checked Exception),强制业务层处理或声明,但大多数数据访问异常不可恢复。Spring 将其转换为非受检的 DataAccessException,使业务层代码更干净,同时统一了不同数据库产品的错误码差异(如 MySQL 的 1062 与 Oracle 的唯一约束异常)。

Q2:JdbcTemplate 的 queryForObject 和 query 有什么区别?

queryForObject 期望结果集恰好为 1 行,为空时抛 EmptyResultDataAccessException,多于 1 行时抛 IncorrectResultSizeDataAccessException。query 返回 List<T>,允许 0 行、1 行或多行,更适合不确定结果数量的场景。

Q3:Spring 的模板方法模式与经典 GoF 模板方法模式有何不同?

经典模板方法通过继承抽象类并重写钩子方法实现变化点。Spring 的 JdbcTemplate 通过回调接口(如 RowMapper、PreparedStatementSetter)将变化点外化为策略对象,更符合"组合优于继承"的设计原则,也更适合 Java 的单继承限制。

上一页
数据访问与事务管理
下一页
@EnableTransactionManagement