MyBatis:面试专题 - 从基础到源码的高频问题解析

一、基础概念与核心原理

1. 什么是 MyBatis?它与其他 ORM 框架(如 Hibernate)有什么区别?
MyBatis 是一款基于 Java 的半自动化 ORM(对象关系映射)框架,它通过 XML 或注解的方式将 SQL 语句与 Java 方法关联,实现对象与关系数据库的映射。与 Hibernate 等全自动 ORM 框架相比,主要区别在于:

  • SQL 控制粒度:MyBatis 允许开发者直接编写 SQL,灵活控制查询逻辑;Hibernate 通过 HQL 或 Criteria API 生成 SQL,屏蔽了底层 SQL 细节。
  • 学习成本:MyBatis 学习曲线平缓,适合 SQL 优化需求高的场景;Hibernate 初期学习成本高,但能快速开发简单 CRUD 功能。
  • 性能优化:MyBatis 可手动优化 SQL,适合复杂查询场景;Hibernate 在复杂查询时性能优化较困难。
  • 适用场景:MyBatis 适合需求多变、SQL 优化要求高的项目(如电商核心系统);Hibernate 适合快速开发、SQL 相对简单的项目。

面试点睛:重点强调 MyBatis 的 “半自动化” 特性 —— 既保留了 SQL 的灵活性,又提供了 ORM 的便利,这是它在企业级应用中广泛使用的核心原因。

2. MyBatis 的核心组件有哪些?各自的作用是什么?
MyBatis 的核心组件包括:
1、SqlSessionFactory:会话工厂,负责创建 SqlSession,生命周期为应用级别(单例)。通常由 SqlSessionFactoryBuilder 根据配置文件构建。

2、SqlSession:会话对象,代表与数据库的一次交互,包含了执行 SQL 的所有方法。生命周期为一次请求或事务,线程不安全,需及时关闭。

3、Executor:执行器,SqlSession 的底层实现,负责 SQL 的执行和缓存管理。有三种类型:

SimpleExecutor:默认执行器,每次执行 SQL 都会创建新的 Statement
ReuseExecutor:复用 Statement
BatchExecutor:批量执行 SQL

4、MappedStatement:映射语句,封装了 SQL 语句、参数类型、结果类型等信息,是 MyBatis 对 SQL 的抽象表示。

5、StatementHandler:处理 JDBC 的 Statement 操作,负责参数设置、SQL 执行和结果集处理。

6、ResultHandler:结果处理器,用于自定义结果集的处理逻辑。

7、TypeHandler:类型处理器,负责 Java 类型与 JDBC 类型之间的转换。

面试点睛:这些组件的协作流程是面试重点 ——SqlSessionFactory创建SqlSession,SqlSession通过Executor执行MappedStatement,最终由StatementHandler与数据库交互。

3. MyBatis 的工作原理是什么?请简述其执行流程。

MyBatis 的工作流程可分为初始化和执行两个阶段:

初始化阶段:
1、加载 MyBatis 全局配置文件(mybatis-config.xml)和 Mapper 映射文件。
2、通过 SqlSessionFactoryBuilder 解析配置文件,生成 Configuration 对象(包含所有配置信息)。
3、由 Configuration 创建 SqlSessionFactory 实例。

执行阶段:
1、调用 SqlSessionFactory 的 openSession () 方法创建 SqlSession。
2、SqlSession 通过 Mapper 接口的全限定名 + 方法名找到对应的 MappedStatement。
3、Executor 根据 MappedStatement 的配置,通过 StatementHandler 执行 SQL:
ParameterHandler 处理参数绑定
执行 SQL 语句
ResultSetHandler 处理结果集映射
4、事务提交或回滚。
5、关闭 SqlSession。

流程图解:

配置文件 → SqlSessionFactoryBuilderSqlSessionFactorySqlSessionExecutorMappedStatement → 数据库

面试点睛:重点说明 Configuration 的核心作用(存储所有配置)和 MappedStatement 的作用(封装 SQL 信息),体现对框架设计的理解。

二、配置与映射

4. MyBatis 的核心配置文件(mybatis-config.xml)包含哪些主要配置项?
核心配置文件的主要配置项按顺序如下:
properties:加载外部属性文件(如数据库连接信息)。

<properties resource="db.properties"/>
settings:全局配置参数,如缓存开关、延迟加载、日志实现等。
<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>

typeAliases:为 Java 类型设置别名,简化 Mapper 文件中的类型引用。

<typeHandlers>
    <typeHandler handler="com.example.handler.MyTypeHandler"/>
</typeHandlers>

plugins:配置 MyBatis 插件(如分页插件、性能监控插件)。

<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>

environments:配置数据库环境,支持多环境切换。

<environments default="dev">
    <environment id="dev">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <!-- 其他属性 -->
        </dataSource>
    </environment>
</environments>

mappers:注册 Mapper 映射文件或接口。

<mappers>
    <mapper resource="com/example/mapper/UserMapper.xml"/>
    <package name="com.example.mapper"/> <!-- 批量注册 -->
</mappers>

面试点睛:需说明配置项的顺序是固定的(MyBatis 解析时严格按顺序处理),且重点掌握 settings 和 mappers 的配置。

5. 如何理解 MyBatis 中的 Mapper 接口?它为什么不需要实现类?
MyBatis 的 Mapper 接口(也称映射器接口)是 SQL 操作的抽象定义,它与 Mapper.xml 文件或注解中的 SQL 语句绑定,无需手动实现。其底层原理是:

1、动态代理:MyBatis 在运行时通过 JDK 动态代理为 Mapper 接口生成代理对象(MapperProxy)。
2、方法映射:代理对象将接口方法调用转换为对SqlSession方法的调用,通过 “接口全限定名 + 方法名” 匹配对应的 MappedStatement。
3、参数传递:代理对象负责将接口方法的参数转换为 SQL 所需的参数类型。

关键源码片段:

// MapperProxy的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
    }
    // 转换为MapperMethod执行
    MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

面试点睛:核心是动态代理机制,这也是 MyBatis"接口编程" 思想的体现,简化了数据访问层的代码。

6. resultMap 和 resultType 的区别是什么?何时使用 resultMap?
两者都是用于指定 SQL 查询结果的映射方式,主要区别如下:

在这里插入图片描述

resultMap 示例(解决字段名与属性名不一致问题):

<resultMap id="userMap" type="User">
    <id column="user_id" property="id"/> <!-- 主键映射 -->
    <result column="user_name" property="username"/> <!-- 普通字段映射 -->
    <result column="create_time" property="createTime"/> <!-- 日期类型映射 -->
</resultMap>

<select id="selectUser" resultMap="userMap">
    SELECT user_id, user_name, create_time FROM t_user WHERE id = #{id}
</select>

使用建议:
1、简单查询用 resultType,代码更简洁
2、复杂映射(如多表关联、字段名不一致、集合映射)必须用 resultMap

面试点睛:resultMap 是 MyBatis 映射能力的核心,能解决 resultType 无法处理的复杂场景,体现对 MyBatis 高级特性的掌握。

7. 动态 SQL 有哪些标签?请举例说明其用法。
MyBatis 的动态 SQL 用于根据条件动态生成 SQL 语句,核心标签包括:
if:条件判断,满足条件则包含标签内的 SQL。

<select id="selectUser" resultType="User">
    SELECT * FROM user
    WHERE 1=1
    <if test="username != null">AND username LIKE CONCAT('%', #{username}, '%')</if>
    <if test="status != null">AND status = #{status}</if>
</select>

where:替代 WHERE 关键字,自动处理 AND/OR 前缀。

<select id="selectUser" resultType="User">
    SELECT * FROM user
    <where>
        <if test="username != null">AND username LIKE CONCAT('%', #{username}, '%')</if>
        <if test="status != null">AND status = #{status}</if>
    </where>
</select>

choose/when/otherwise:多条件分支判断(类似 switch-case)。

<select id="selectUser" resultType="User">
    SELECT * FROM user
    <where>
        <choose>
            <when test="id != null">AND id = #{id}</when>
            <when test="username != null">AND username = #{username}</when>
            <otherwise>AND status = 1</otherwise>
        </choose>
    </where>
</select>

foreach:遍历集合,常用于 IN 条件或批量操作。

<select id="selectByIds" resultType="User">
    SELECT * FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

set:用于 UPDATE 语句,自动处理逗号。

<update id="updateUser">
    UPDATE user
    <set>
        <if test="username != null">username = #{username},</if>
        <if test="email != null">email = #{email},</if>
    </set>
    WHERE id = #{id}
</update>

trim:自定义字符串截取规则,可替代 where/set。

<trim prefix="WHERE" prefixOverrides="AND|OR">
    <!-- 内容 -->
</trim>

面试点睛:动态 SQL 是 MyBatis 的强大特性,能避免手动拼接 SQL 的风险,需重点掌握 if、where、foreach 的用法及场景。

三、SQL 执行与事务

8. MyBatis 的 SqlSession 有哪些常用方法?它是线程安全的吗?
SqlSession 是 MyBatis 与数据库交互的核心接口,常用方法包括:

查询方法:

<T> T selectOne(String statement, Object parameter):查询单个结果
List<T> selectList(String statement, Object parameter):查询集合
Map<K, V> selectMap(String statement, Object parameter, String mapKey):查询结果映射为 Map

插入方法:

int insert(String statement, Object parameter):执行插入,返回影响行数

更新方法:

int update(String statement, Object parameter):执行更新,返回影响行数

删除方法:

int delete(String statement, Object parameter):执行删除,返回影响行数

事务方法:

void commit():提交事务
void rollback():回滚事务

获取 Mapper:

<T> T getMapper(Class<T> type):获取 Mapper 接口代理对象

线程安全性:

  • SqlSession不是线程安全的,其设计为一次请求或一个事务的生命周期。原因是:
  • SqlSession 内部持有 Connection 对象,而 Connection 是非线程安全的
  • 多个线程共享 SqlSession 会导致事务管理混乱和数据不一致

最佳实践:

  • 在 Spring 环境中,通过@Autowired注入的 Mapper 接口由 Spring 管理,无需手动处理 SqlSession
  • 非 Spring 环境中,应在方法内部创建 SqlSession,使用后立即关闭(try-with-resources)

面试点睛:线程安全性是高频考点,需明确说明 SqlSession 不可共享,并解释原因。

9. MyBatis 的 Executor 有哪些类型?它们的区别是什么?
Executor 是 MyBatis 的核心执行器,负责 SQL 执行和缓存管理,主要有三种类型:

SimpleExecutor:

默认执行器,每次执行 SQL 都会创建新的 Statement(PreparedStatement/Statement)
执行后关闭 Statement,适用于大多数场景
优点:简单直观;缺点:频繁创建和关闭 Statement,性能略低

ReuseExecutor:

复用 Statement,根据 SQL 语句的 hash 值缓存 Statement
同一 SQL 语句重复执行时,直接复用已创建的 Statement
优点:减少 Statement 创建开销;缺点:缓存 Statement 会占用一定内存

BatchExecutor:

批量执行 SQL,将多个 INSERT/UPDATE/DELETE 操作缓存,统一提交
调用SqlSession.flushStatements()或commit()时执行批量操作
优点:大幅提升批量操作性能;缺点:只适用于批量写入场景

配置方式:
通过全局配置指定默认执行器:

<settings>
    <setting name="defaultExecutorType" value="SIMPLE"/> <!-- 默认为SIMPLE -->
</settings>

或在创建 SqlSession 时指定:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);

适用场景:

  • 普通查询:SimpleExecutor
  • 重复执行相同 SQL:ReuseExecutor
  • 批量插入 / 更新:BatchExecutor

面试点睛:BatchExecutor 的使用场景和原理是重点,体现对性能优化的理解。

10. MyBatis 如何处理事务?它与 Spring 事务如何整合?
MyBatis 自身的事务管理基于 JDBC 的事务机制,核心实现如下:
事务管理类型:

  • JDBC:依赖数据库的事务支持,通过 Connection 的 commit/rollback 实现
  • MANAGED:将事务管理交给容器(如 Spring),MyBatis 不参与事务控制

默认行为:

  • SqlSession 默认开启事务(autoCommit=false)
  • 需手动调用 commit () 提交事务,或 rollback () 回滚事务
  • 关闭 SqlSession 时若未提交,会自动回滚

与 Spring 事务整合:
企业级应用中通常使用 Spring 的声明式事务管理,整合步骤:
配置数据源:使用 Spring 管理的数据源,确保 MyBatis 与 Spring 共享同一 Connection

@Bean
public DataSource dataSource() {
    // 配置数据源(如HikariCP)
}

配置 SqlSessionFactory:使用 Spring 提供的SqlSessionFactoryBean,注入数据源

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    // 其他配置(如Mapper位置)
    return factory.getObject();
}

配置事务管理器:使用DataSourceTransactionManager

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

启用声明式事务:通过@EnableTransactionManagement注解

@Configuration
@EnableTransactionManagement
public class AppConfig { ... }

使用@Transactional注解:在 Service 方法上声明事务属性

@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
    // 业务逻辑
}

整合原理:

  • Spring 通过 AOP 为标注@Transactional的方法创建代理,在方法执行前开启事务,执行后根据是否异常决定提交或回滚,确保 MyBatis 的 SqlSession 与 Spring 事务使用同一 Connection。

面试点睛:重点说明整合的核心是共享数据源和 Connection,以及 Spring 事务管理器如何接管 MyBatis 的事务控制。

四、缓存机制

11. MyBatis 的一级缓存和二级缓存有什么区别?如何配置和使用?
MyBatis 提供两级缓存机制,用于减少数据库访问,提升性能:

在这里插入图片描述

一级缓存示例:

try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    // 第一次查询:从数据库获取
    User user1 = mapper.selectById(1);
    // 第二次查询:命中一级缓存
    User user2 = mapper.selectById(1);
    System.out.println(user1 == user2); // true(同一对象)
}

二级缓存配置与使用:

  1. 全局开启二级缓存:
<settings>
    <setting name="cacheEnabled" value="true"/> <!-- 默认true,可省略 -->
</settings>
  1. 在 Mapper.xml 中配置缓存:
<mapper namespace="com.example.mapper.UserMapper">
    <!-- 开启二级缓存 -->
    <cache 
        eviction="LRU"  <!-- 淘汰策略:LRU(最近最少使用) -->
        flushInterval="60000"  <!-- 自动刷新时间(毫秒) -->
        size="1024"  <!-- 最大缓存对象数 -->
        readOnly="false"/>  <!-- 是否只读 -->

    <!-- 配置SQL使用缓存 -->
    <select id="selectById" resultType="User" useCache="true">
        SELECT * FROM user WHERE id = #{id}
    </select>

    <!-- 写操作刷新缓存 -->
    <update id="updateById" flushCache="true">
        UPDATE user SET username = #{username} WHERE id = #{id}
    </update>
</mapper>
  1. 实体类实现序列化:
public class User implements Serializable { ... }

使用建议:

  • 一级缓存无需手动配置,注意 SqlSession 的生命周期即可
  • 二级缓存适用于查询频繁、更新较少的数据(如字典表)
  • 关联查询多的表不建议使用二级缓存,可能导致数据不一致

面试点睛:需明确两级缓存的作用范围和失效机制,以及二级缓存的适用场景限制。

12. 如何整合 Redis 作为 MyBatis 的二级缓存?
MyBatis 默认的二级缓存基于内存,不适合分布式环境,整合 Redis 作为二级缓存的步骤如下:
引入依赖:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

配置 Redis 缓存:
在src/main/resources下创建redis.properties:

redis.host=localhost
redis.port=6379
redis.timeout=2000
redis.password=
redis.database=0
redis.keyPrefix=mybatis:cache:
redis.expire=3600  # 缓存过期时间(秒)

在 Mapper 中使用 Redis 缓存:

<mapper namespace="com.example.mapper.DictMapper">
    <!-- 指定使用Redis缓存 -->
    <cache type="org.mybatis.caches.redis.RedisCache"/>

    <!-- 查询方法使用缓存 -->
    <select id="selectByType" resultType="Dict" useCache="true">
        SELECT * FROM dict WHERE type = #{type}
    </select>
</mapper>

自定义 Redis 缓存(可选):
如需自定义序列化方式或缓存逻辑,可继承RedisCache:

public class CustomRedisCache extends RedisCache {
    public CustomRedisCache(String id) {
        super(id);
    }

    @Override
    public void putObject(Object key, Object value) {
        // 自定义存入逻辑(如使用JSON序列化)
        super.putObject(key, value);
    }

    @Override
    public Object getObject(Object key) {
        // 自定义获取逻辑
        return super.getObject(key);
    }
}

并在 Mapper 中引用:

<cache type="com.example.cache.CustomRedisCache"/>

整合原理:
MyBatis 的缓存接口Cache是扩展点,RedisCache实现了该接口,将缓存数据存储到 Redis 而非内存,实现分布式环境下的缓存共享。

面试点睛:重点说明 MyBatis 缓存的扩展机制(通过实现 Cache 接口),以及分布式缓存的必要性。

五、高级特性与源码分析

13. MyBatis 的插件机制是什么?如何实现一个自定义插件(如分页插件)?
MyBatis 的插件机制基于拦截器模式,允许在 SQL 执行过程中插入自定义逻辑,可拦截的四大核心对象包括:

  • Executor:执行器(update/query/flushStatements/commit/rollback 等方法)
  • ParameterHandler:参数处理器(getParameterObject/setParameters 方法)
  • ResultSetHandler:结果集处理器(handleResultSets/handleOutputParameters 方法)
  • StatementHandler:语句处理器(prepare/parameterize/batch/update/query 方法)

实现自定义分页插件的步骤:
实现 Interceptor 接口:

@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 {
        // 1. 获取StatementHandler
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();

        // 2. 获取当前SQL信息
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String sql = (String) metaObject.getValue("delegate.boundSql.sql");

        // 3. 判断是否需要分页(假设参数中包含Page对象)
        Object parameter = statementHandler.getParameterHandler().getParameterObject();
        if (parameter instanceof Page) {
            Page<?> page = (Page<?>) parameter;
            // 4. 重写SQL,添加分页条件(以MySQL为例)
            String pageSql = sql + " LIMIT " + page.getOffset() + ", " + page.getPageSize();
            metaObject.setValue("delegate.boundSql.sql", pageSql);
        }

        // 5. 执行原方法
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 生成代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 读取插件配置参数
    }
}

配置插件:

<plugins>
    <plugin interceptor="com.example.plugin.PageInterceptor">
        <!-- 可选配置参数 -->
        <property name="dialect" value="mysql"/>
    </plugin>
</plugins>

使用分页插件:

Page<User> page = new Page<>(1, 10); // 第1页,每页10条
List<User> users = userMapper.selectByPage(page);
long total = page.getTotal(); // 总条数(需额外查询count)

插件执行原理:
MyBatis 通过 JDK 动态代理为被拦截对象生成代理,当调用被拦截方法时,会先执行插件的intercept方法,再通过invocation.proceed()执行原方法。

面试点睛:需说明插件可拦截的对象和方法,以及拦截器的实现要点(@Intercepts 注解、Plugin.wrap 方法)。

14. MyBatis 的延迟加载(懒加载)原理是什么?如何配置?
延迟加载是 MyBatis 的关联查询优化机制,指在查询主对象时不立即加载关联对象,而是在真正使用关联对象时才执行查询,减少不必要的数据库访问。

实现原理:
MyBatis 通过 CGLIB 或 JDK 动态代理为关联对象创建代理对象,当调用关联对象的 getter 方法时,触发代理逻辑,执行关联查询 SQL。

配置方式:
全局开启懒加载:

<settings>
    <setting name="lazyLoadingEnabled" value="true"/> <!-- 开启懒加载 -->
    <setting name="aggressiveLazyLoading" value="false"/> <!-- 关闭积极加载(按需加载) -->
</settings>

在 resultMap 中配置关联查询:

<!-- 订单结果映射 -->
<resultMap id="orderMap" type="Order">
    <id column="id" property="id"/>
    <result column="order_no" property="orderNo"/>
    <!-- 懒加载订单项(一对多) -->
    <collection 
        property="items" 
        ofType="OrderItem"
        select="com.example.mapper.OrderItemMapper.selectByOrderId"
        column="id"
        fetchType="lazy"/> <!-- 显式指定懒加载 -->
</resultMap>
<select id="selectOrder" resultMap="orderMap">
    SELECT id, order_no FROM `order` WHERE id = #{id}
</select>

订单项查询:

<mapper namespace="com.example.mapper.OrderItemMapper">
    <select id="selectByOrderId" resultType="OrderItem">
        SELECT * FROM order_item WHERE order_id = #{orderId}
    </select>
</mapper>

使用效果:

Order order = orderMapper.selectOrder(1L);
// 此时未查询订单项,只查询了订单主表
System.out.println(order.getOrderNo()); // 不触发关联查询
List<OrderItem> items = order.getItems(); 
// 调用getItems()时,触发懒加载,执行selectByOrderId查询
items.forEach(System.out::println);

注意事项:

  • 懒加载仅适用于关联查询(association/collection)
  • 必须在 SqlSession 生命周期内使用懒加载的关联对象,否则会抛出异常
  • 可通过fetchType="eager"为特定关联配置立即加载

面试点睛:核心是动态代理机制,需说明懒加载的触发时机和配置要点,以及与 N+1 查询问题的关系。

15. MyBatis 的 Mapper 代理是如何实现的?请结合源码说明。
MyBatis 的 Mapper 代理通过 JDK 动态代理实现,核心是MapperProxy类,其工作流程如下:
获取 Mapper 接口代理对象:

当调用SqlSession.getMapper(Class<T> type)时,MyBatis 通过MapperProxyFactory创建代理对象。
// MapperRegistry类
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    return mapperProxyFactory.newInstance(sqlSession);
}
// MapperProxyFactory类
public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
}
protected T newInstance(MapperProxy<T> mapperProxy) {
    // 创建JDK动态代理
    return (T) Proxy.newProxyInstance(
        mapperInterface.getClassLoader(),
        new Class[] { mapperInterface },
        mapperProxy
    );
}

代理对象执行方法:
当调用 Mapper 接口方法时,会触发MapperProxy的invoke方法。

// MapperProxy类
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 处理Object类的方法(如toString、hashCode)
    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
    }
    // 处理接口默认方法
    if (method.isDefault()) {
        return invokeDefaultMethod(proxy, method, args);
    }
    // 处理Mapper接口方法
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

方法执行映射:
MapperMethod将接口方法映射为SqlSession的对应操作。

// MapperMethod类
public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case INSERT: {
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
        case UPDATE: {
            // 处理更新
        }
        case DELETE: {
            // 处理删除
        }
        case SELECT:
            if (method.returnsVoid() && method.hasResultHandler()) {
                // 处理带ResultHandler的查询
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args); // 处理集合查询
            } else if (method.returnsMap()) {
                // 处理Map查询
            } else {
                // 处理单个对象查询
            }
            break;
        case FLUSH:
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    // 处理返回结果
    return result;
}

核心原理:
通过 “接口全限定名 + 方法名” 与 Mapper.xml 中 SQL 的 id 进行匹配,将接口方法调用转换为对SqlSession相应方法的调用,实现无侵入式的 SQL 执行。

面试点睛:这是 MyBatis 的核心设计,需结合动态代理和方法映射的过程说明,体现对框架底层的理解。

六、实战问题与性能优化

16. 什么是 N+1 查询问题?如何解决?
N+1 查询问题是 MyBatis 关联查询中常见的性能陷阱,指查询 N 条主表数据后,每条主表数据又触发一次子表查询,导致总共 N+1 次数据库交互。
问题示例:

// 1. 查询所有订单(1次查询)
List<Order> orders = orderMapper.selectAll();
// 2. 遍历订单,查询每个订单的明细(N次查询)
for (Order order : orders) {
    List<OrderItem> items = orderItemMapper.selectByOrderId(order.getId());
    order.setItems(items);
}
// 总计:1 + N 次查询

解决方案:
关联查询一次性加载:
使用association或collection进行 JOIN 查询,一次性获取所有数据。

<resultMap id="orderWithItemsMap" type="Order">
    <id column="id" property="id"/>
    <result column="order_no" property="orderNo"/>
    <collection property="items" ofType="OrderItem">
        <id column="item_id" property="id"/>
        <result column="product_id" property="productId"/>
    </collection>
</resultMap>
<select id="selectOrdersWithItems" resultMap="orderWithItemsMap">
    SELECT 
        o.id, o.order_no,
        oi.id AS item_id, oi.product_id
    FROM `order` o
    LEFT JOIN order_item oi ON o.id = oi.order_id
</select>

延迟加载 + 批量查询:
结合懒加载和foreach实现 “1+1” 次查询。

<!-- 订单映射(懒加载) -->
<resultMap id="orderMap" type="Order">
    <id column="id" property="id"/>
    <collection 
        property="items" 
        select="com.example.mapper.OrderItemMapper.selectByOrderIds"
        column="id" 
        fetchType="lazy"/>
</resultMap>
<!-- 订单项批量查询 -->
<select id="selectByOrderIds" resultType="OrderItem">
    SELECT * FROM order_item WHERE order_id IN
    <foreach collection="list" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

使用 MyBatis-Plus 的关联查询:
利用 MyBatis-Plus 的@TableName和@TableField注解简化关联查询。

面试点睛:需说明 N+1 问题的成因和两种解决方案的适用场景(关联查询适合数据量小的场景,延迟加载 + 批量查询适合数据量大的场景)。

17. MyBatis 有哪些常见的性能优化手段?
MyBatis 的性能优化需从 SQL、缓存、连接池等多方面入手,常见手段包括:

SQL 优化:

  • 避免使用SELECT *,只查询必要字段
  • 优化 JOIN 操作,控制关联表数量(不超过 3 张)
  • 使用索引优化查询条件,避免全表扫描
  • 分页查询限制返回数据量,避免大数据集加载

缓存优化:

  • 合理使用一级缓存(控制 SqlSession 生命周期)
  • 对热点数据启用二级缓存或分布式缓存(如 Redis)
  • 设置合理的缓存过期时间,避免缓存雪崩

连接池优化:

  • 使用性能优异的连接池(如 HikariCP)
  • 合理配置连接池参数(最大连接数、等待时间等)
<dataSource type="POOLED">
    <property name="poolMaximumActiveConnections" value="20"/> <!-- 最大活跃连接 -->
    <property name="poolMaximumIdleConnections" value="10"/> <!-- 最大空闲连接 -->
    <property name="poolMaximumCheckoutTime" value="20000"/> <!-- 最大 checkout 时间 -->
</dataSource>

4. 批量操作优化:

使用 BatchExecutor 执行批量插入 / 更新
避免循环中执行单条 SQL,改用foreach批量操作

<insert id="batchInsert">
    INSERT INTO user (username, email) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.email})
    </foreach>
</insert>

5. 其他优化:

  • 启用延迟加载,减少不必要的关联查询
  • 使用resultMap代替resultType,避免字段映射开销
  • 合理使用fetchSize控制 JDBC 的每次抓取行数
  • 监控慢查询,使用EXPLAIN分析并优化

面试点睛:性能优化是企业级应用的重点,需结合具体场景说明优化手段,体现实战经验。

18. MyBatis 如何处理存储过程?请举例说明。
MyBatis 支持调用数据库存储过程,通过statementType="CALLABLE"配置,并使用#{parameterMode=IN/OUT/INOUT}指定参数模式。
示例(MySQL 存储过程):
创建存储过程:

-- 根据用户ID查询用户信息,并返回总记录数
DELIMITER //
CREATE PROCEDURE select_user(
    IN userId INT,
    OUT total INT
)
BEGIN
    SELECT COUNT(1) INTO total FROM user;
    SELECT * FROM user WHERE id = userId;
END //
DELIMITER ;

Mapper.xml 配置:

<select id="callSelectUser" statementType="CALLABLE" resultType="User">
    {
        call select_user(
            #{userId, mode=IN, jdbcType=INTEGER},
            #{total, mode=OUT, jdbcType=INTEGER}
        )
    }
</select>

Mapper 接口:

public interface UserMapper {
    User callSelectUser(Map<String, Object> params);
}

调用存储过程:

Map<String, Object> params = new HashMap<>();
params.put("userId", 1);
// 执行存储过程
User user = userMapper.callSelectUser(params);
// 获取OUT参数
Integer total = (Integer) params.get("total");

处理结果集的存储过程:
对于返回多个结果集的存储过程,需使用resultSets属性指定结果集映射:

<select id="callMultiResultProc" statementType="CALLABLE" 
        resultSets="users,orders" 
        resultMap="userMap,orderMap">
    {call get_user_orders(#{userId, mode=IN})}
</select>

七、总结与面试建议

MyBatis 作为企业级应用的主流 ORM 框架,其面试考察点涵盖基础概念、核心原理、配置使用、性能优化和源码理解等多个层面。准备面试时,建议:
夯实基础:熟练掌握核心组件、配置文件、映射规则和动态 SQL。
理解原理:深入理解 Mapper 代理、SQL 执行流程、缓存机制等底层实现。
注重实战:掌握事务管理、批量操作、存储过程调用等实际应用场景。
关注性能:能分析和解决 N+1 查询、慢查询等性能问题。
阅读源码:重点理解 SqlSession、Executor、MapperProxy 等核心类的实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小马不敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值