MyBatis 延迟加载、一二级缓存、架构设计的面试题(常问,重点了解)
MyBatis 的面试题整理快接近尾声了,我对着问题写了 6 个 Demo,把 MyBatis 的源码翻了几遍,收获满满。不知道你看的有没有收获?可以在文末参与讨论。Let's go on...
1、MyBatis 的 SQL 执行日志如何开启?
这是之前遗留的一个问题。
在配置文件的 <setting>标签上设置 logImpl 参数值(SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING),指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
如 MyBatis 实现的标准输出的配置
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"></setting>
</settings>
2、MyBatis 中注册 Mapper 有哪些方式?
方式一:在配置文件 mybatis-config.xml 中添加
及其子标签 ,编写对应的 Mapper 接口与 XML
<mappers>
<mapper resource="constxiong/mapper/UserMapper.xml"/>
</mappers>
方式二、硬编码方式在 configuration 对象中注册 Mapper 接口
//配置
Configuration configuration = new Configuration(environment);
//注册
configuration.addMapper(UserMapper.class);
3、MyBatis 如何支持延迟加载?现实原理是什么?
支持延迟加载的配置:
在配置文件的标签内设置参数
lazyLoadingEnabled:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态
aggressiveLazyLoading:开启时,任一方法的调用都会加载该对象的所有延迟加载属性。否则,每个延迟加载属性会按需加载
lazyLoadTriggerMethods:指定对象的哪些方法触发一次延迟加载
resultMap 中配置 <association> 或 <collection>
配置与测试示例
//配置文件
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="lazyLoadTriggerMethods" value=""/>
</settings>
//Mapper xml
<select id="selectUserWithLazyInfo" resultMap="UserWithLazyInfo">
select * from user where id = 1
</select>
<resultMap id="UserWithLazyInfo" type="constxiong.po.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<association property="info" javaType="constxiong.po.Info" select="constxiong.mapper.InfoMapper.selectInfoByUserId" column="id"/>
</resultMap>
//InfoMapper
public interface InfoMapper {
@Select("select * from info where user_id = #{userId}")
@Results(value = {@Result(column="user_id", property = "userId")})
Info selectInfoByUserId(int userId);
}
//测试代码
System.out.println("------ selectUserWithLazyInfo ------");
User user = userMapper.selectUserWithLazyInfo();
System.out.println(user);
System.out.println(user.getInfo());
//打印 User 对象里的 Info 为空,使用 getInfo 能够查询对应的值
------ selectUserWithLazyInfo ------
User{id=1, name='ConstXiong1', mc='null', info=null, articles=null}
Info{userId=1, name=大熊}
实现原理:
支持延迟加载是通过字节码增强实现的,MyBatis 3.3 及以上默认使用了 javassist,3.3 以前使用 cglib 实现。
我本地用的 MyBatis 3.5.5,使用了 javassist 增强,核心源码如下
//DefaultResultSetHandler getRowValue 获取每条的查询数据,resultMap 中如果包含懒加载 rowValue 在 createResultObject 方法通过 javassist 代理增强
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
//对象数据,通过 javassist 代理增强
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
//根据从数据库查询到的 resultSet,根据 resultMap 通过反射设置 rowValue 的值
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this.useConstructorMappings = false; // reset previous mapping result
final List<Class<?>> constructorArgTypes = new ArrayList<>();
final List<Object> constructorArgs = new ArrayList<>();
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
//如果返回对象的属性中包含懒加载,使用 javassist 代理增强,当设置属性值时被代理到 JavassistProxyFactory 的 invoke 方法
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
//JavassistProxyFactory 的 invoke 方法
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
if (WRITE_REPLACE_METHOD.equals(methodName)) {
...
} else {
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
} else if (PropertyNamer.isSetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property);
} else if (PropertyNamer.isGetter(methodName)) {
//测试代码中 user.getInfo() 方法的调用,在此执行懒加载查询关联 SQL 设置 info 属性
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
lazyLoader.load(property);
}
}
}
}
}
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
完整 Demo:
https://javanav.com/val/973ded541e9244aa8b3169b9fb869d60.html
4、MyBatis 中的本地缓存和二级缓存的作用是什么?怎么实现的?
本地缓存
作用:
SqlSession 级别的缓存,默认开启,在 MyBatis 配置文件中可以修改 MyBatis 文件中 <setting> 标签 localCacheScope 参数值改变缓存的作用域。statementId、boundSql.getSql() 执行 sql、查询参数、RowBounds 都相同,即认为是同一次查询,返回缓存值。
实现原理:
每个 SqlSession 对象包含一个 Executor 对象,Executor 对象中 localCache 属性使用 PerpetualCache 对象缓存查询数据;从源码中看 DefaultSqlSession 的 close、commit、rollback、insert、delete、update 相关的方法都会触发 BaseExecutor 对象清掉缓存。
二级缓存
作用:
MappedStatement 级别的缓存,默认不开启,可以在 Mapper xml 中通过 <cache> 标签开启 或者 MyBatis 文件中 <setting> 标签设置 cacheEnabled 参数为 true 全局开启 或者 mapper xml 配置文件中的 select 节点需要加上属性 useCache,在 SqlSession 关闭或提交之后才会生效。
开启二级缓存的默认作用摘自官网
映射语句文件中的所有 select 语句的结果将会被缓存。
映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
缓存不会定时进行刷新(也就是说,没有刷新间隔)。
缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
实现原理:
XMLMappedBuilder 解析 Mapper xml 中的 <cache>、<cache-ref> 标签
通过 builderAssistant 对象 addMappedStatement 方法,设置 cache 信息到 MappedStatement 对象内
CachingExecutor 对象的 query 方法先 MappedStatement 对象中 getCache() 获取缓存 Cache 对象,如果没有查到则到 BaseExecutor 中查询,走本地缓存逻辑
5、MyBatis 的源码中的核心类有哪些?如何实现框架功能的?
Configuration: 配置类
Environment: 环境信息
SqlSessionFactoryBuilder: SqlSessionFactory 构造者类
SqlSessionFactory: SqlSession 工厂类
SqlSession: 执行 SQL 的一次会话
XMLConfigBuilder: MyBatis xml 配置文件构造者类
XMLMapperBuilder: Mapper xml 配置文件构造者类
MapperBuilderAssistant: Mapped 匹配信息构造者类,如构造添加MappedStatement
XMLStatementBuilder: Mapper xml 配置文件中 SQL 标签的构造者类,构造 MappedStatement
MappedStatement: 通过 Mapper xml 或注解,生成的 select|update|delete|insert Statement 的封装
MapperProxy: Mapper 接口的代理类
MapperMethod: Mapper 接口的方法,包含匹配的 SQL 执行种类和具体方法签名等信息
Executor: 执行器,是 MyBatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护
BaseExecutor: SqlSession 中的基本数据库的增删改查的执行器,涉及本地缓存与数据库查询
CachingExecutor: 带缓存的执行器,涉及二级缓存,未命中走本地缓存逻辑
ResultMap: 返回值类型匹配的类
SqlSource: 负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回该对象
BoundSql: 动态生成的 SQL 语句以及相应的参数信息
StatementHandler: Statement 处理接口,封装 JDBC Statement 操作
ParameterHandler: 参数处理接口,负责对用户传递的参数转换成 JDBC Statement 所需要的参数
ResultSethandler: 执行结果处理接口
TypeHandler: 返回类型处理接口
…
框架如何实现,这个问题的细节就特别多了,画了一张我个人理解的图
MyBatis 插件的运行原理是什么?如何编写一个自定义插件?
MyBatis 是如何被 Spring 集成的?
-
Spring 中如何配置 MyBatis?
留着下回分解...