【MyBatis】入门篇:一个订单系统吃透持久层框架
MyBatis 从入门到面试:一个订单系统带你吃透持久层框架
本文以电商订单系统为主线案例,从 JDBC 痛点出发,逐步深入 MyBatis 核心机制,覆盖注解式 CRUD、XML 映射、
#{}vs${}、SQL 注入防御、连接池选型、缓存机制等面试高频考点。全文八章,循序渐进,适合 Java 后端初学者和 1-3 年经验面试备战者。
目录
- 从 JDBC 到 MyBatis — 为什么要"轮子"
- 第一个 MyBatis 程序 — 查询商品列表
- MyBatis 日志 — 调试的"火眼金睛"
- MyBatis 注解式 CRUD
- XML 配置文件方式 — 复杂 SQL 的主场
#{}vs${}— 面试必问的"送命题"- 数据库连接池 — 性能的幕后英雄
- MyBatis 缓存机制 — 被忽略的面试杀器
一、从 JDBC 到 MyBatis — 为什么要"轮子"
1.1 JDBC 的八步"标准流程"
任何一个 Java 后端开发者都绕不开 JDBC。回忆一下操作数据库的标准八步:
- 引入数据库驱动依赖
- 创建
DataSource数据源 - 获取
Connection连接 - 构造 SQL 字符串
- 创建
PreparedStatement并替换占位符 - 执行 SQL(
executeQuery/executeUpdate) - 遍历
ResultSet手动映射到对象 - 在
finally中关闭连接、释放资源
每一步都是必不可少的,但每一步都让代码变得臃肿。假设我们要查询所有上架商品:
// JDBC 原生查询 —— 注意这不是本文案例,只是对比演示
DataSource ds = new MysqlDataSource();
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM product WHERE status = ?");
ResultSet rs = ps.executeQuery()) {
ps.setInt(1, 1);
List<Product> list = new ArrayList<>();
while (rs.next()) {
Product p = new Product();
p.setId(rs.getInt("id"));
p.setProdName(rs.getString("prod_name"));
p.setPrice(rs.getBigDecimal("price"));
// ... 还有 6 个字段
list.add(p);
}
}
一个简单查询写了近 20 行,而且每个 DAO 方法都要重复这段"连接 → SQL → 执行 → 映射 → 释放"的模板代码。
1.2 JDBC 的三大痛点
| 痛点 | 具体表现 | 后果 |
|---|---|---|
| 模板代码重复 | 每个方法都要写获取连接、try-catch-finally、释放资源 | 代码膨胀,维护噩梦 |
| 连接手动管理 | 开发者自己控制 Connection 的创建和销毁 | 忘记关闭 → 连接泄漏 → 数据库挂掉 |
| SQL 硬编码与参数拼接 | SQL 写在 Java 字符串里,参数手动 setXxx,结果集手动映射字段 |
改一个字段名要改 5 处代码,极易出错 |
1.3 MyBatis 解决了什么
MyBatis 是一个半自动 ORM 持久层框架。理解"半自动"这个词是面试中的加分项:
- 全自动 ORM(如 Hibernate):SQL 由框架生成,你只管对象操作。优点是开发快,缺点是复杂查询不可控。
- 半自动 ORM(MyBatis):SQL 仍然由你编写,框架帮你做连接管理、参数映射、结果映射。优点是你对 SQL 有绝对掌控权。
通俗类比:JDBC 像自己买菜、洗菜、切菜、炒菜、洗碗全流程一个人干。MyBatis 像净菜半成品——菜已洗好切好,锅碗瓢盆(连接管理、资源释放)框架帮你管,你只需要决定怎么炒(写 SQL)。
| JDBC 原生 | MyBatis | |
|---|---|---|
| 连接管理 | 手动创建/销毁 Connection | 连接池自动管理 |
| SQL 编写 | Java 字符串拼接,改一行动全身 | 注解/XML 集中管理 |
| 参数映射 | 逐个 setXxx() 设参 |
#{} 占位一行搞定 |
| 结果映射 | 手工 rs.getString() 逐字段 |
自动映射到实体类 |
| 资源释放 | try-catch-finally 必写 | 框架自动处理 |
📌 面试点:MyBatis 和 Hibernate 的本质区别?—— 半自动 vs 全自动 ORM。MyBatis 由开发者掌控 SQL,适合复杂查询和性能调优;Hibernate 自动生成 SQL,适合标准 CRUD 场景。
二、第一个 MyBatis 程序 — 查询商品列表
本章使用电商订单系统的 product 表,建表语句如下(后文所有代码均基于此表):
CREATE DATABASE ecommerce_db;
USE ecommerce_db;
CREATE TABLE product (
id INT PRIMARY KEY AUTO_INCREMENT,
prod_name VARCHAR(100) NOT NULL COMMENT '商品名称',
price DECIMAL(10,2) NOT NULL COMMENT '单价',
stock INT NOT NULL COMMENT '库存',
category VARCHAR(30) COMMENT '分类',
status TINYINT DEFAULT 1 COMMENT '状态(0下架/1上架)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO product (prod_name, price, stock, category) VALUES
('机械键盘 K8', 499.00, 120, '数码'),
('蓝牙耳机 Pro', 299.00, 80, '数码'),
('Java 核心技术 卷I', 149.00, 200, '图书'),
('MySQL 必知必会', 69.00, 150, '图书');
2.1 环境搭建
Spring Boot 项目中引入 MyBatis 起步依赖和 MySQL 驱动:
<!-- pom.xml -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
application.yml 配置数据库连接(四项必填):
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/ecommerce_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
2.2 实体类与 Mapper 接口
实体类遵循驼峰命名,与数据库的下划线字段对应:
@Data
public class Product {
private Integer id;
private String prodName; // 对应 prod_name
private BigDecimal price;
private Integer stock;
private String category;
private Integer status;
private Date createTime; // 对应 create_time
private Date updateTime; // 对应 update_time
}
Mapper 接口加上 @Mapper 注解,方法上写 @Select:
@Mapper
public interface ProductMapper {
@Select("SELECT * FROM product WHERE status = 1")
List<Product> listOnSale();
}
单元测试验证:
@SpringBootTest
class ProductMapperTest {
@Autowired
private ProductMapper productMapper;
@Test
void testListOnSale() {
List<Product> products = productMapper.listOnSale();
products.forEach(p -> System.out.println(p.getProdName()));
}
}
2.3 @Mapper 注解背后发生了什么
这是面试中容易被追问的点。@Mapper 并非简单的"标记注解",它触发了 MyBatis 的核心机制——动态代理:
你的代码调用 productMapper.listOnSale()
│
▼
动态代理对象拦截(MapperProxy)
│
▼
SqlSession 获取 MappedStatement(从 @Select 注解解析出的 SQL + 配置)
│
▼
Executor 执行器处理(含缓存、事务等拦截链)
│
▼
StatementHandler 创建 PreparedStatement、设参
│
▼
ResultSetHandler 将 JDBC 结果集映射为 List<Product>
│
▼
返回结果
四个核心对象的职责:
| 对象 | 职责 |
|---|---|
| SqlSession | 门面,对外提供 API(selectOne/selectList/insert/update/delete) |
| Executor | 执行器,负责 SQL 执行、缓存维护、事务管理 |
| StatementHandler | 封装 PreparedStatement 操作(创建、参数化、执行) |
| ResultSetHandler | 将 JDBC ResultSet 映射为 Java 对象集合 |

理解这条链路,你就能回答"为什么 Mapper 接口不需要实现类也能工作"——因为 MyBatis 在运行时为每个 @Mapper 接口生成了 JDK 动态代理对象,代理对象拦截方法调用并委托给 SqlSession 执行。
📌 面试点:
@Mapper和@Repository的区别?——@Mapper是 MyBatis 的注解,告诉框架为此接口生成代理对象并交 IOC 管理;@Repository是 Spring 的 stereotype 注解,仅标记为持久层组件。两者可共存,但@Mapper是 MyBatis 必需的。@MapperScan("com.example.mapper")的作用?—— 扫描指定包下所有接口并自动注册为 Mapper,省去每个接口单独加@Mapper。
三、MyBatis 日志 — 调试的"火眼金睛"
开发阶段打开 MyBatis SQL 日志是排查问题的标配操作。在 application.yml 中配置:
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
执行 listOnSale() 后控制台输出示例:
==> Preparing: SELECT * FROM product WHERE status = 1
==> Parameters:
<== Columns: id, prod_name, price, stock, category, status, create_time, update_time
<== Row: 1, 机械键盘 K8, 499.00, 120, 数码, 1, 2026-01-15 10:00:00, 2026-01-15 10:00:00
<== Row: 2, 蓝牙耳机 Pro, 299.00, 80, 数码, 1, 2026-01-15 10:00:00, 2026-01-15 10:00:00
<== Total: 2
日志三要素:
| 行 | 含义 | 价值 |
|---|---|---|
Preparing |
执行的 SQL 语句(占位符形式) | 检查 SQL 是否正确 |
Parameters |
传入的参数类型和值 | 检查参数是否匹配 |
Rows / Total |
每行返回值和总行数 | 检查结果是否符合预期 |
生产环境注意:StdOutImpl 将日志直接输出到标准输出流,性能较差且不便于集中管理。线上建议关闭或切换为 SLF4J 集成,通过日志框架统一管理输出级别。
四、MyBatis 注解式 CRUD
本章覆盖 CRUD 四操作 + 参数传递 + 自增主键返回 + 字段映射三大方案,是全文篇幅最大的模块。建议按子章节顺序阅读,也可直接跳转到你关心的部分。
4.1 参数传递
MyBatis 通过 #{} 占位符获取方法参数,本质是 JDBC 的 ? 预编译占位符。
单参数传递——名称任意:
@Select("SELECT * FROM product WHERE id = #{xxx}")
Product getById(Integer id);
#{xxx} 中写什么名字都可以,因为只有一个参数,MyBatis 直接用值填充唯一的 ?。
多参数传递——必须用 @Param 绑定:
@Select("SELECT * FROM product WHERE category = #{cate} AND status = #{st}")
List<Product> getByCategoryAndStatus(@Param("cate") String category,
@Param("st") Integer status);
不加 @Param 会报错,因为 MyBatis 对多参数默认使用 arg0、arg1 或 param1、param2 命名,你的 SQL 中写 #{cate} 找不到对应参数名。
对象传参——直接用属性名:
@Insert("INSERT INTO product(prod_name, price, stock, category) " +
"VALUES(#{prodName}, #{price}, #{stock}, #{category})")
int add(Product product);
当参数是一个对象时,#{} 中直接写对象的属性名即可。
4.2 新增 + 返回自增主键
@Insert 的返回值是影响行数,而非主键。要获取自增 ID,需配置 @Options:
@Insert("INSERT INTO customer(name, phone, level) VALUES(#{name}, #{phone}, #{level})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int addCustomer(Customer customer);
调用后:
Customer c = new Customer();
c.setName("张三");
c.setPhone("13800138000");
c.setLevel(1);
customerMapper.addCustomer(c);
System.out.println(c.getId()); // 输出自增 ID,如 1
主键被回填到参数对象的 id 属性中,返回值 int 仍是影响行数。
4.3 删除
@Delete("DELETE FROM product WHERE id = #{id}")
int deleteById(Integer id);
这只是物理删除。生产环境更推荐逻辑删除——通过 status 字段标记,配合 @Update 实现:
@Update("UPDATE product SET status = 0 WHERE id = #{id}")
int offShelf(Integer id);
逻辑删除的好处:数据可恢复、保留审计轨迹、避免外键级联问题。
4.4 修改
@Update("UPDATE product SET stock = #{stock}, update_time = NOW() WHERE id = #{id}")
int updateStock(Integer id, Integer stock);
思考:如果只想更新库存,而不更新其他字段(如商品名),传一个完整的 Product 对象就不合适了。这时要么用
@Param传多参数,要么用后续的 XML 动态 SQL(<if>标签)实现按需更新。
4.5 查询 — 字段映射三大方案(面试重点)
来看一个经典问题:查询商品列表,prod_name 字段值为 null。
根因:数据库字段 prod_name(下划线)与 Java 属性 prodName(驼峰)不匹配。MyBatis 默认按名称精确匹配映射列到属性,找不到就赋 null。
方案①:SQL 起别名
@Select("SELECT id, prod_name AS prodName, price, stock, category, status, " +
"create_time AS createTime, update_time AS updateTime " +
"FROM product WHERE status = 1")
List<Product> listOnSale();
优点:直接有效。缺点:每个字段都要写 AS,10 个字段就写 10 个,繁琐易漏。
方案②:@Results + @Result
@Results(id = "productMap", value = {
@Result(property = "id", column = "id"),
@Result(property = "prodName", column = "prod_name"),
@Result(property = "createTime", column = "create_time"),
@Result(property = "updateTime", column = "update_time")
})
@Select("SELECT * FROM product WHERE status = 1")
List<Product> listOnSale();
// 其他方法复用
@ResultMap("productMap")
@Select("SELECT * FROM product WHERE id = #{id}")
Product getById(Integer id);
优点:声明式映射,可复用(@ResultMap)。缺点:每个需要此映射的查询方法都要声明或引用 @ResultMap,接口方法一多仍然冗余。这就是社区最终推荐方案③的原因。
方案③(推荐):驼峰命名自动转换
mybatis:
configuration:
map-underscore-to-camel-case: true
一行配置,全局生效。MyBatis 在映射时会自动将 prod_name 匹配到 prodName,create_time 匹配到 createTime。开发中首选此方案。
三种方案对比:
| 方案 | 复杂度 | 维护性 | 推荐度 |
|---|---|---|---|
| SQL 别名 | 低但繁琐 | 差,每个 SQL 都要写 | 仅临时用 |
| @Results | 中 | 中,可复用但每个方法都要引用 | 需要多表不同映射时 |
| 驼峰配置 | 极低 | 优,一行配置全局生效 | ★★★★★ |
📌 面试点:数据库字段和实体属性名不一致如何解决?—— 三种方案递进说明,最终推荐驼峰配置。加分项:补充说明"如果确实有特殊字段无法用驼峰规则自动匹配(如
deleteFlag→is_deleted),才用@Results单独处理"。
五、XML 配置文件方式 — 复杂 SQL 的主场
注解适合简单 CRUD,但当 SQL 复杂到包含多表联查、动态条件、<foreach> 批量操作时,XML 是更好的选择。
5.1 配置 XML 路径
mybatis:
mapper-locations: classpath:mapper/*.xml
5.2 XML 基本格式
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.OrderMapper">
<select id="getById" resultType="com.example.entity.OrderInfo">
SELECT * FROM order_info WHERE id = #{id}
</select>
</mapper>
XML 三要素:namespace(指向 Mapper 接口全限定名)、id(对应接口方法名)、resultType(返回类型)。
5.3 resultType vs resultMap
resultType:直接指定实体类全限定名,MyBatis 自动按字段名映射。前提是字段名匹配(或已开启驼峰命名自动转换)。resultMap:手动定义列到属性的映射关系。当字段名差异大、多表查询结果需要自定义映射时使用。
<resultMap id="orderDetailMap" type="com.example.entity.OrderInfo">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="custName" column="cust_name"/> <!-- 来自 customer 表 -->
<result property="prodName" column="prod_name"/> <!-- 来自 product 表 -->
<result property="totalPrice" column="total_price"/>
</resultMap>
<select id="getOrderDetail" resultMap="orderDetailMap">
SELECT o.*, c.name AS cust_name, p.prod_name
FROM order_info o
LEFT JOIN customer c ON o.cust_id = c.id
LEFT JOIN product p ON o.prod_id = p.id
WHERE o.id = #{id}
</select>
MyBatis 不区分单表和多表查询——它只看三要素:SQL + 映射关系 + 实体类。只要你能写出正确的 SQL 并配置好映射,单表和多表是一样的操作。
5.4 注解 vs XML 选型原则
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 简单 CRUD(单表、无动态条件) | 注解 | 代码紧凑,SQL 和接口在一起,方便阅读 |
| 多表联查、复杂 SQL | XML | SQL 和 Java 代码分离,可读性强,避免注解中拼接长字符串 |
动态 SQL(<if>、<foreach>、<trim>) |
XML | 注解不支持动态 SQL 标签 |
| 存储过程调用 | XML | 语义更清晰 |
📌 面试点:你们项目用注解还是 XML?为什么?—— 没有标准答案,关键是说出理由:简单查询用注解省事;复杂 SQL 用 XML 便于维护和 DBA 审核。可以补充"我们的项目是混合使用,简单 CRUD 用注解,报表和复杂查询用 XML"。
六、#{} 和 ${} — 面试必问的"送命题"
这是全文最重要的章节。如果你只有时间精读一章,请读这章。
6.1 核心区别
#{} 和 ${} 底层走的完全是两条路:

| 维度 | #{} |
${} |
|---|---|---|
| SQL 生成方式 | 预编译(PreparedStatement),用 ? 占位 |
即时拼接(Statement),字符串直接替换 |
| SQL 注入 | 安全 | 危险 |
| 字符串自动加引号 | 是,#{name} → '张三' |
否,需手动加 ${'name'} → '张三' |
| 编译缓存 | 有(语法树只解析一次) | 无(每次重新解析整条 SQL) |
| 性能 | 高(重复执行时复用编译结果) | 低 |
| 适用场景 | 值参数(推荐默认使用) | 表名、字段名、排序关键字 |
6.2 SQL 注入攻击演示
以电商客户登录场景为例,Mapper 中存在这样的查询:
// ❌ 危险写法
@Select("SELECT * FROM customer WHERE name = '${name}'")
Customer loginDangerous(String name);
正常输入:张三 → SQL:SELECT * FROM customer WHERE name = '张三' ✅
恶意输入:' OR 1=1 -- → SQL:
SELECT * FROM customer WHERE name = '' OR 1=1 --'
-- 是 MySQL 注释符,后面的内容被忽略。1=1 恒为真,OR 逻辑导致查询返回所有用户。如果程序取第一条作为登录成功,攻击者直接绕过了身份验证。
// ✅ 安全写法
@Select("SELECT * FROM customer WHERE name = #{name}")
Customer loginSafe(String name);
#{name} 会将参数作为值传递,输入 ' OR 1=1 -- 会被当作普通字符串 "' OR 1=1 --'" 来匹配 name 字段,自然查不到任何结果。

6.3 PreparedStatement 底层防御原理
很多人只知道"#{} 防注入",但不知道为什么。面试追问时,能说出原理是明显加分项:
- 预编译阶段:MySQL 服务端收到
SELECT * FROM customer WHERE name = ?,解析并缓存语法树(AST)。此时?被标记为参数占位符,不是 SQL 关键字。 - 执行阶段:参数
' OR 1=1 --通过独立的协议通道发送,被当作纯字符串值,直接填充到已解析的语法树中。由于语法树已经固定,参数不可能改变 SQL 结构。
用通俗类比理解:预编译 SQL 像一份填空题试卷(空格已固定),参数像你填的答案。无论你在空格里写什么,都不可能改变试卷上其他题目的内容。而即时 SQL 像让你在试卷上随便写——你可以划掉原来的题目,自己出题。
6.4 ${} 的必要使用场景
既然 ${} 有注入风险,为什么 MyBatis 还要保留它?因为有些场景 #{} 无法工作——它会对参数自动加引号。
场景①:动态排序
// ❌ 错误——执行后 SQL: ORDER BY price 'asc'(语法错误)
@Select("SELECT * FROM product WHERE status = 1 ORDER BY price #{sort}")
List<Product> listSorted(String sort);
// ✅ 正确
@Select("SELECT * FROM product WHERE status = 1 ORDER BY price ${sort}")
List<Product> listSorted(String sort);
#{} 会将 "asc" 变成 'asc',ORDER BY price 'asc' 是非法 SQL。排序关键字、表名、字段名不需要引号,必须用 ${}。
⚠️ 安全措施:前端传入的排序参数必须在后端做白名单校验,只允许
"ASC"和"DESC",否则${}仍然是注入入口。
场景②:动态表名
@Select("SELECT * FROM ${tableName} WHERE status = 1")
List<Product> listByTable(String tableName);
表名同样不能加引号。处理方式同样是白名单校验。
6.5 LIKE 查询的安全写法
模糊搜索商品名称是一个常见需求。三种写法对比:
// ❌ 方案A:报错——#{} 被当作字符串的一部分不是占位符
@Select("SELECT * FROM product WHERE prod_name LIKE '%#{keyword}%'")
// ⚠️ 方案B:功能正常但危险——${} 存在注入风险
@Select("SELECT * FROM product WHERE prod_name LIKE '%${keyword}%'")
// ✅ 方案C:推荐——用 MySQL 的 CONCAT 函数安全拼接
@Select("SELECT * FROM product WHERE prod_name LIKE CONCAT('%', #{keyword}, '%')")
List<Product> searchByName(String keyword);
CONCAT 函数在 MySQL 服务端执行字符串拼接,#{keyword} 仍然走预编译占位符,既实现了模糊查询,又不会引入 SQL 注入风险。
📌 面试点总结:
- 必问:
#{}和${}有什么区别?—— 预编译 vs 即时拼接,安全性、性能、引号处理。- 追问:既然
${}不安全,为什么还保留?—— 排序/表名/字段名必须用${},但需要白名单校验。- 实战:LIKE 查询怎么写?——
CONCAT('%', #{keyword}, '%')。
七、数据库连接池 — 性能的幕后英雄
7.1 为什么需要连接池
数据库连接(Connection)的创建和销毁代价高昂:
- TCP 三次握手建立网络连接
- 数据库服务端分配线程和内存
- 认证握手(用户名密码验证)
如果每次请求都新建一个 Connection,高并发下数据库很快会因为连接耗尽而拒绝服务。更严重的是,频繁的创建/销毁会增加大量不必要的网络开销和 CPU 消耗。
连接池的核心思想:启动时预创建一批 Connection 放入池中,请求来了直接取用,用完后归还而非销毁。
7.2 HikariCP — Spring Boot 默认之选
HikariCP 是 Spring Boot 2.x 起的默认连接池,设计哲学是"极致性能"。性能优势来自几个关键优化:
- 字节码级精简:相比其他连接池,HikariCP 的代码量极少,大量使用 JIT 友好的编码模式,减少方法调用层级。
- 无锁设计:使用
ConcurrentBag替代传统BlockingQueue,减少线程间的锁竞争。 - 连接代理轻量化:生成的
ProxyConnection对象极轻,GC 压力小。
不需要额外依赖,Spring Boot 自动配置即用。
7.3 Druid — 阿里的"瑞士军刀"
Druid 不只是连接池,更像一个数据库中间件全家桶:
| 功能 | 说明 |
|---|---|
| SQL 监控 | 统计每条 SQL 的执行次数、耗时、并发、返回行数 |
| WallFilter 防火墙 | 基于白名单/黑名单拦截 SQL 注入,比 #{} 多了一层应用级防护 |
| 可视化面板 | 内置 Web 监控页面,实时查看 SQL 执行情况和连接池状态 |
| 日志 | 支持慢 SQL 记录、执行异常日志 |
| 密码加密 | 数据库密码在配置文件中可加密存储 |
引入 Druid(Spring Boot 3.x):
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.21</version>
</dependency>
spring:
datasource:
druid:
url: jdbc:mysql://127.0.0.1:3306/ecommerce_db
username: root
password: your_password
stat-view-servlet:
enabled: true # 开启监控页面
url-pattern: /druid/* # 访问路径
访问 http://localhost:8080/druid/ 即可看到监控面板。
7.4 对比与选型
| 维度 | HikariCP | Druid |
|---|---|---|
| 性能 | ★★★★★ 极致 | ★★★★ 优秀 |
| 监控能力 | 无内置 | ★★★★★ SQL 统计 + 可视化面板 |
| SQL 防火墙 | 无 | ★★★★ WallFilter |
| 配置复杂度 | 几乎零配置 | 中等,功能多配置项也多 |
| 社区生态 | 全球社区 | 国内生态,中文文档丰富 |
| 适用场景 | 追求极致性能的中小型项目 | 需要 SQL 监控、运维可视化的企业级项目 |
📌 面试点:
- Spring Boot 默认连接池是什么?—— HikariCP(2.x 起取代 Tomcat JDBC Pool)。
- 为什么从 Tomcat 换成 Hikari?—— Hikari 性能更优,代码更精简,Spring 官方认定为最佳选择。
- Druid 的 WallFilter 原理?—— 拦截 SQL 执行前,基于规则引擎(白名单/黑名单)判断 SQL 是否安全,相当于应用的"WAF"。
八、MyBatis 缓存机制 — 被忽略的面试杀器
缓存是 MyBatis 面试中被询问比例仅次于
#{}${}的考点,但很多候选人答不好。这一章让你建立完整认知。

8.1 一级缓存(SqlSession 级别)
默认开启,无需配置。
作用域:同一个 SqlSession 内。当你在同一个 SqlSession 中执行两次相同的查询,第二次不会发送 SQL 到数据库,而是直接从缓存返回。
// 同一个 SqlSession
Product p1 = productMapper.getById(1); // 发送 SQL
Product p2 = productMapper.getById(1); // 不发送 SQL,从缓存取
System.out.println(p1 == p2); // true,同一对象引用
一级缓存失效的四种场景(面试常问):
| 序号 | 场景 | 原因 |
|---|---|---|
| 1 | 不同的 SqlSession | 一级缓存是 SqlSession 级别的,换个 SqlSession 就没缓存了 |
| 2 | 同一个 SqlSession 但查询条件不同 | 缓存的 key 是 statementId + SQL + 参数,条件变了 key 就变了 |
| 3 | 两次查询之间执行了增删改操作 | 增删改会清空一级缓存(因为数据可能已被修改,缓存不保证一致性) |
| 4 | 手动清空缓存 sqlSession.clearCache() |
显式清除 |
8.2 二级缓存(Mapper 级别 / namespace 级别)
默认关闭,需要手动配置。
作用域:同一个 namespace(即同一个 Mapper)内的所有 SqlSession 共享。
开启步骤:
Step 1:application.yml 全局开关
mybatis:
configuration:
cache-enabled: true
Step 2:在 XML Mapper 文件中加 <cache/> 标签
<mapper namespace="com.example.mapper.ProductMapper">
<cache/>
<!-- ... -->
</mapper>
Step 3:实体类实现 Serializable 接口
@Data
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
二级缓存的工作流程:
SqlSession1 查询 → 查 DB → 结果放入二级缓存
↓
SqlSession1 关闭(此时一级缓存数据刷入二级缓存)
↓
SqlSession2 查询相同 SQL → 命中二级缓存 → 直接返回
8.3 一、二级缓存对比
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | SqlSession | Mapper(namespace) |
| 默认状态 | 开启 | 关闭 |
| 生命周期 | 随 SqlSession 创建/销毁 | 整个应用生命周期(或过期策略控制) |
| 是否跨 SqlSession | 否 | 是 |
| 清空时机 | 增删改 / close / clearCache | 增删改 / 过期策略 |
| 序列化要求 | 无 | 实体类需实现 Serializable |
8.4 使用建议与注意事项
- 一级缓存:简单可靠,无需额外配置,放心用。
- 二级缓存:谨慎使用。以下场景不适合开启:
- 多表关联查询(A 表数据变了,B 表相关的缓存不会自动失效)
- 数据频繁修改的表(缓存命中率低,反而增加序列化/反序列化开销)
- 分布式部署(二级缓存是 JVM 本地缓存,多实例数据不一致)
📌 面试点:
- MyBatis 有一级缓存和二级缓存,分别是什么?—— 一级:SqlSession 级别,默认开启;二级:Mapper 级别,需手动配置。
- 一级缓存在什么情况下会失效?—— 四种场景(不同 SqlSession、不同查询条件、中间有增删改、手动 clearCache)。
- 二级缓存的使用风险?—— 多表关联导致脏数据、分布式环境不一致。
总结
本文以电商订单系统的 product、customer、order_info 三张表为案例,完整覆盖了 MyBatis 入门到面试的全部核心知识点。回顾一下八大模块的主线:
| 模块 | 核心收获 | 面试权重 |
|---|---|---|
| JDBC → MyBatis | 理解框架存在的意义:消除模板代码 | ★★ |
| 快速入门 | 能独立搭建 Spring Boot + MyBatis 项目 | ★★ |
| 日志配置 | 掌握调试手段 | ★ |
| 注解式 CRUD | 会用注解完成增删改查 + 字段映射三方案 | ★★★ |
| XML 方式 | 知道什么场景用 XML,能写 resultMap |
★★★ |
#{} vs ${} |
必须精通:预编译原理 + 注入防御 + ${} 场景 |
★★★★★ |
| 连接池 | 理解 Hikari vs Druid 的选型差异 | ★★★ |
| 缓存机制 | 一级/二级缓存的区别和失效场景 | ★★★★ |
一句话总结:MyBatis 的本质是"把 SQL 还给你"——它不帮你生成 SQL,但帮你打理好连接、参数、映射、缓存、事务等所有脏活累活。掌握 MyBatis,就是掌握 Java 后端持久层的基本功。
本文技术栈:Spring Boot 3.1.x + MyBatis 3.0.x + MySQL 8.0 | 案例数据库:ecommerce_db
本文为《MyBatis 从入门到面试》系列的第一篇。进阶篇请访问:【MyBatis】进阶篇:动态 SQL + 关联映射 + 插件原理全通关。系列文章持续更新中,感谢关注。
更多推荐

所有评论(0)