Skip to content

十、Mybatis(8 题) ​

10.1 ★★★★Mybatis 的核心执行流程是怎样的?从 SqlSession 到执行 SQL 的完整流程?各组件的职责是什么? ​

📖 点击查看答案

核心组件:

  • SqlSessionFactory:创建 SqlSession 的工厂,重量级对象,全局单例。
  • SqlSession:会话对象,封装 CRUD API,线程不安全,方法级生命周期。
  • Executor:执行器,负责 SQL 执行与缓存管理,有 SIMPLE、REUSE、BATCH 三种类型。
  • StatementHandler:处理 JDBC Statement 的创建与参数设置。
  • ParameterHandler:设置 SQL 参数(占位符赋值)。
  • ResultSetHandler:处理结果集映射为 Java 对象。
  • TypeHandler:Java 类型与 JDBC 类型转换。

完整执行流程:

  1. 调用 SqlSession.selectOne("namespace.id", param)。
  2. Configuration 根据 statement id 获取 MappedStatement(封装 SQL、参数映射、结果映射)。
  3. SqlSession 委托给 Executor 执行。
  4. Executor 先查一级缓存 → 二级缓存,未命中则继续。
  5. 创建 StatementHandler,通过 ParameterHandler 设置参数。
  6. 执行 JDBC Statement,获取 ResultSet。
  7. ResultSetHandler 根据 <resultMap> 将行数据映射为 Java 对象。
  8. 返回结果,Executor 写入缓存。
java
// 核心流程源码(简化)
public <E> List<E> selectList(String statement, Object parameter) {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
}
// Executor.query → StatementHandler.prepare → ParameterHandler.setParameters → execute → ResultSetHandler.handleResultSets

动态代理机制:Mapper 接口通过 MapperProxyFactory 生成代理对象,方法调用被 MapperProxy.invoke 拦截,转为调用 SqlSession 的对应方法,statementId = 接口全限定名 + 方法名。

关键点:Configuration 全局唯一,缓存所有 XML 解析结果;Executor 是缓存与执行的核心;插件通过拦截 Executor/StatementHandler/ParameterHandler/ResultSetHandler 实现增强。


10.2 ★★★Mybatis 的延迟加载原理是什么?Mapper 接口和 XML 文件是如何绑定的?为什么 DAO 接口没有实现类也能工作? ​

📖 点击查看答案

延迟加载原理:

  • 配置 lazyLoadingEnabled=true 后,关联对象(association/collection)在真正访问时才执行 SQL。
  • 底层通过 CGLIB 动态代理 生成代理对象,拦截 toString、hashCode、equals 等方法及属性 getter。
  • 主对象加载时不加载关联属性,调用 getDetail() 时触发拦截器 → 调用 loadObject() → 执行关联 SQL。
  • aggressiveLazyLoading=false(3.4+ 默认)时按需加载,否则任一方法触发全部加载。
xml
<!-- 配置 -->
<settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

<!-- resultMap 中 association 配置 fetchType="lazy" -->
<association property="user" javaType="User" fetchType="lazy" select="selectUserById" column="user_id"/>

Mapper 接口与 XML 绑定:

  • XML 中 namespace = Mapper 接口全限定名。
  • <select id="xxx"> 的 id 对应接口方法名。
  • 解析 XML 时将 MappedStatement 以 namespace.id 为 key 存入 Configuration。

无实现类原理(JDK 动态代理):

  1. MapperRegistry 注册 Mapper 接口。
  2. 调用 getMapper(UserMapper.class) 时,MapperProxyFactory.newInstance() 通过 JDK 动态代理生成代理对象。
  3. 代理对象实现 UserMapper 接口,方法调用被 MapperProxy.invoke() 拦截。
  4. invoke 方法根据 method 全限定名拼出 statementId,委托给 SqlSession 执行。
java
// JDK 动态代理核心
public Object invoke(Object proxy, Method method, Object[] args) {
    String statementId = interfaceName + "." + method.getName();
    Class<?> returnType = method.getReturnType();
    if (returnType == List.class) {
        return sqlSession.selectList(statementId, args[0]);
    } else {
        return sqlSession.selectOne(statementId, args[0]);
    }
}

关键点:Mapper 接口方法不能重载(id 唯一);延迟加载需关联查询在同一 SqlSession 内(缓存可用)。


10.3 ★★★★#{} 和 ${} 的区别是什么?为什么 #{} 能防止 SQL 注入?底层是如何实现的? ​

📖 点击查看答案

核心区别:

维度#{}${}
底层PreparedStatement 占位符 ?字符串拼接
SQL 注入防止不能防止
类型处理自动 TypeHandler 转换原样拼接
性能可预编译缓存每次生成新 SQL
使用场景参数值表名、列名、排序字段等结构

#{} 防 SQL 注入原理:

  1. 解析 SQL 时,#{} 被替换为 ?,参数值单独保存为 ParameterMapping。
  2. 执行时使用 PreparedStatement.setXXX(position, value) 设置参数。
  3. JDBC 驱动对参数值做转义处理(如单引号转义),不会改变 SQL 结构。
java
// 源码解析(SqlSourceBuilder)
// 原 SQL: SELECT * FROM user WHERE name = #{name}
// 解析后: SELECT * FROM user WHERE name = ?
// 参数映射: ParameterMapping{property='name', javaType=String}

// 执行时
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE name = ?");
ps.setString(1, "张三' OR '1'='1");  // 整体作为字符串值,不会拼接进 SQL

${} 字符串拼接示例:

java
// 原 SQL: SELECT * FROM user ORDER BY ${column}
// 解析后: SELECT * FROM user ORDER BY id  (column=id 时)
// 风险:若 column = "id; DROP TABLE user",会执行恶意 SQL

使用场景对比:

xml
<!-- #{} 用于值 -->
<select id="selectByName">
    SELECT * FROM user WHERE name = #{name}
</select>

<!-- ${} 用于结构(表名/列名/排序),需做白名单校验 -->
<select id="selectByOrder">
    SELECT * FROM user ORDER BY ${column}  <!-- 必须业务层校验 -->
</select>

关键点:#{} 本质是 PreparedStatement 机制;${} 是字符串替换,安全性需业务层保证;动态表名/排序字段用 ${},但必须校验白名单。


10.4 ★★★★Mybatis 的动态 SQL 有哪些标签?底层是如何拼接 SQL 的?使用场景有哪些? ​

📖 点击查看答案

动态 SQL 标签:

标签作用场景
<if>条件判断可选查询条件
<where>自动处理 AND/OR 前缀多条件 WHERE 拼接
<choose>/<when>/<otherwise>if-else 分支互斥条件
<trim>自定义前后缀与删除灵活拼接
<set>自动处理 UPDATE 末尾逗号动态更新
<foreach>遍历集合IN 查询、批量插入
<bind>定义 OGNL 变量模糊查询拼接

底层拼接原理:

  1. 解析 XML 时,动态 SQL 被转换为 SqlNode 树(如 MixedSqlNode、IfSqlNode、ForEachSqlNode)。
  2. 执行时调用 SqlNode.apply(DynamicContext),根据参数动态生成 SQL 片段,写入 DynamicContext。
  3. DynamicContextSource 将拼接后的 SQL 与参数传给 SqlSourceBuilder,将 #{} 替换为 ?,最终生成 BoundSql。
xml
<!-- 典型示例 -->
<select id="selectUsers" resultType="User">
    SELECT * FROM user
    <where>
        <if test="name != null">
            AND name = #{name}
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
        <choose>
            <when test="sort == 'name'">
                ORDER BY name
            </when>
            <otherwise>
                ORDER BY id
            </otherwise>
        </choose>
    </where>
</select>

<!-- foreach 批量插入 -->
<insert id="batchInsert">
    INSERT INTO user (name, age) VALUES
    <foreach collection="list" item="u" separator=",">
        (#{u.name}, #{u.age})
    </foreach>
</insert>

SqlNode 树结构示例:

java
// <where><if>...</if></where> 解析为
MixedSqlNode
├── StaticTextSqlNode("SELECT * FROM user")
├── WhereSqlNode
│   └── IfSqlNode(test="name!=null", contents=[StaticTextSqlNode("AND name = #{name}")])

使用场景:

  • 多条件查询:用户可选填筛选条件。
  • 批量操作:<foreach> 拼接 IN/VALUES。
  • 动态更新:只更新非空字段。
  • 多数据库兼容:通过 <databaseId> 切换。

注意:<if> 的 test 用 OGNL 表达式;<foreach> 的 index 是下标(List)或 key(Map);动态 SQL 频繁变化会导致预编译缓存失效。


10.5 ★★★★Mybatis 的缓存机制是怎样的?一级缓存和二级缓存的区别、作用域和失效场景是什么? ​

📖 点击查看答案

一级缓存(Local Cache):

  • 作用域:SqlSession 级别,默认开启,无法关闭(可设 localCacheScope=STATEMENT 关闭)。
  • 数据结构:PerpetualCache(HashMap),key = statementId + params + rowBounds + sql。
  • 存放位置:BaseExecutor.localCache。

二级缓存(namespace 级别):

  • 作用域:Mapper 的 namespace,跨 SqlSession 共享。
  • 默认关闭,需在 XML 中 <cache/> 开启,实体类需 Serializable。
  • 查询顺序:二级缓存 → 一级缓存 → 数据库。

对比:

维度一级缓存二级缓存
作用域SqlSessionnamespace(跨 SqlSession)
默认开启关闭
失效条件insert/update/delete、clearCache()、关闭会话整个 namespace 的增删改
实现Executor 内 HashMapDecorator 模式(LRU/FIFO/Soft/Weak)
并发单线程使用跨会话,需 Serializable

失效场景:

  1. 一级缓存:执行 update/insert/delete(同 SqlSession 内清空)、调用 sqlSession.clearCache()、SqlSession 关闭。
  2. 二级缓存:namespace 内任意 insert/update/delete 后清空整个 namespace 缓存。
xml
<!-- 开启二级缓存 -->
<mapper namespace="com.xxx.UserMapper">
    <cache eviction="LRU" flushInterval="60000" size="1024" readOnly="false"/>
    <!-- eviction: LRU/FIFO/SOFT/WEAK -->
</select>
</mapper>

多表查询缓存问题:

  • 二级缓存基于 namespace,若 A Mapper 查询关联 B 表,B 表更新不会清空 A 的缓存 → 脏读。
  • 解决:使用 <cache-ref namespace="..."/> 共享缓存,或禁用多表查询的二级缓存。

分布式缓存集成:通过实现 Cache 接口,可对接 Redis/Memcached:

java
public class RedisCache implements Cache {
    public String getId() { ... }
    public void putObject(Object key, Object value) { redis.set(key, value); }
    public Object getObject(Object key) { return redis.get(key); }
}

关键点:Spring 整合下 SqlSession 由 SqlSessionTemplate 管理,未开启事务时每次方法调用创建新 SqlSession → 一级缓存失效;开启事务时同事务内复用 SqlSession,一级缓存生效。


10.6 ★★★★Mybatis 的插件(Interceptor)机制原理是什么?可以拦截哪些对象的方法?如何实现一个自定义的分页插件? ​

📖 点击查看答案

插件原理:

  • 基于 JDK 动态代理 + 责任链模式,在创建 Executor/StatementHandler/ParameterHandler/ResultSetHandler 时通过 InterceptorChain.pluginAll() 包装代理。
  • 每个插件层层包裹目标对象,调用时依次执行各插件的 intercept 方法。

可拦截的四大对象:

对象拦截方法典型场景
Executorupdate, query, commit, rollback缓存、事务、慢 SQL
StatementHandlerprepare, parameterize, batch改写 SQL(分页)
ParameterHandlersetParameters参数加密
ResultSetHandlerhandleResultSets结果脱敏

实现分页插件示例:

java
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        // 通过反射获取原始 SQL
        MetaObject metaObject = SystemMetaObject.forObject(handler);
        String originalSql = (String) metaObject.getValue("delegate.boundSql.sql");
        // 从 ThreadLocal 获取分页参数
        Page page = PageContext.get();
        if (page != null) {
            // 改写 SQL
            String pageSql = originalSql + " LIMIT " + page.getOffset() + ", " + page.getPageSize();
            metaObject.setValue("delegate.boundSql.sql", pageSql);
            // 查询总数(可选)
        }
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);  // 仅拦截目标类型
    }
    @Override
    public void setProperties(Properties properties) { }
}
xml
<!-- 注册插件 -->
<plugins>
    <plugin interceptor="com.xxx.PageInterceptor">
        <property name="dialect" value="mysql"/>
    </plugin>
</plugins>

关键点:

  • @Intercepts + @Signature 指定拦截的方法签名。
  • Plugin.wrap 用动态代理包装目标对象,仅拦截匹配方法。
  • 通过 MetaObject 反射读写目标对象私有属性(如 boundSql.sql)。
  • 多插件按注册顺序反向执行(洋葱模型)。

注意:插件过多影响性能(多层代理);分页插件要考虑不同数据库方言;PageHelper 是成熟的开源实现,优先使用。


10.7 ★★★Mybatis 是如何进行分页的?RowBounds 分页和插件分页的区别?分页插件 PageHelper 的原理是什么? ​

📖 点击查看答案

分页方式:

  1. SQL 分页(推荐):在 SQL 中写 LIMIT offset, size,性能最好。
  2. RowBounds 分页(逻辑分页):Mybatis 内置,通过 ResultSet 跳过前 N 条记录。
  3. 插件分页(物理分页):通过 Interceptor 改写 SQL,加 LIMIT 子句。

RowBounds vs 插件分页:

维度RowBounds插件分页
类型逻辑分页物理分页
实现ResultSet.absolute(offset) 跳过SQL 加 LIMIT
数据量全量查询后跳过只查需要的
性能差(大结果集慢)好
适用小数据量生产环境
java
// RowBounds(不推荐)
List<User> users = sqlSession.selectList("selectAll", null, new RowBounds(10, 20));
// 实际执行:SELECT * FROM user(全量),ResultSet 跳过前 10 条

PageHelper 原理:

  1. 调用 PageHelper.startPage(pageNum, pageSize) → 将 Page 对象存入 ThreadLocal。
  2. 紧接的下一次查询被拦截器拦截(PageInterceptor)。
  3. 拦截器从 ThreadLocal 取 Page,改写 SQL:
    • MySQL:SELECT * FROM user LIMIT ?, ?
    • Oracle:使用 ROWNUM 子查询
  4. 额外执行 COUNT 查询获取总数,封装为 PageInfo。
  5. 查询完成后清理 ThreadLocal。
java
// PageHelper 使用
PageHelper.startPage(2, 10);  // 第 2 页,每页 10 条
List<User> list = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(list);
// pageInfo.getList() → 当前页数据
// pageInfo.getTotal() → 总记录数
java
// PageInterceptor 核心逻辑
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class PageInterceptor implements Interceptor {
    public Object intercept(Invocation invocation) throws Throwable {
        Page page = PageHelper.getLocalPage();
        if (page != null) {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            BoundSql boundSql = ms.getBoundSql(args[1]);
            String sql = boundSql.getSql();
            // 改写 SQL
            String pageSql = sql + " LIMIT " + page.getStartRow() + ", " + page.getPageSize();
            // 通过反射更新 boundSql.sql
            ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
        }
        return invocation.proceed();
    }
}

关键点:startPage 必须紧邻查询,否则 ThreadLocal 会被下次调用污染;COUNT 查询会解析原 SQL 去除 ORDER BY 优化;多数据源时需指定 dialect。


10.8 ★★★★Mybatis-Plus 相比原生 Mybatis 有哪些核心增强功能?代码生成器和通用 CRUD 的底层原理是什么? ​

📖 点击查看答案

核心增强功能:

  1. 通用 CRUD(BaseMapper):无需 XML 即可实现 insert/updateById/deleteById/selectById/selectList 等。
  2. 条件构造器(Wrapper):链式 API 构建查询条件,如 QueryWrapper.eq("name", "张三").ge("age", 18)。
  3. 代码生成器:根据数据库表自动生成 Entity/Mapper/Service/Controller。
  4. 分页插件:内置 PaginationInnerInterceptor,无需额外配置。
  5. 主键策略:@TableId(type = IdType.AUTO/ASSIGN_ID/ASSIGN_UUID)。
  6. 逻辑删除:@TableLogic 字段,删除转为 update。
  7. 自动填充:@TableField(fill = FieldFill.INSERT/UPDATE) + MetaObjectHandler。
  8. 乐观锁:@Version 字段 + OptimisticLockerInnerInterceptor。
  9. 多租户:TenantLineInnerInterceptor 自动拼接租户条件。

通用 CRUD 原理:

  • 启动时 MP 扫描 @TableName 注解的实体类,注册到 MybatisMapperRegistry。
  • 为每个 BaseMapper 方法注入 MappedStatement(SQL 模板预定义)。
  • SQL 通过 SqlInjector 动态注入到 Configuration 中,与 XML 解析的 statement 共存。
  • 实体类通过反射获取表名(@TableName 或类名驼峰转下划线)、字段名(@TableField)。
java
// BaseMapper 方法注入核心
public class DefaultSqlInjector extends AbstractSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        return Stream.of(
            new Insert(),
            new DeleteById(),
            new UpdateById(),
            new SelectById(),
            new SelectList()
        ).collect(Collectors.toList());
    }
}
// 每个 AbstractMethod 调用 inject() 生成 MappedStatement

代码生成器原理:

  1. 通过 JDBC DatabaseMetaData 获取表结构与字段信息。
  2. 模板引擎(Velocity/Freemarker)渲染 Entity/Mapper/Service/Controller。
  3. 命名策略:表名 → 类名(下划线转驼峰),字段名 → 属性名。
java
// 代码生成器配置
FastAutoGenerator.create("jdbc:mysql://localhost/test", "root", "root")
    .globalConfig(builder -> builder.author("author").outputDir("src/main/java"))
    .packageConfig(builder -> builder.parent("com.xxx").moduleName("user"))
    .strategyConfig(builder -> builder.addInclude("user").addTablePrefix("t_"))
    .execute();

Wrapper 条件构造器原理:

  • QueryWrapper 内部维护 List<ISqlSegment> 存储条件片段(name = ?、age > ?)。
  • getCustomSqlSegment() 拼接为 ${ew.customSqlSegment},参数通过 paramNameValuePairs 传递。
  • XML 中使用 #{ew.paramNameValuePairs.MPGENVAL1} 引用参数。

关键点:MP 不破坏 Mybatis 原生能力,XML 与 BaseMapper 可混用;BaseMapper 方法本质是预生成的 MappedStatement;自定义 SQL 仍走 XML/注解方式。

基于 4 份主流面试题库整合精选,共 200 题