欢迎光临
我们一直在努力

【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 对比与选型

    维度HikariCPDruid
    性能 ★★★★★ 极致 ★★★★ 优秀
    监控能力 无内置 ★★★★★ 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

    赞(0)
    未经允许不得转载:171主机测评 » 【MyBatis】入门篇
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址