一、Mybatis基础高频面试题(必背)
1. 什么是Mybatis?它的核心作用是什么?与JDBC相比有哪些优势?
核心答案:Mybatis是一款基于Java的持久层框架,它封装了JDBC的底层操作,允许开发者通过XML或注解的方式定义SQL语句,将Java对象与SQL语句进行映射,实现数据的CRUD操作;核心作用是简化持久层开发,解耦Java代码与SQL语句,提升开发效率和代码可维护性;相比JDBC,Mybatis解决了JDBC代码冗余、参数手动设置、结果集手动映射等痛点,具备更强的灵活性和可扩展性。
原理解析:
1. Mybatis的核心定位
Mybatis属于“半ORM框架”(区别于Hibernate的全ORM),它不强制要求Java实体类与数据库表完全映射,允许开发者灵活编写SQL语句,兼顾了SQL的灵活性和ORM的便捷性;核心目标是“简化JDBC开发”,将开发者从繁琐的JDBC代码(加载驱动、创建连接、创建Statement、处理结果集、关闭资源)中解放出来,专注于SQL逻辑和业务逻辑。
2. Mybatis与JDBC的对比(优势详解)
(1)JDBC的痛点(Mybatis的优化点)
-
代码冗余:每次数据库操作都需要重复编写加载驱动、创建连接、关闭资源等代码,冗余度高;
-
参数手动设置:需要手动将Java参数转换为SQL参数,容易出现参数类型不匹配、索引错误等问题;
-
结果集手动映射:需要手动将SQL查询结果集转换为Java实体类,代码繁琐,易出错;
-
SQL与Java代码耦合:SQL语句硬编码在Java代码中,修改SQL需要修改Java代码,可维护性差;
-
无缓存机制:每次查询都需要执行SQL,性能较差;
-
异常处理繁琐:需要手动捕获SQL异常,处理逻辑复杂。
(2)Mybatis的优势
-
简化代码:封装了JDBC的底层操作,通过SqlSession管理数据库连接和操作,开发者无需关注资源的创建和关闭;
-
参数自动映射:支持通过#{ }占位符自动将Java参数映射到SQL语句,避免手动设置参数,支持多种参数类型(基本类型、对象、集合等);
-
结果集自动映射:支持将SQL查询结果自动映射为Java实体类(通过resultType、resultMap),无需手动处理结果集;
-
SQL与Java代码解耦:SQL语句可编写在XML文件或注解中,修改SQL无需修改Java代码,可维护性大幅提升;
-
支持缓存:内置一级缓存(SqlSession级别)和二级缓存(SqlSessionFactory级别),减少数据库查询次数,提升性能;
-
支持动态SQL:通过if、choose、foreach等标签,编写动态SQL语句,适配不同查询条件,灵活性高;
-
易于扩展:支持自定义插件(如分页插件、日志插件),可根据业务需求扩展功能;
-
轻量级:核心包体积小,配置简单,无需依赖过多第三方jar包,易于集成到Spring等框架中。
扩展补充:
-
Mybatis的核心依赖:mybatis.jar(核心包)、数据库驱动jar(如mysql-connector-java),若集成Spring,需额外引入mybatis-spring.jar;
-
半ORM与全ORM的区别:全ORM(如Hibernate)无需编写SQL,通过实体类注解映射数据库表,开发效率高,但灵活性差,难以优化SQL;半ORM(Mybatis)需要编写SQL,灵活性高,便于SQL优化,适合复杂业务场景;
-
避坑点:Mybatis虽然简化了JDBC开发,但仍需开发者熟悉SQL语句,否则无法充分发挥其灵活性;此外,Mybatis不支持数据库无关性,更换数据库时,部分SQL语句可能需要修改。
2. Mybatis的核心组件有哪些?各自的作用是什么?(核心原理必问)
核心答案:Mybatis的核心组件包括「SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession、Mapper接口、Mapper.xml(映射文件)」,此外还有ParameterHandler、ResultSetHandler、StatementHandler、Executor等核心处理器;各组件相互配合,完成从SQL编写、参数映射、SQL执行到结果集映射的全流程,核心流转关系:SqlSessionFactoryBuilder → SqlSessionFactory → SqlSession → Mapper接口 → 数据库操作。
原理解析:
1. 核心组件及作用(面试必背)
(1)SqlSessionFactoryBuilder(构建者)
-
作用:负责构建SqlSessionFactory对象,读取Mybatis的核心配置文件(mybatis-config.xml),将配置信息解析为Configuration对象,然后通过build()方法创建SqlSessionFactory;
-
特点:生命周期短暂,仅在创建SqlSessionFactory时使用,创建完成后即可销毁(通常在项目启动时执行一次);
-
核心方法:build(InputStream inputStream)、build(InputStream inputStream, String environment),其中environment用于指定环境(如开发环境、生产环境)。
(2)SqlSessionFactory(工厂)
-
作用:负责创建SqlSession对象,是Mybatis的核心工厂类,存储了Mybatis的核心配置信息(Configuration),包括映射器、缓存、插件等;
-
特点:生命周期与应用程序一致,是单例对象(项目启动时创建,应用停止时销毁),避免重复创建造成资源浪费;
-
核心方法:openSession()(默认手动提交事务)、openSession(boolean autoCommit)(指定是否自动提交事务)、openSession(Connection connection)(使用指定的数据库连接)。
(3)SqlSession(会话)
-
作用:代表与数据库的一次会话,是Mybatis的核心操作对象,负责执行具体的SQL语句、管理事务、获取Mapper接口实例;
-
特点:生命周期短暂,每次数据库操作都应创建一个新的SqlSession(用完即关闭),避免多线程共享SqlSession导致的线程安全问题;
-
核心方法:selectOne()(查询单个结果)、selectList()(查询多个结果)、insert()(插入)、update()(更新)、delete()(删除)、commit()(提交事务)、rollback()(回滚事务)、getMapper()(获取Mapper接口实例)、close()(关闭会话)。
(4)Mapper接口(映射接口)
-
作用:定义数据库操作的方法(如selectById、insert),无需编写实现类,Mybatis通过动态代理生成接口的实现类,将接口方法与Mapper.xml中的SQL语句关联起来;
-
特点:接口名通常与Mapper.xml文件名一致,接口方法名与Mapper.xml中的SQL标签id一致,参数类型和返回值类型与SQL语句的参数和结果集对应;
-
注意:Mapper接口不能有实现类,不能是抽象类,方法名不能与Object类的方法重名(如toString、equals)。
(5)Mapper.xml(映射文件)
-
作用:编写SQL语句,定义参数映射、结果集映射、动态SQL等,是Mybatis中SQL与Java代码解耦的核心;
-
注意:Mapper.xml的namespace属性必须与对应的Mapper接口全类名一致,否则Mybatis无法关联接口与映射文件。
(6)核心处理器(底层组件,面试扩展)
- Executor:执行器,是Mybatis的核心执行组件,负责SQL语句的执行和缓存的管理,分为三种类型:
-
SimpleExecutor:默认执行器,每次执行SQL都会创建一个Statement,执行完成后关闭,适合简单场景;
-
ReuseExecutor:可复用Statement,将Statement缓存起来,下次执行相同SQL时直接复用,提升性能;
-
BatchExecutor:批量执行器,用于批量插入、更新、删除操作,提升批量处理性能。
-
ParameterHandler:参数处理器,负责将Java参数转换为SQL语句中的参数,处理#{ }占位符,避免SQL注入;
-
ResultSetHandler:结果集处理器,负责将SQL查询结果集转换为Java实体类,实现结果集的自动映射;
-
StatementHandler:语句处理器,负责创建Statement对象,设置SQL语句和参数,执行SQL并获取结果集。
2. 核心组件流转流程(面试必说)
-
项目启动时,SqlSessionFactoryBuilder读取mybatis-config.xml配置文件,解析配置信息(如数据库连接、Mapper映射文件路径),创建SqlSessionFactory对象(单例);
-
当需要执行数据库操作时,通过SqlSessionFactory.openSession()创建SqlSession对象,开启与数据库的会话;
-
通过SqlSession.getMapper()方法,获取Mapper接口的动态代理实例;
-
调用Mapper接口的方法,Mybatis通过动态代理,找到对应的Mapper.xml中的SQL语句;
-
Executor调用ParameterHandler处理参数,将Java参数映射到SQL语句,StatementHandler创建Statement对象,执行SQL;
-
ResultSetHandler将SQL执行结果集映射为Java实体类,返回给调用者;
-
操作完成后,调用SqlSession.commit()(提交事务)或rollback()(回滚事务),然后关闭SqlSession(释放资源)。
扩展补充:
-
动态代理原理:Mybatis通过JDK动态代理生成Mapper接口的实现类,代理类中会调用SqlSession的对应方法,将接口方法与SQL语句关联起来;核心是InvocationHandler接口的invoke()方法,拦截接口方法的调用,执行对应的SQL操作;
-
避坑点:① SqlSession不能多线程共享,否则会导致事务混乱、参数错误等问题;② Mapper.xml的namespace必须与Mapper接口全类名一致,否则会报“BindingException: Invalid bound statement (not found)”异常;③ 接口方法名必须与Mapper.xml中SQL标签的id一致,否则无法匹配SQL语句。
3. Mybatis的核心配置文件(mybatis-config.xml)包含哪些核心配置?各自的作用是什么?
核心答案:Mybatis的核心配置文件(mybatis-config.xml)是Mybatis的全局配置文件,用于配置Mybatis的核心参数,包括「环境配置、数据源配置、Mapper映射配置、缓存配置、插件配置、类型别名配置」等;核心作用是统一管理Mybatis的全局配置,确保Mybatis能够正常运行,配置顺序有严格要求(不能随意打乱)。
原理解析:
1. 核心配置顺序(面试必记,不能打乱)
Mybatis核心配置文件的标签顺序必须遵循以下规则,否则会报配置异常:
properties → settings → typeAliases → typeHandlers → objectFactory → objectWrapperFactory → reflectorFactory → plugins → environments → databaseIdProvider → mappers
2. 核心配置及作用(重点掌握)
(1)properties(属性配置)
-
作用:引入外部属性文件(如db.properties),或定义全局属性,用于动态替换配置中的占位符(${ });
-
常用配置:
<!-- 引入外部属性文件 -->
<properties resource="db.properties"/>
<!-- 定义全局属性 -->
<properties>
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</properties>
- 注意:外部属性文件的优先级高于内部定义的属性,若两者有同名属性,以外部文件为准;占位符{driver})。
(2)settings(全局设置)
-
作用:配置Mybatis的全局行为,如缓存开关、日志实现、驼峰命名映射、懒加载等,是Mybatis最核心的配置之一;
-
常用配置(面试必背):
<settings>
<!-- 开启二级缓存(默认true) -->
<setting name="cacheEnabled" value="true"/>
<!-- 开启驼峰命名自动映射(如数据库字段user_name → Java实体类userName) -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启懒加载(默认false),延迟加载关联对象 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 关闭立即加载(配合懒加载,默认true),只有调用关联对象时才加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 配置日志实现(如SLF4J、LOG4J2),用于打印SQL语句 -->
<setting name="logImpl" value="SLF4J"/>
<!-- 开启SQL语句的自动拼接(默认false),避免手动拼接SQL的麻烦 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 配置默认的执行器(SIMPLE/REUSE/BATCH) -->
<setting name="defaultExecutorType" value="SIMPLE"/>
</settings>
- 重点说明:驼峰命名映射(mapUnderscoreToCamelCase)是开发中常用配置,可避免手动配置resultMap映射数据库字段和Java实体类属性,提升开发效率。
(3)typeAliases(类型别名)
-
作用:为Java实体类、基本类型等定义别名,简化Mapper.xml中参数类型和返回值类型的编写(无需写全类名);
-
两种配置方式:
- 单个别名配置:
<typeAliases>
<typeAlias type="com.example.entity.User" alias="User"/>
</typeAliases>
- 包扫描配置(推荐,批量生成别名):
<typeAliases>
<!-- 扫描com.example.entity包下的所有类,别名默认为类名(首字母可大写可小写) -->
<package name="com.example.entity"/>
</typeAliases>
- 注意:Mybatis内置了常用类型的别名(如int → _int、String → string、List → list),可直接使用。
(4)environments(环境配置)
-
作用:配置数据库环境(如开发环境、测试环境、生产环境),可配置多个环境,通过default属性指定当前使用的环境;
-
核心子标签:
-
environment:单个环境配置,id属性指定环境唯一标识;
-
transactionManager:事务管理器,Mybatis提供两种事务管理器:
-
JDBC:依赖数据库的事务管理,支持手动提交、回滚,适用于简单场景;
-
MANAGED:由容器(如Spring)管理事务,Mybatis不负责事务的提交和回滚,适用于Spring集成场景;
- dataSource:数据源配置,用于配置数据库连接信息,Mybatis支持三种数据源类型:
-
UNPOOLED:非池化数据源,每次请求都创建一个新的数据库连接,效率低,适用于测试环境;
-
POOLED:池化数据源,维护一个数据库连接池,复用连接,效率高,适用于生产环境;
-
JNDI:依赖容器的JNDI数据源,适用于Java EE环境(如Tomcat);
-
常用配置示例:
<environments default="development">
<!-- 开发环境 -->
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<!-- 连接池最大连接数 -->
<property name="poolMaximumActiveConnections" value="20"/>
<!-- 连接池最大空闲连接数 -->
<property name="poolMaximumIdleConnections" value="5"/>
</dataSource>
</environment>
<!-- 生产环境 -->
<environment id="production">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${prod.driver}"/>
<property name="url" value="${prod.url}"/>
<property name="username" value="${prod.username}"/>
<property name="password" value="${prod.password}"/>
</dataSource>
</environment>
</environments>
(5)mappers(Mapper映射配置)
-
作用:配置Mapper接口和Mapper.xml的映射关系,告诉Mybatis去哪里寻找Mapper映射文件;
-
三种配置方式(面试必背):
- 单个Mapper配置(指定Mapper.xml路径):
<mappers>
<mapper resource="com/example/mapper/UserMapper.xml"/>
</mappers>
- 单个Mapper配置(指定Mapper接口全类名,适用于注解方式编写SQL):
<mappers>
<mapper class="com.example.mapper.UserMapper"/>
</mappers>
- 包扫描配置(推荐,批量扫描Mapper接口):
<mappers>
<package name="com.example.mapper"/>
</mappers>
- 注意:使用包扫描配置时,需满足两个条件:① Mapper接口与Mapper.xml文件名一致;② Mapper接口与Mapper.xml在同一包下(或通过配置指定路径)。
(6)plugins(插件配置)
-
作用:配置Mybatis的插件,用于扩展Mybatis的功能(如分页、日志、性能监控);
-
常用插件:Mybatis PageHelper(分页插件)、Mybatis Log(日志插件);
-
配置示例(PageHelper插件):
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 配置分页参数合理化 -->
<property name="reasonable" value="true"/>
<!-- 配置支持的数据库类型 -->
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>
扩展补充:
-
配置优先级:Mybatis的配置遵循“局部配置优于全局配置”,如Mapper.xml中的配置会覆盖mybatis-config.xml中的全局配置;
-
避坑点:① 核心配置标签顺序不能打乱,否则会报“ConfigurationException”异常;② 数据源类型选择POOLED(池化)时,需合理配置连接池参数,避免连接池溢出;③ 包扫描配置时,确保Mapper接口和Mapper.xml的路径、名称一致,否则无法关联。
4. Mybatis的Mapper.xml中,#{}和${}的区别是什么?为什么推荐使用#{}?(高频必问)
核心答案:#{}和{}都是Mybatis中用于接收参数的占位符,核心区别在于「参数解析方式和SQL注入风险」;#{}会将参数解析为预编译语句(PreparedStatement)的参数占位符(?),自动进行参数类型转换和SQL注入防护;{}会将参数直接拼接在SQL语句中,不进行预编译,存在SQL注入风险;因此,开发中优先使用#{},仅在特殊场景(如动态表名、动态排序字段)下使用${},且需做好SQL注入防护。
原理解析:
1. #{}的工作原理(推荐使用)
-
解析过程:Mybatis遇到#{}时,会将其替换为PreparedStatement中的?占位符,然后通过ParameterHandler将Java参数转换为对应的SQL参数,设置到?占位符中,执行预编译SQL;
-
核心特点:
-
预编译:SQL语句在执行前会进行预编译,编译一次后可多次执行,提升性能;
-
类型转换:自动将Java参数转换为对应的SQL类型(如Java的String → SQL的VARCHAR,Java的Integer → SQL的INT),避免类型不匹配问题;
-
防SQL注入:将参数作为普通字符串处理,自动转义特殊字符(如'、"、\),避免恶意参数拼接SQL语句,导致SQL注入;
- 示例:
Mapper接口方法:User selectById(Integer id);
Mapper.xml SQL:
select * from user where id = #{id}
解析后执行的SQL(预编译):select * from user where id = ?,参数id=1时,设置参数为1,最终执行:select * from user where id = 1;
若参数为恶意值(如id='1 or 1=1'),#{}会将其作为字符串处理,执行SQL:select * from user where id = '1 or 1=1',不会出现SQL注入。
2. ${}的工作原理(谨慎使用)
-
解析过程:Mybatis遇到${}时,会直接将参数值拼接在SQL语句中,不进行预编译,不进行参数类型转换,也不转义特殊字符;
-
核心特点:
-
无预编译:每次执行SQL都会重新编译,性能较差;
-
无类型转换:直接拼接参数值,若参数类型不匹配,会导致SQL语法错误;
-
存在SQL注入风险:恶意参数可拼接SQL语句,篡改SQL逻辑,导致数据泄露、删除等安全问题;
- 示例:
Mapper接口方法:List selectByTableName(String tableName);
Mapper.xml SQL:
select * from user where id = #{id}
若参数tableName='user; drop table user;',拼接后执行的SQL:select * from user; drop table user;,会导致user表被删除,造成严重安全问题。
4. ${}的使用场景及安全防护(面试扩展)
(1)适用场景(仅以下场景可使用${})
-
动态表名:如根据不同业务场景,查询不同的表(如user_2024、user_2025);
-
动态排序字段:如根据前端传入的字段,动态排序(order by ${sortField});
-
动态SQL片段拼接:如拼接不同的查询条件(非参数传递)。
(2)${}的SQL注入防护措施
-
参数白名单:限制传入参数的取值范围,如排序字段仅允许传入“id、name、age”等合法字段,若传入非法字段,直接抛出异常;
-
参数校验:对传入的参数进行校验,过滤特殊字符(如;、'、"、or、and等),避免恶意参数拼接;
-
手动转义:对传入的参数进行转义处理,将特殊字符转换为普通字符串,避免SQL注入;
-
避免直接使用用户输入:尽量避免将用户直接输入的内容作为${}的参数,若必须使用,需严格校验。
扩展补充:
-
特殊场景:当参数是字符串类型,且需要拼接在SQL中(如in条件),不能使用${},可使用Mybatis的foreach标签配合#{}实现,示例:
-
避坑点:① 开发中严禁使用{}动态排序时,需校验排序字段的合法性,避免传入“id; drop table user”等恶意参数;③ 当参数为null时,#{}会自动处理为null(如where id = null),而${}会拼接null字符串(如where id = null),可能导致SQL语法错误。
5. Mybatis的动态SQL有哪些标签?各自的作用是什么?(实战重点)
核心答案:Mybatis的动态SQL是指根据不同的参数条件,动态生成不同的SQL语句,核心标签包括「if、choose(when、otherwise)、where、set、foreach、trim、bind」;动态SQL解决了SQL语句拼接的繁琐和易错问题,提升了SQL的灵活性和可维护性,是Mybatis实战中最常用的特性之一。
原理解析:
1. 核心动态SQL标签(面试必背,结合示例)
(1)if标签(最常用)
-
作用:根据参数条件判断,若条件为true,则将标签内的SQL片段拼接到主SQL中;
-
核心属性:test(判断条件,支持OGNL表达式,如test="name != null and name != ''");
-
说明:where 1=1是为了避免当所有if条件都不满足时,SQL语句出现“where and ...”的语法错误;也可使用where标签替代(见下文)。
(2)choose(when、otherwise)标签(分支判断)
-
作用:类似Java中的switch-case语句,多条件分支判断,仅执行第一个满足条件的when标签内的SQL片段,若所有when条件都不满足,则执行otherwise标签内的SQL片段;
-
核心属性:when标签的test(判断条件);
-
说明:choose标签是“互斥”的,仅执行一个分支,而if标签是“并列”的,可执行多个分支。
(3)where标签(简化where条件拼接)
-
作用:自动处理SQL语句中的where关键字,以及多余的and/or关键字,避免SQL语法错误;
-
核心特性:若标签内的SQL片段以and/or开头,where标签会自动剔除该and/or;若标签内无SQL片段,where标签不会生成where关键字;
-
说明:当name和age都有值时,生成的SQL:select * from user where name like '%xxx%' and age = ?;当所有if条件都不满足时,生成的SQL:select * from user(无where关键字),避免语法错误。
(4)set标签(简化更新语句拼接)
-
作用:用于update语句中,自动处理set关键字,以及多余的逗号(,),避免SQL语法错误;
-
核心特性:若标签内的SQL片段以逗号结尾,set标签会自动剔除该逗号;若标签内无SQL片段,会导致SQL语法错误(需避免);
-
示例(动态更新用户信息):
<update id="updateUser">
update user
<set>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="gender != null">
gender = #{gender}
</if>
</set>
where id = #{id}
</update>
- 说明:当name和age有值时,生成的SQL:update user set name = ?, age = ? where id = ?(自动剔除age后的逗号);若仅更新name,生成的SQL:update user set name = ? where id = ?,无语法错误。
(5)foreach标签(循环遍历)
-
作用:用于循环遍历集合(如List、Array、Map),将集合中的元素拼接成SQL片段(如in条件、批量插入);
-
核心属性(面试必背):
-
collection:指定要遍历的集合参数名称;
-
item:遍历集合时,当前元素的别名(如item="id");
-
open:SQL片段的开头字符(如in条件的"(");
-
separator:遍历元素之间的分隔符(如in条件的",");
-
close:SQL片段的结尾字符(如in条件的")");
-
index:遍历List时,index是索引;遍历Map时,index是键;
- 示例(批量插入):
<insert id="batchInsert">
insert into user (name, age, gender) values
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age}, #{user.gender})
</foreach>
</insert>
- 说明:批量插入时,需确保数据库支持批量操作(如MySQL默认支持),同时可配置useGeneratedKeys="true"获取批量插入的主键。
(6)trim标签(自定义拼接处理)
-
作用:自定义SQL片段的拼接规则,可替代where、set标签,更灵活地处理多余的and/or、逗号等;
-
核心属性(面试必背):
-
prefix:SQL片段的前缀(如"where");
-
suffix:SQL片段的后缀;
-
prefixOverrides:需要剔除的前缀字符(如"and|or",剔除开头的and或or);
-
suffixOverrides:需要剔除的后缀字符(如",",剔除结尾的逗号);
- 示例(替代set标签):
<update id="updateUser">
update user
<trim prefix="set" suffixOverrides=",">
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="age != null">
age = #{age},
</if>
</trim>
where id = #{id}
</update>
(7)bind标签(自定义变量)
-
作用:在SQL语句中定义一个变量,用于简化SQL拼接(如模糊查询),避免不同数据库的函数差异(如MySQL的concat、Oracle的||);
-
核心属性:name(变量名)、value(变量值,支持OGNL表达式);
-
说明:bind标签定义的变量likeName,值为'%' + name + '%',无论MySQL还是Oracle,都可使用该变量进行模糊查询,避免数据库函数差异导致的SQL兼容性问题。
2. 动态SQL的执行原理(面试扩展)
Mybatis的动态SQL本质是通过「OGNL表达式」解析参数条件,然后由SqlNode接口的实现类(如IfSqlNode、ChooseSqlNode、ForEachSqlNode)动态拼接SQL片段,最终生成完整的SQL语句;执行流程:
-
Mybatis解析Mapper.xml时,将动态SQL标签解析为对应的SqlNode对象;
-
执行SQL时,根据传入的参数,通过OGNL表达式判断条件是否成立;
-
若条件成立,将对应的SqlNode对象中的SQL片段拼接起来,生成完整的SQL语句;
-
执行生成的SQL语句,返回结果。
扩展补充:
-
动态SQL的注意事项:① 避免在动态SQL中拼接${},防止SQL注入;② 注意条件判断的准确性(如null判断、空字符串判断),避免出现逻辑错误;③ 批量操作(如批量插入、批量更新)时,需控制批量数据量,避免数据库压力过大;
-
避坑点:① 使用foreach标签时,collection属性的名称需与Mapper接口方法的参数名称一致,若参数是List,默认名称为list;若参数是Array,默认名称为array;② 使用set标签时,需确保至少有一个if条件成立,否则会生成“update user set where id = ?”的语法错误;③ 模糊查询时,避免使用拼接(如name like '%${name}%'),优先使用bind标签或concat函数配合#{}。
6. Mybatis的缓存机制是什么?一级缓存和二级缓存的区别是什么?(核心原理必问)
核心答案:Mybatis的缓存机制是为了减少数据库查询次数,提升查询性能,分为「一级缓存(SqlSession级别)」和「二级缓存(SqlSessionFactory级别)」;一级缓存是默认开启的,缓存范围是单个SqlSession,当SqlSession关闭或执行commit/rollback时,一级缓存会被清空;二级缓存需要手动开启,缓存范围是整个SqlSessionFactory,多个SqlSession可共享二级缓存,缓存数据会被序列化存储,适用于查询频繁、修改较少的数据。
原理解析:
1. 缓存的核心作用
Mybatis的缓存本质是将查询结果存储在内存中,当再次执行相同的SQL查询时,无需访问数据库,直接从内存中获取缓存结果,减少数据库IO操作,提升查询性能;缓存的核心是“key-value”存储,key是SQL语句+参数+分页信息+环境信息,value是查询结果集。
2. 一级缓存(SqlSession级别,默认开启)
(1)核心特点
-
缓存范围:单个SqlSession,即同一个SqlSession中,执行相同的SQL查询(SQL语句、参数、分页信息完全一致),会从缓存中获取结果;
-
开启方式:默认开启,无需任何配置;
-
缓存存储:存储在SqlSession的Executor中,本质是一个HashMap,key是CacheKey(由SQL语句、参数、分页、环境等组成),value是查询结果;
-
缓存清空时机(面试必背):
-
执行SqlSession的commit()方法(提交事务):清空一级缓存,避免缓存数据与数据库数据不一致;
-
执行SqlSession的rollback()方法(回滚事务):清空一级缓存;
-
执行SqlSession的close()方法(关闭会话):清空一级缓存,释放资源;
-
执行相同SQL的update/delete操作:清空一级缓存(因为修改操作会改变数据库数据,缓存数据会失效)。
(2)示例(一级缓存效果)
// 1. 获取SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 2. 获取Mapper接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 3. 第一次查询,访问数据库,将结果存入一级缓存
User user1 = userMapper.selectById(1);
// 4. 第二次查询,相同SQL和参数,从一级缓存获取,不访问数据库
User user2 = userMapper.selectById(1);
System.out.println(user1 == user2); // true(同一个对象,从缓存获取)
// 5. 执行update操作,清空一级缓存
userMapper.updateName(1, "张三");
sqlSession.commit();
// 6. 第三次查询,缓存已清空,重新访问数据库
User user3 = userMapper.selectById(1);
System.out.println(user1 == user3); // false(不同对象,从数据库获取)
// 7. 关闭SqlSession,清空一级缓存
sqlSession.close();
3. 二级缓存(SqlSessionFactory级别,手动开启)
(1)核心特点
-
缓存范围:整个SqlSessionFactory,即同一个SqlSessionFactory创建的所有SqlSession,都可以共享二级缓存;无论是否关闭SqlSession,只要SqlSessionFactory未销毁,二级缓存就会一直存在;
-
开启方式(面试必背,三步开启):
-
第一步:在mybatis-config.xml的settings标签中,开启全局二级缓存(默认值为true,可省略,但建议显式配置):;
-
第二步:在需要开启二级缓存的Mapper.xml文件中,添加标签(该Mapper下的所有SQL操作,都会使用二级缓存);
-
第三步:确保缓存的Java实体类实现Serializable接口(因为二级缓存会将数据序列化后存储,避免序列化异常)。
-
缓存存储:存储在SqlSessionFactory的Configuration对象中,本质是一个Cache接口的实现类(默认是PerpetualCache,可自定义缓存实现,如Redis缓存);
-
缓存清空时机(面试必背):
-
执行该Mapper下的insert、update、delete操作时,Mybatis会自动清空该Mapper对应的二级缓存,避免缓存数据与数据库数据不一致;
-
手动调用SqlSession的clearCache()方法,仅清空当前SqlSession的一级缓存,不会影响二级缓存;
-
关闭SqlSessionFactory时,二级缓存会被清空(应用停止时)。
(2)示例(二级缓存效果)
// 1. 获取SqlSessionFactory(单例)
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2. 创建第一个SqlSession
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
// 3. 第一次查询,访问数据库,将结果存入一级缓存和二级缓存
User user1 = userMapper1.selectById(1);
sqlSession1.close(); // 关闭SqlSession,一级缓存清空,但二级缓存保留
// 4. 创建第二个SqlSession(同一个SqlSessionFactory)
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 5. 第二次查询,相同SQL和参数,从二级缓存获取,不访问数据库
User user2 = userMapper2.selectById(1);
System.out.println(user1 == user2); // false(二级缓存存储的是序列化后的对象,每次获取都是新对象) sqlSession2.close();
// 6. 创建第三个SqlSession,执行update操作
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
userMapper3.updateName(1, "李四");
sqlSession3.commit(); // 提交事务,清空该Mapper的二级缓存
sqlSession3.close();
// 7. 创建第四个SqlSession,查询相同SQL
SqlSession sqlSession4 = sqlSessionFactory.openSession();
UserMapper userMapper4 = sqlSession4.getMapper(UserMapper.class);
User user4 = userMapper4.selectById(1); // 二级缓存已清空,重新访问数据库
System.out.println(user1 == user4); // false
sqlSession4.close();
(3)二级缓存的进阶配置(面试扩展)
Mapper.xml中的标签支持自定义配置,常用属性:
<cache eviction="LRU" <!-- 缓存回收策略,默认LRU --> flushInterval="60000" <!-- 缓存刷新间隔(毫秒),60秒自动刷新一次 --> size="1024" <!-- 缓存最大存储数量,默认1024个对象 --> readOnly="false" <!-- 是否只读,false表示可读写(默认),true表示只读 --> />
核心配置说明:
- eviction(缓存回收策略,面试必记):
-
LRU(Least Recently Used):最近最少使用,默认策略,当缓存满时,删除最久未使用的缓存对象;
-
FIFO(First In First Out):先进先出,按缓存对象的存入顺序,删除最早存入的对象;
-
SOFT(软引用):基于JVM的软引用,当JVM内存不足时,才会回收缓存对象;
-
WEAK(弱引用):基于JVM的弱引用,只要JVM进行垃圾回收,就会回收缓存对象。
-
flushInterval:缓存自动刷新间隔,单位毫秒,若不配置,默认不自动刷新,仅在执行增删改操作时手动刷新;
-
size:缓存最大存储数量,需根据实际业务调整,避免缓存过大占用过多内存;
-
readOnly:true表示缓存对象是只读的,返回的是缓存对象的引用,性能高但线程不安全;false表示缓存对象是可读写的,返回的是缓存对象的副本,线程安全但性能略低(默认false)。
5. 缓存的执行顺序(面试必说)
Mybatis查询数据时,会优先从缓存中获取,执行顺序遵循「二级缓存 → 一级缓存 → 数据库」,具体流程:
-
当调用Mapper接口方法查询数据时,Mybatis首先检查二级缓存:若缓存存在且有效,直接从二级缓存中获取序列化后的对象,反序列化后返回;
-
若二级缓存中无对应数据,再检查当前一级缓存(SqlSession内):若缓存存在,直接获取对象引用返回,无需访问数据库;
-
若一级缓存也无对应数据,执行SQL查询数据库,获取结果集;
-
查询结果会先存入一级缓存(供当前SqlSession后续复用);
-
当当前SqlSession关闭时,一级缓存中的数据会被同步到二级缓存(若该Mapper开启了二级缓存),供其他SqlSession复用。
6. 二级缓存的扩展场景(面试加分)
(1)自定义二级缓存实现(替代默认缓存)
Mybatis的二级缓存支持自定义实现,默认使用PerpetualCache(内存缓存),但在分布式项目中,内存缓存无法跨节点共享,需集成第三方缓存(如Redis、Ehcache),实现步骤:
-
导入第三方缓存依赖(如Redis相关依赖);
-
自定义缓存类,实现Mybatis的Cache接口(重写get、put、remove、clear等方法,关联第三方缓存);
-
在Mapper.xml的cache标签中,指定自定义缓存类:
<cache type="com.example.cache.RedisCache"/>
- 核心说明:自定义缓存需保证线程安全,同时处理缓存的过期、回收逻辑,避免缓存雪崩、缓存穿透问题。
(2)局部关闭二级缓存
若某个Mapper开启了二级缓存,但其中部分SQL查询不需要使用二级缓存(如实时性要求高的数据),可在SQL标签中添加useCache="false"关闭局部缓存:
(3)刷新二级缓存(手动触发)
若需要手动刷新二级缓存(如批量操作后,避免缓存数据不一致),可在SQL标签中添加flushCache="true",执行该SQL后会自动清空当前Mapper的二级缓存:
<!-- 执行该更新后,清空当前Mapper的二级缓存 -->
<update id="batchUpdate" flushCache="true">
update user set status = 1 where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
7. 缓存的注意事项及避坑点(面试必背)
(1)核心注意事项
-
缓存仅适用于「查询频繁、修改较少」的数据(如字典表、配置表),对于实时性要求高的数据(如订单表、用户余额),不建议使用二级缓存,避免缓存数据与数据库数据不一致;
-
二级缓存的序列化要求:缓存的Java实体类必须实现Serializable接口,否则会抛出SerializationException异常;
-
多表关联查询的缓存问题:若关联查询涉及多个Mapper,二级缓存无法跨Mapper共享数据,可能导致缓存失效,建议使用一级缓存或自定义全局缓存;
-
分布式场景下,避免使用Mybatis自带的二级缓存(内存缓存),需集成Redis等分布式缓存,确保跨节点缓存共享。
(2)常见坑点及解决方案
- 坑点1:一级缓存导致的脏数据(多SqlSession操作同一数据)
-
场景:SqlSession1查询数据存入一级缓存,SqlSession2修改该数据并提交,SqlSession1再次查询时,仍从一级缓存获取旧数据;
-
解决方案:① 每次查询后手动调用SqlSession.clearCache()清空一级缓存;② 避免长时间持有SqlSession,用完即关闭;③ 对实时性要求高的查询,使用useCache="false"跳过一级缓存。
- 坑点2:二级缓存未开启却报序列化异常
-
原因:虽然未显式开启全局二级缓存,但Mybatis默认cacheEnabled="true",若Mapper.xml中添加了cache标签,仍会启用二级缓存,未实现Serializable接口则报错;
-
解决方案:① 不需要二级缓存时,删除Mapper.xml中的cache标签;② 需使用二级缓存时,确保实体类实现Serializable接口。
- 坑点3:二级缓存缓存了null值,导致后续查询一直返回null
-
场景:查询某个不存在的数据,返回null,Mybatis会将null值存入二级缓存,后续查询该数据时,直接返回null,不会访问数据库;
-
解决方案:① 配置二级缓存的eviction策略(如LRU),及时回收null值缓存;② 在查询方法中判断返回值,若为null,手动移除缓存;③ 避免查询不存在的数据(提前做参数校验)。
- 坑点4:Spring集成Mybatis后,一级缓存失效
-
原因:Spring集成Mybatis时,默认会为每个Service方法创建一个SqlSession,方法执行完毕后自动关闭SqlSession,导致一级缓存无法复用;
-
解决方案:① 开启Spring的事务管理,确保同一个事务中使用同一个SqlSession;② 手动控制SqlSession的生命周期(不推荐,易造成资源泄露);③ 对需要复用缓存的查询,使用二级缓存。
扩展补充:
-
Mybatis缓存与Spring缓存的区别:Mybatis缓存是基于SQL语句的缓存,仅作用于持久层;Spring缓存是基于方法的缓存,作用于Service层,可缓存任意方法的返回值,两者可配合使用(如Spring缓存缓存Service方法结果,Mybatis缓存缓存SQL查询结果);
-
分布式缓存集成推荐:生产环境中,优先使用Redis作为Mybatis的二级缓存,配合Spring Cache注解(@Cacheable、@CacheEvict),实现缓存的统一管理,避免缓存一致性问题。
2. Mybatis的resultType和resultMap的区别是什么?各自的使用场景是什么?(实战重点)
核心答案:resultType和resultMap都是Mybatis中用于将SQL查询结果集映射为Java实体类的配置方式;resultType用于「简单映射场景」,直接指定返回值类型(实体类、基本类型、Map等),要求SQL查询字段名与Java实体类属性名完全一致(或开启驼峰命名映射);resultMap用于「复杂映射场景」,可自定义字段与属性的映射关系,支持一对一、一对多关联映射,解决字段名与属性名不一致、复杂关联查询等问题,灵活性更高。
原理解析:
1. resultType(简单映射)
(1)核心特点
-
映射逻辑:Mybatis会自动将SQL查询结果集中的字段名,与指定的resultType类型(实体类、Map等)的属性名进行匹配,若字段名与属性名完全一致(或开启驼峰命名映射,如数据库字段user_name → 实体类属性userName),则自动完成映射;
-
适用场景:字段名与属性名一致(或可通过驼峰映射匹配)、无复杂关联查询(一对一、一对多)、返回值为基本类型(如Integer、String)、Map类型的简单查询;
-
局限性:无法自定义字段与属性的映射关系,无法处理复杂关联查询,无法解决字段名与属性名不一致(且未开启驼峰映射)的问题。
(2)示例(三种常见用法)
- 返回实体类(字段名与属性名一致):
<!-- User实体类:id(Integer)、name(String)、age(Integer);数据库表user:id、name、age -->
<select id='selectUserById' resultType="User">
select * from user where id = #{id}
</select>
- 返回基本类型(统计数量):
<select id='countUserByAge' resultType="int">
select count(1) from user where age = #{age}
</select>
- 返回Map类型(字段名作为key,字段值作为value):
<select id='selectUserById' resultType="map">
select * from user where id = #{id}
</select>
2. resultMap(复杂映射)
(1)核心特点
-
映射逻辑:通过resultMap标签自定义SQL字段与Java实体类属性的映射关系,可手动指定字段名(column)与属性名(property)的对应关系,支持关联映射、级联查询;
-
适用场景:字段名与属性名不一致(且未开启驼峰映射)、有复杂关联查询(一对一、一对多)、需要自定义映射规则(如类型转换)的场景;
-
优势:灵活性高,可解决resultType无法处理的复杂映射问题,支持关联查询的懒加载和立即加载。
(2)核心标签及属性
<resultMap id="UserResultMap" type="com.example.entity.User">
<!-- 主键映射:column是数据库字段名,property是实体类属性名 -->
<id column="user_id" property="id"/>
<!-- 普通字段映射 -->
<result column="user_name" property="name"/>
<result column="user_age" property="age"/>
<!-- 一对一关联映射(association标签) -->
<association property="card" javaType="com.example.entity.IdCard">
<id column="card_id" property="id"/>
<result column="card_no" property="cardNo"/>
</association>
<!-- 一对多关联映射(collection标签) -->
<collection property="orders" ofType="com.example.entity.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
</collection>
</resultMap>
核心标签说明:
-
id:用于映射主键字段,Mybatis会将主键字段作为缓存的key的一部分,提升缓存效率;
-
result:用于映射普通字段,column(数据库字段名),property(实体类属性名);
-
association:用于一对一关联映射,property(实体类中关联对象的属性名),javaType(关联对象的类型);
-
collection:用于一对多关联映射,property(实体类中关联集合的属性名),ofType(集合中元素的类型)。
(3)示例(复杂映射场景)
场景:数据库表user(user_id、user_name、user_age、card_id),id_card(card_id、card_no),用户与身份证是一对一关系,字段名与实体类属性名不一致,使用resultMap映射:
- 实体类:
// User实体类
public class User {
private Integer id; // 对应数据库user_id
private String name; // 对应数据库user_name
private Integer age; // 对应数据库user_age
private IdCard card; // 一对一关联身份证
// getter/setter省略
}
// IdCard实体类
public class IdCard {
private Integer id; // 对应数据库card_id
private String cardNo; // 对应数据库card_no
// getter/setter省略
}
- Mapper.xml配置:
<!-- 定义resultMap,映射User和IdCard -->
<resultMap id="UserWithCardResultMap" type="com.example.entity.User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
<result column="user_age" property="age"/>
<!-- 一对一关联映射,通过card_id关联 -->
<association property="card" javaType="com.example.entity.IdCard">
<id column="card_id" property="id"/>
<result column="card_no" property="cardNo"/>
</association>
</resultMap>
<!-- 关联查询,使用resultMap -->
4. 关联映射的进阶(面试扩展)
(1)一对一关联映射的两种方式
- 嵌套查询(懒加载):通过子查询获取关联对象,可开启懒加载(在mybatis-config.xml中配置lazyLoadingEnabled="true"),只有当访问关联对象时,才执行子查询;
示例:
<resultMap id="UserWithCardLazyResultMap" type="com.example.entity.User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
<result column="user_age" property="age"/>
<!-- 嵌套查询,select指定子查询的id,column指定关联字段 -->
<association property="card" javaType="com.example.entity.IdCard" select="selectCardById" column="card_id"/>
</resultMap>
<!-- 子查询:根据card_id查询身份证信息 -->
<!-- 主查询:查询用户信息 -->
- 嵌套结果(立即加载):通过一次关联查询获取所有数据,Mybatis自动将结果映射到关联对象,无需执行子查询,性能更高;
示例:即前文“复杂映射场景”中的示例,通过left join一次查询获取用户和身份证信息。
(2)一对多关联映射的两种方式
-
嵌套查询(懒加载):通过子查询获取关联集合,开启懒加载后,只有访问集合时才执行子查询;
-
嵌套结果(立即加载):通过一次关联查询(left join)获取主表和从表数据,Mybatis自动将从表数据封装为集合,映射到主表实体类的集合属性中。
扩展补充:
-
避坑点1:使用resultType时,若字段名与属性名不一致且未开启驼峰映射,会导致属性值为null;解决方案:① 开启驼峰映射(mapUnderscoreToCamelCase="true");② 使用SQL别名(如select user_id as id from user);③ 改用resultMap;
-
避坑点2:关联映射时,若未正确配置column属性(关联字段),会导致关联对象为null;解决方案:确保column属性与数据库关联字段名一致;
-
性能优化:嵌套查询(懒加载)适合关联数据访问频率低的场景,可减少不必要的查询;嵌套结果(立即加载)适合关联数据访问频率高的场景,减少数据库查询次数;
-
类型转换器:若需要处理特殊类型映射(如数据库的datetime → Java的LocalDateTime),可自定义类型转换器(实现TypeHandler接口),在resultMap中通过typeHandler属性指定。
3. Mybatis的插件机制是什么?常用插件有哪些?如何自定义Mybatis插件?(进阶考点)
核心答案:Mybatis的插件机制是Mybatis提供的扩展能力,基于JDK动态代理和责任链模式,允许开发者在不修改Mybatis核心源码的前提下,拦截Mybatis的核心方法(如SQL执行、参数处理、结果集映射),实现自定义功能(如日志记录、性能监控、分页、SQL注入防护);常用插件有PageHelper(分页插件)、Mybatis Log(日志插件)、Mybatis-Plus插件等;自定义Mybatis插件需实现Interceptor接口,重写拦截方法,配置插件拦截的目标方法,然后在Mybatis核心配置文件中注册插件。
原理解析:
1. 插件的核心原理(面试必背)
Mybatis的插件本质是「动态代理+责任链模式」,核心拦截的是Mybatis的四大核心处理器(Executor、ParameterHandler、ResultSetHandler、StatementHandler)的方法,具体流程:
-
开发者自定义插件,实现Interceptor接口,指定拦截的目标处理器和目标方法;
-
Mybatis启动时,将插件注册到Configuration对象中;
-
当创建四大核心处理器实例时,Mybatis通过Plugin类(Mybatis提供的插件代理工具类)为处理器创建动态代理对象;
-
当执行被拦截的方法时,会先触发插件的intercept()方法,执行自定义逻辑,然后再执行目标方法(或拦截目标方法);
-
多个插件可形成责任链,按注册顺序依次执行拦截逻辑。
(1)核心拦截对象(四大处理器)及可拦截方法
- Executor:执行器,负责SQL执行和缓存管理,可拦截的核心方法:
-
update():执行insert、update、delete操作;
-
query():执行select查询操作;
-
commit()、rollback():事务提交、回滚。
- ParameterHandler:参数处理器,负责参数设置,可拦截的核心方法:
- setParameters():将Java参数设置到PreparedStatement的?占位符中。
- ResultSetHandler:结果集处理器,负责结果集映射,可拦截的核心方法:
- handleResultSets():将ResultSet转换为Java实体类。
- StatementHandler:语句处理器,负责创建Statement、执行SQL,可拦截的核心方法:
-
prepare():创建Statement对象;
-
execute():执行SQL语句。
(2)Plugin类的核心作用
Plugin类是Mybatis插件的核心工具类,主要作用:
-
判断插件是否拦截目标对象(通过Interceptor的@Intercepts注解);
-
为目标对象创建动态代理对象(实现InvocationHandler接口);
-
触发插件的intercept()方法,执行拦截逻辑。
2. 常用Mybatis插件(面试必知)
(1)PageHelper(最常用,分页插件)
-
核心作用:简化分页查询,自动拼接分页SQL(如limit、offset),无需手动编写分页逻辑,支持多种数据库(MySQL、Oracle、SQL Server等);
-
集成步骤:
- 导入依赖(Maven):
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.2</version>
</dependency>
- 在mybatis-config.xml中配置插件:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 配置分页参数合理化,避免页码超出范围 -->
<property name="reasonable" value="true"/>
<!-- 配置支持的数据库类型 -->
<property name="helperDialect" value="mysql"/>
<!-- 配置是否支持分页插件自动检测数据库类型 -->
<property name="autoDialect" value="true"/>
</plugin>
</plugins>
- 实战使用:
// 分页查询,第1页,每页10条数据
PageHelper.startPage(1, 10);
// 执行查询,PageHelper会自动拼接分页SQL
List<User> userList = userMapper.selectAll();
// 封装分页结果(总条数、总页数等)
PageInfo<User> pageInfo = new PageInfo<>(userList);
System.out.println("总条数:" + pageInfo.getTotal());
System.out.println("总页数:" + pageInfo.getPages());
(2)Mybatis Log(日志插件)
-
核心作用:将Mybatis执行的SQL语句、参数、执行时间等信息打印到日志中,方便开发调试(替代手动打印SQL);
-
集成方式:导入依赖(如mybatis-log-plugin),或在mybatis-config.xml中配置logImpl(如LOG4J2、SLF4J),配合日志框架(如Logback)使用。
(3)Mybatis-Plus插件(后续Mybatis-Plus部分详细讲解)
核心作用:基于Mybatis扩展,提供分页、乐观锁、逻辑删除、自动填充等功能,简化开发。
3. 自定义Mybatis插件(面试加分,实战能力)
(1)自定义插件的步骤(面试必背)
-
实现Interceptor接口,重写3个核心方法:intercept()、plugin()、setProperties();
-
为插件添加@Intercepts注解,指定拦截的目标对象(四大处理器)和目标方法;
-
在mybatis-config.xml中注册插件;
-
测试插件功能。
4. 插件的注意事项及避坑点(面试必背)
(1)核心注意事项
-
拦截范围:插件只能拦截Mybatis的四大核心处理器(Executor、ParameterHandler、ResultSetHandler、StatementHandler)的方法,不能拦截Mapper接口的方法;
-
责任链顺序:多个插件注册时,按配置顺序依次执行拦截逻辑,先注册的插件先执行;
-
性能影响:插件会额外增加执行逻辑,过多插件会影响Mybatis的执行性能,建议只保留必要的插件;
-
兼容性:自定义插件时,需注意Mybatis的版本兼容性,不同版本的核心方法可能存在差异。
(2)常见坑点及解决方案
- 坑点1:插件未生效,未拦截到目标方法
-
原因:① @Intercepts注解配置错误(type、method、args不匹配);② 未在mybatis-config.xml中注册插件;③ 拦截的方法参数与目标方法参数不一致;
-
解决方案:① 检查@Intercepts注解的配置,确保type是四大处理器之一,method是目标方法名,args是目标方法的参数类型数组;② 确认插件已在mybatis-config.xml中注册;③ 核对目标方法的参数,确保与args一致。
- 坑点2:插件拦截逻辑异常,导致SQL执行失败
-
原因:① 拦截逻辑中修改了目标方法的参数,导致参数不匹配;② 未调用invocation.proceed()方法,导致目标方法未执行;
-
解决方案:① 避免随意修改目标方法的参数,若需修改,需确保参数类型和格式正确;② 必须在intercept()方法中调用invocation.proceed(),执行目标方法(否则SQL无法执行)。
- 坑点3:多个插件冲突,导致功能异常
-
原因:多个插件拦截同一个目标方法,执行顺序不当或逻辑冲突;
-
解决方案:① 调整插件的注册顺序,避免冲突;② 简化插件逻辑,避免多个插件对同一方法进行拦截;③ 若必须拦截,确保插件逻辑互不影响。
扩展补充:
-
插件的应用场景:日志记录、性能监控、分页处理、SQL注入防护、参数加密/解密、结果集加密/解密等;
-
生产环境建议:避免自定义过多插件,优先使用成熟的开源插件(如PageHelper),减少开发成本和维护成本;
-
源码关联:Mybatis的插件机制核心源码在org.apache.ibatis.plugin包下,包括Interceptor、Plugin、Invocation等类,面试时可简单提及源码结构,提升竞争力。